Programs

A program 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 — and because the plan is a value, you can read it, save it under a name, and run it again with different inputs.

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 — "$prs", "${prs[0].title}" — and the runner substitutes before dispatch. For reshaping, two core ops do the work: core.let binds a CEL expression, core.transform runs a jq filter over any binding — group, reduce, slice, build new objects.

Control flow is part of the language

  • core.if — a CEL condition with then / else regions.
  • core.map — run a body over every item, with maxConcurrency and per-item error policy.
  • flow.parallel — independent branches at once, failFast optional.
  • Per-op onError (halt, continue, or a default value) and retry with backoff.

The plan carries its own branching, so a conditional isn't a second round-trip to the model — it's a line in the program.

Functions have inputs and sealed scopes

A program 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 program (no inputs) sees cross-turn bindings normally, which is the convenient form for one-shots.

Save it by name, call it like a tool

save_program stores the function form under a name; list_programs, read_program, and archive_program manage the library. Any program can then invoke it with core.call — the inner body runs sealed, and the call's id binds to the inner program's bindings. A saved program is a tool you taught the system yesterday.

Every run leaves a trace

Each tool op dispatches through the normal call path, so a program run lands in the job's trace like any other work: every op a span, every cost counted, every write gated by approvals.

What's next

  • Traces — the record a run leaves.
  • Jobs — where programs execute.
  • Assets — what programs act on and produce.

Updated 2026-07-18