Flow control
The runner controls all step execution and flow decisions. Agents never
decide what happens next — they only report findings and score confidence.
For each step:
- Evaluate optional
when:condition — if false, skip the step entirely - Parse and validate the LLM response as
LLMOutputvia Pydantic - Run verifier if configured and trigger fires
- Check
effective_confidenceagainstconfidence_threshold - If below threshold, apply
on_low_confidenceaction (escalate | abort | proceed) - If confidence passes, check
proceed:proceed: false→ pipeline stops with statusstoppedproceed: true→ continue to next step
- If the executor raised an error (step status =
failed), checkon_failure:on_failure: abort(default) → abort the pipelineon_failure: continue→ log and continue to next step- If
on_failure.webhookis set, fire that callback regardless of policy
- Record step result to SQLite before proceeding
Step and run status reference
Section titled “Step and run status reference”Step statuses:
| Status | Set when | Counts as failure? |
|---|---|---|
completed |
Step ran successfully and proceed: true |
No |
stopped |
Step ran successfully and returned proceed: false |
No |
escalated |
Confidence below threshold and on_low_confidence: escalate |
No |
aborted |
Confidence below threshold and on_low_confidence: abort |
No |
failed |
Executor exception, timeout, bad JSON, or schema validation error | Yes |
For agent success rate calculations, failed is the only status that counts
against a model.
Pipeline run statuses:
| Status | Meaning |
|---|---|
completed |
All steps ran to completion |
stopped |
A step returned proceed: false — clean intentional stop |
escalated |
A step was escalated — run halted, human notified |
aborted |
A step aborted due to low confidence |
failed |
A step raised an unhandled error |
running |
Currently in progress |
interrupted |
Service restarted/crashed mid-run — set by the startup sweep, not the runner. A pipeline with durable: set resumes instead of landing here, unless the guard conditions in Durability & resume rule it out |
Conditional steps (when:)
Section titled “Conditional steps (when:)”Any sequential step or parallel group can have an optional when: field
containing a Jinja2-compatible boolean expression. A false result skips the
step cleanly — no DB row, no executor call, invisible to subsequent steps.
steps: - name: triage executor: openclaw executor_config: agent: sre-triage prompt_template: | Analyse the alert. Return JSON with an "action" field: "remediate" | "escalate_human" | "ignore"
- name: auto-remediation when: "steps.triage.action == 'remediate'" executor: openclaw executor_config: agent: sre-remediator prompt_template: | Triage says: {{steps.triage.summary}}. Attempt remediation.
- name: page-oncall when: "steps.triage.action == 'escalate_human'" executor: human prompt_template: | <b>Page oncall?</b> Triage: {{steps.triage.summary}}when: vs proceed: false:
when:— pipeline author decides in advance which steps are relevant given prior step outputsproceed: false— the agent signals the pipeline is complete and no further steps are warranted
Refinement loops (loop_until)
Section titled “Refinement loops (loop_until)”loop_until re-runs a step until its effective confidence reaches a target, giving the agent a
chance to correct itself:
- name: draft-summary executor: gateway executor_config: { agent: writer } loop_until: confidence: 0.85 # stop once effective confidence is at least this max_iterations: 3 # default 3; the loop never runs more than this prompt_template: | Summarise the incident. {% if loop.iteration > 1 %} Your previous answer scored {{ loop.prior_confidence }}. Previous output: {{ loop.prior_output.summary }} Fix what was weak. {% endif %}Each iteration’s template gets a loop object:
| Variable | Value |
|---|---|
loop.iteration |
1, 2, 3 … |
loop.max_iterations |
the configured limit |
loop.prior_confidence |
the previous iteration’s effective confidence (after verifier and grounding), or None on the first iteration |
loop.prior_output |
the previous iteration’s output as a dict (summary, next_step_context, reasoning, any extra fields), or None on the first iteration |
The loop stops as soon as the effective confidence meets confidence, or after max_iterations.
Only the last iteration’s output is saved and passed on. Every iteration costs a full agent
call, and the run log records loop_iteration, then loop_converged or loop_exhausted. If it is
exhausted, the step carries on with its last result and is then judged against the step’s own
confidence_threshold like any other step. Guard loop.prior_* with loop.iteration > 1, since they
are None on the first pass.
Referencing earlier steps in templates
Section titled “Referencing earlier steps in templates”Later steps read earlier outputs as steps.<name>.<field>. A step name with a hyphen cannot be
written with dot notation (Jinja reads - as subtraction), so every step is available under both its
original name and a copy with hyphens turned into underscores:
{{ steps.slice_inputs.summary }} {# hyphen → underscore #}{{ steps['slice-inputs'].summary }} {# original name, bracket form #}Branches of a parallel group are addressed by their own names. Branches of a fan_out are named
<fan_out name>/<index> and need the bracket form: {{ steps['check-upstreams/0'].summary }}. The
group as a whole is not a step output, so use the branches.
Per-step failure policy (on_failure)
Section titled “Per-step failure policy (on_failure)”By default a failed step (executor exception, timeout) aborts the pipeline. For non-critical steps — enrichment lookups, external API calls, notifications — you can allow failures to pass through:
- name: enrich-from-cmdb executor: webhook on_failure: continue # pipeline keeps running if this step fails executor_config: url: "https://cmdb.internal/api/enrich"The step is still recorded in run history with status: failed and the
error message is available in step_outputs[name].summary for downstream
when: conditions or prompt templates.
A step that fails with policy: continue still counts toward the
budget — its spend is added and
the budget is checked straight after, so a pipeline whose steps keep failing and
continuing can still be stopped by budget.max_usd.
on_failure only applies to executor errors. Low-confidence results that
trigger on_low_confidence: abort are LLM-quality decisions and are always
pipeline-stopping regardless of this setting.
Step-level failure webhook callback
Section titled “Step-level failure webhook callback”Attach a webhook to any step that fires when that step fails, without adding a separate notify step to the pipeline. The callback fires before the pipeline decides whether to abort or continue, so it always goes out regardless of the policy.
- name: triage executor: gateway executor_config: agent: sre-triage on_failure: policy: continue # pipeline continues even if this step fails webhook: url: "${PAGERDUTY_URL}" headers: Authorization: "Token ${PAGERDUTY_TOKEN}" payload: summary: "Triage step failed: {{step_failure.summary}}" severity: criticalon_failure as a block:
| Field | Default | Description |
|---|---|---|
policy |
abort |
abort or continue — what the pipeline does after the failure |
webhook.url |
required | Outbound URL (${ENV_VAR} expansion supported) |
webhook.method |
POST |
HTTP method |
webhook.headers |
{} |
Header dict; values support ${ENV_VAR} expansion |
webhook.payload |
{} |
JSON body dict; string values are Jinja2 templates |
webhook.timeout_seconds |
30 |
Request timeout |
String shorthand (on_failure: continue or on_failure: abort) is
equivalent to setting policy only with no webhook.
The Jinja2 context for the webhook payload includes all standard step context
variables plus step_failure.step, step_failure.summary, and
step_failure.status. Webhook delivery failures are logged as a
step_failure_webhook_failed run-log event and never abort the pipeline.
Retry logic
Section titled “Retry logic”Optional retry: block on any step. Retries wrap the executor call only —
low-confidence results are valid outputs and are never retried.
retry: attempts: 3 backoff: exponential # fixed | exponential delay_seconds: 2.0| Field | Default | Description |
|---|---|---|
attempts |
3 |
Total attempts, including the first — attempts: 3 means up to 2 retries. |
backoff |
exponential |
fixed (always delay_seconds) or exponential (delay_seconds doubling each attempt). |
delay_seconds |
1.0 |
Base delay between attempts — the example above raises it to 2.0. |
Two details worth knowing:
- A reply cut off at the model’s output-token limit is not retried. The same request would truncate the same way, so retrying only pays for it again; the step fails straight away with an output truncated error.
- Failed attempts are recorded and costed. Every attempt that reached the model adds its tokens and cost to the one step row, and the last attempt’s trace and prompt are kept, so a step that failed after three tries shows what all three cost. An attempt that never reached the model (a timeout or connection error) has no tokens to count.