Scripts
A script is the agent's plan as an artifact. Instead of one tool call at a time, the agent emits a list of ops with explicit data flow. Because the plan is a value, you can read it before it runs and re-read it after.
The agent emits the whole plan at once
One run_program call carries the full sequence. Ops run in order, each result can be named, and the runner reports back what completed, so the model never repeats a write that already happened.
run_program({
program: [
{ op: "github_list_prs", id: "prs", params: { repo: "$repo" } },
{ op: "core.let", id: "open", params: { be: "prs.filter(p, p.state == 'open')" } }
],
inputs: { repo: "acme/api" }
})
Every op has the same shape: op (the tool), an optional id (bind the result to a name), params, and optional onError and retry policies.
Data flows through $-refs
An op's id binds its output; later ops reference it in any string value, like "$prs" or "${prs[0].title}", and the runner substitutes before dispatch. For reshaping, two core ops do the work: core.let binds a CEL expression, and core.transform runs a jq filter over any binding to group, reduce, slice, or build new objects.
Control flow is part of the language
core.if: a CEL condition withthen/elseregions.core.map: run a body over every item, withmaxConcurrencyand per-item error policy.flow.parallel: independent branches at once,failFastoptional.- Per-op
onError(halt,continue, or a default value) andretrywith backoff.
The plan carries its own branching, so a conditional runs without a second round-trip to the model.
Functions have inputs and sealed scopes
A script with inputs runs sealed: the body sees only its declared inputs, the job's history, and names bound by its own ops. That's what makes it a function: same body, different inputs, no accidental dependence on conversation state. A bare script (no inputs) sees cross-turn bindings normally, which is the convenient form for one-shots.
Every run leaves a trace
Each tool op dispatches through the normal call path, so a script run lands in the job's trace like any other work: every op a span, every cost counted, every write gated by approvals.
An op can be addressed to a person
ask takes a to and puts the question to someone by name. The op suspends, the ops that depend on it wait, and everything independent of it keeps running.
{ op: "ask", id: "scope", to: "ana@acme.de",
params: { question: "Extend the pilot to include S/4 writes?",
choices: ["approve", "decline"] } }
Ana answers wherever she is: a card in the scene, a push on her phone, a reply in the channel her workspace uses. Everyone else in the job sees one line, waiting on Ana, and since when. $scope binds to what she picked, so the branch below it is ordinary control flow.
say is the same shape without the wait. Addressed to a person, it delivers a message; with no to, it posts to everyone reading the job. A say never blocks, which is what makes it usable for talk rather than for coordination.
The to accepts a person, a role, or another agent. An agent addressed by ask answers by running, which is why prompting the agent and asking a colleague are the same op with different addressees.
A done is a claim until something settles it
An answer records what the actor said, not what is true. That holds for a person tapping done, and it holds for a callback from a system you wired in. What separates them is how much checking the op asks for.
expect names the evidence and the window:
{ op: "ask", id: "swap", to: "jonas@acme.de",
params: { question: "Replace the filter on line 2" } },
{ op: "expect", id: "seated", after: "swap",
params: { evidence: "camera.line_2", check: "new filter seated, housing closed",
within: "10m" } }
The claim and its evidence are two ops, so they can disagree. When they agree the run continues with both on record. When they disagree, or when the window passes with nothing arriving, the expectation misses.
Expectations settle over days
An expect op holds a window rather than a call, so a script can state what should happen next and wait for the world. A five-step onboarding writes all five ops on day one: the welcome that goes out this morning, and the check-in that resolves in two weeks.
{ op: "expect", id: "signin",
params: { evidence: "auth.first_signin", within: "3d", onMiss: "replan" } }
Pending ops are rows in the job, not a process parked in memory, so a run holding a fourteen-day window costs nothing while it waits and stays readable the whole time. Open the job and it shows three tenses at once: what happened, what is waiting on whom, and what the script does next.
What the world can be
evidence names a stream. Anything that arrives in a scene as an event can settle an expectation: a customer's reply on a connected messaging account, a webhook from a system you integrate, a clock, an endpoint the script mints for someone to call, or another script finishing. The op reads the same whichever it is, and the matching event becomes its value, addressed like any other result.
| evidence | settles when |
|---|---|
line:message (or any connected channel) | the next message from that conversation arrives |
<provider>:<event> | a webhook from that integration lands in the scene |
timer | the clock passes the window |
http | something POSTs to the URL the op hands back before it waits |
job | the named script reaches its end |
A booking script, read top to bottom. The customer tapped Book in the account's menu; the script sends the schedule, then waits for them:
[
{ op: "line_send", to: "$tap.conversation",
params: { messages: [{ type: "card", title: "Today at CUBIC", buttons: "$schedule" }] } },
{ op: "expect", id: "pick",
params: { evidence: "line:message", from: "$tap.conversation", within: "10m" },
onMiss: [{ op: "line_send", to: "$tap.conversation",
params: { messages: [{ type: "text", text: "Still want to book? Reply with a time." }] } }] },
{ op: "book_class", params: { slot: "$pick.event.text" } },
{ op: "line_send", to: "$tap.conversation",
params: { messages: [{ type: "text", text: "Booked. See you at $pick.event.text." }] } }
]
The run pauses at pick for as long as the customer takes, and the job shows it waiting on that conversation. When the reply comes, the event is pick's value and the next op is ready; if ten minutes pass instead, the branch you wrote runs. Nothing about the script is different because its evidence came from a phone rather than a sign-in: a step addressed to a person and a step that waits for a message are the same shape of op, settled by whoever or whatever was named.
Every expectation carries a window. An op that waits for the world without one is rejected when the script is written, so a run can never hold an item open forever.
A miss follows the branch you wrote
onMiss names what happens when the window passes, next to the op it belongs to. It takes the same regions as core.if, so the branch is ordinary control flow that was written before the run started:
{ op: "expect", id: "signin",
params: { evidence: "auth.first_signin", within: "3d" },
onMiss: [
{ op: "say", to: "$customer.email", params: { body: "$templates.nudge" } },
{ op: "expect", id: "signin_2", params: { evidence: "auth.first_signin", within: "3d",
onMiss: [{ op: "ask", to: "$owner" }] } }
] }
Read the script and you know what a miss will do before it happens, which is the property that makes a run repeatable: the same script meets the same miss with the same branch.
onMiss: "replan" is the deliberate exception. It hands the agent the overdue expectation along with the rest of the plan and lets it edit the pending ops, for the cases where the right move depends on why the miss happened. What it changes lands in the job's history with its reason and its author, and past ops are untouched, because the plan and the record are the same list.
Retry policy stays available for the mechanical cases (onError, retry with backoff).
People edit a running script the same way. An ask can only be answered by the person it names; handing it to someone else rewrites the op's to, and the step says so.
What's next
- Traces: the record a run leaves.
- Jobs: where scripts execute.
- Approvals: the gate a consequential op waits on.
- Assets: what scripts act on and produce.
Updated 2026-08-15