Skip to content
Talk to our solutions team

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.

The agent: node is sugar that expands into the explicit loop:

name: agent-loop
vars:
model: gateway:agent-default
output: "{{ 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 }}"

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:

GateStops the loop when
max_turnsThe iteration count is reached
max_cost_usdAccumulated spend crosses the ceiling
max_tokensAccumulated tokens cross the ceiling
max_durationWall-clock time is exhausted
no_progressNothing has changed for window turns
task_progressThe 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.

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).

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.Outcome

Model 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.

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 events
h.Signal(runID, harness.Signal{...}) // resume a suspended run
snap, _ := h.Snapshot(runID) // serialize at a resting node
res, _ = h.Resume(ctx, snap, &signal) // reconstitute and continue

This 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.

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.

You needUse
Fixed sequence of steps, known in advanceAn ordinary flow
Model decides what to do next, iterativelyA harness
A conversationAI 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.