Harnesses & Agentic Loops
A harness is a flow with cycles. Where an ordinary flow is a DAG that runs each task once and finishes, a harness can loop — call a model, dispatch a tool, look at the result, and go round again until it is done or a budget stops it.
That loop is the agentic core. It is also the dangerous part, which is why the format treats bounding as a compile-time requirement rather than a runtime hope.
A harness
Section titled “A harness”The agent: node is sugar that expands into the explicit loop:
name: agent-loopvars: model: gateway:agent-defaultoutput: "{{ result.content }}"tasks: - name: act agent: model: "{{ model }}" input: "{{ goal }}" tools: [search, fetch, tasks] # `tasks` enables the built-in plan tool gates: - max_turns: 20 - max_cost_usd: 2.00 - no_progress: { window: 3 } - task_progress: { window: 5 } output: result next: go: summarize - name: summarize print: "agent finished: {{ result.content }}"Bounded cycles
Section titled “Bounded cycles”The compiler rejects any cycle without a bound. A loop must carry either a budget gate
(max_turns, max_cost_usd, max_tokens, max_duration) or a bound: N annotation on its
back-edge.
This is the single most important property of the engine. An unbounded agentic loop is not a bug you find in testing — it is a bug you find on the invoice, or when a run has been spinning for six hours. Making it a compile error means the failure is impossible to deploy rather than merely unlikely.
Gates come in two flavours:
| Gate | Stops the loop when |
|---|---|
max_turns | The iteration count is reached |
max_cost_usd | Accumulated spend crosses the ceiling |
max_tokens | Accumulated tokens cross the ceiling |
max_duration | Wall-clock time is exhausted |
no_progress | Nothing has changed for window turns |
task_progress | The structured plan has not advanced in window turns |
The last two are the interesting ones. A loop can burn its full turn budget going in circles
while every individual turn looks successful; no_progress and task_progress catch the
stall rather than waiting for the budget to run out.
What a harness inherits
Section titled “What a harness inherits”Everything the flow format already does: shell, print, assign, choice, map,
foreach, wait, subworkflow, next:/error: routing, retry: policies, Liquid
templating, iterators and multi-flow files.
New node kinds are model, tool, gate, agent and script. Alongside vars, a run
carries two more pieces of context: threads (conversations) and tasks (the structured
plan the agent is working through).
Running one
Section titled “Running one”h, err := harness.Compile(ctx, yamlBytes, harness.CompileOptions{ Models: registry, // resolves to the AI Gateway in production Tools: router, Gates: gates, Recorder: recorder, // trajectory recording Metadata: meta, // tenant / block / purpose / request})
res, err := h.Run(ctx, map[string]any{"goal": "..."})// res.Output, res.Vars, res.Threads, res.Tasks,// res.Usage, res.CostUSD, res.Trajectory, res.OutcomeModel calls resolve through the AI Gateway, so an agentic loop lands in
the same aliases, budgets, caches and cost accounting as everything else. The harness’s own
max_cost_usd gate and the gateway’s per-tenant budget are independent belts — the gate stops
one run, the budget stops a tenant.
Suspension and resumability
Section titled “Suspension and resumability”Interruption is first-class. Cancel the context and the run stops at the last consistent node
boundary with Outcome = interrupted and a valid snapshot. Node effects are atomic — fully
applied or absent — so there is no half-executed node to reason about on resume.
events, _ := h.RunStream(ctx, input) // node / tool / gate / task eventsh.Signal(runID, harness.Signal{...}) // resume a suspended runsnap, _ := h.Snapshot(runID) // serialize at a resting noderes, _ = h.Resume(ctx, snap, &signal) // reconstitute and continueThis is what makes human-in-the-loop patterns work without special-casing them: a harness that
needs approval suspends at a wait node, the snapshot persists, and a signal resumes it hours
later in a different process.
Trajectories
Section titled “Trajectories”Every run records a trajectory — the ordered sequence of model calls, tool dispatches, gate evaluations and task updates that produced the outcome. It is the artifact that makes agentic behaviour reviewable after the fact, and it is what AI Evals scores when you evaluate a harness rather than a single response.
Judging a final answer tells you whether a run succeeded. Judging the trajectory tells you whether it succeeded for a good reason — which matters when the same harness will run a thousand more times on inputs you have not seen.
When to reach for one
Section titled “When to reach for one”| You need | Use |
|---|---|
| Fixed sequence of steps, known in advance | An ordinary flow |
| Model decides what to do next, iteratively | A harness |
| A conversation | AI Bot, which delegates to a flow for multi-step work |
Prefer a plain flow when the steps are knowable. A harness costs more, is harder to reason about, and is the right answer only when the branching genuinely depends on what the model finds along the way.