---
title: "Flow steps and expressions"
description: "Look up step types, error branches and expression behaviour."
canonical_url: "https://docs.oz0.ai/build/parts/flows/steps-and-expressions"
---
# Flow steps and expressions

> Look up step types, error branches and expression behaviour.

For: plugin authors.

Look up step types, error branches and expression behaviour.

## Steps

Each step has an `id` (lowercase letters, digits and `_`, so expressions can read it as `steps.<id>`) and does one thing:

<table>
<thead>
  <tr>
    <th>
      Step
    </th>
    
    <th>
      Does
    </th>
    
    <th>
      Its output, in <code>
        steps.&lt;id>
      </code>
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        tool: &lt;tool>.&lt;function>
      </code>
      
       with <code>
        input
      </code>
    </td>
    
    <td>
      Calls a tool function on the flow's home node, with the tenant's <a href="https://docs.oz0.ai/build/parts/hooks">
        hooks
      </a>
      
       and <a href="https://docs.oz0.ai/agents/approvals">
        approvals
      </a>
    </td>
    
    <td>
      The tool's structured result, or its text (parsed when it is JSON)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        agent: &lt;name>
      </code>
      
       with <code>
        input
      </code>
    </td>
    
    <td>
      Runs an agent as a child job and waits for its answer
    </td>
    
    <td>
      The agent's answer
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        approval:
      </code>
      
       with <code>
        details
      </code>
      
       and <code>
        timeout
      </code>
    </td>
    
    <td>
      Waits for a person; no answer within <code>
        timeout
      </code>
      
       (default 24 h) is a no
    </td>
    
    <td>
      <code>
        {approved, by, comment}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        output: &lt;name>
      </code>
      
       with <code>
        input
      </code>
    </td>
    
    <td>
      Queues a <a href="https://docs.oz0.ai/build/parts/routines/outputs-and-permissions#safe-outputs">
        routine's safe output
      </a>
      
      , which the edge checks and applies after the flow by default. An output with <code>
        apply: now
      </code>
      
       is checked and applied during the step; only a routine's fire may run it
    </td>
    
    <td>
      <code>
        {queued, message}
      </code>
      
      , or <code>
        {applied, state, result, answer}
      </code>
      
       for <code>
        apply: now
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        wait_for_event:
      </code>
      
       with <code>
        source
      </code>
      
      , <code>
        types
      </code>
      
      , <code>
        subject
      </code>
      
      , <code>
        filter
      </code>
      
      , <code>
        timeout
      </code>
    </td>
    
    <td>
      Waits for an <a href="https://docs.oz0.ai/agents/waiting-for-events">
        event
      </a>
      
       (1 h by default, 7 d at most)
    </td>
    
    <td>
      <code>
        {matched, event}
      </code>
    </td>
  </tr>
</tbody>
</table>

An agent step may also have `best_of:`, which runs the agent as several candidates and keeps one; see [Outcome checks and patterns](https://docs.oz0.ai/improve/outcome-checks/selection#best-of-n).

### A built-in in a step

A `tool:` step may name one of three platform built-ins instead of a plugin's function, and the runtime runs it with the
step's rendered input, without a model:

<table>
<thead>
  <tr>
    <th>
      Built-in
    </th>
    
    <th>
      Does
    </th>
    
    <th>
      Its output
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        oz.report_outcome
      </code>
    </td>
    
    <td>
      Records a <a href="https://docs.oz0.ai/improve/outcome-checks/rollouts#downstream-results">
        downstream result
      </a>
      
       on a finished job of the flow's own plugin
    </td>
    
    <td>
      <code>
        {recorded}
      </code>
      
      , or <code>
        {recorded: false, reason}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        oz.evidence
      </code>
    </td>
    
    <td>
      Reads what a granted plugin's jobs came to, or <a href="https://docs.oz0.ai/improve/evidence-for-agents#where-a-target-is-installed-from">
        where it is installed from
      </a>
    </td>
    
    <td>
      The read's answer, or <code>
        {refused}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        oz.propose_update
      </code>
    </td>
    
    <td>
      Files a <a href="https://docs.oz0.ai/govern/reviewed-changes/proposals#change-requests-a-job-files">
        change request
      </a>
      
       to update an installed plugin, which a person approves
    </td>
    
    <td>
      <code>
        {filed, change, delta_sha256, commit, message}
      </code>
      
      , or <code>
        {filed: false, reason}
      </code>
    </td>
  </tr>
</tbody>
</table>

```yaml [flows/after-merge.yaml]
steps:
  - id: merged
    tool: oz.report_outcome
    input: {job: "${{ steps.job.job }}", outcome: success, name: merged}
  - id: update
    when: steps.merged.recorded
    tool: oz.propose_update
    input: {plugin: notes, note: "${{ 'Pull request ' + string(input.number) + ' was merged.' }}"}
```

Each runs exactly as when an agent calls it: the same checks at the edge (which reads what to do from the job's own history), the tenant's tool policy rules for its name (a refusal fails the step, an approval rule waits for a person), a routine's tool list (a fire calls only the built-ins its routine lists) and the audit. The step's rendered input of `oz.report_outcome` and `oz.propose_update` is scanned for the values of the tenant's secrets before any of these decide it, so one that carries a secret fails the step and never reaches an approval card.

A shadow run stubs `oz.report_outcome` and `oz.propose_update`. Use a step rather than an agent for a call the flow already decides: a model would add cost, and a model the flow's input could steer. `plugin lint` accepts these three names in a step; any other `oz.` name is not a function another plugin provides.

`when:` makes a step conditional; a step whose condition is false is skipped and has no output, which `has(steps.<id>)` tells. Steps run in order. A step that fails, or an approval that is denied, ends the flow with the step's ID in the error, unless a later step reads its failure ([error branches](https://docs.oz0.ai/build/parts/flows/steps-and-expressions#error-branches)).

The flow's answer is `output:`, or the last step's output when there is none.

## Error branches

An agent, tool or approval step that fails does so with a [failure kind](https://docs.oz0.ai/build/parts/contracts-and-limits/failures#failure-kinds): its agent's own, or the platform's (`tool_failed` for a tool call, `denied` for an approval nobody granted). A later step whose `when:` reads `steps.<id>.failure` handles it, and the flow goes on instead of ending:

```yaml [flows/triage.yaml]
steps:
  - id: look
    agent: triage                       # declares not_found and tracker_busy
    input: ${{ input.ticket }}
  - id: label
    tool: github.add_label
    input: {issue: "${{ input.ticket }}", label: "${{ steps.look.team }}"}
  - id: missing
    when: "steps.look.failure.kind == 'not_found'"
    tool: github.comment
    input: {issue: "${{ input.ticket }}", body: "No such issue: ${{ steps.look.failure.message }}"}
  - id: retry_later
    when: steps.look.failure.retryable
    tool: github.add_label
    input: {issue: "${{ input.ticket }}", label: triage-again}
  - id: report
    tool: chat.post
    input: {text: "Triage of ${{ input.ticket }} done"}
```

- What it reads. `steps.<id>.failure` holds `kind`, `retryable`, `partial`, `person`, `declared` and `message` (the agent's words for a kind it declares). A failed step's output is `{failure: ...}`, so `steps.<id>.failure` is the only part a later step can read of it.

### What runs

While a failure is open, only a step whose `when:` reads it may run: `label` above is skipped when `look` fails. The first step that reads it and runs handles it, and the steps after it run as usual (`report`). A step whose `when:` reads the failure of a step that did not fail is skipped. If that step's output has a field named `failure` of its own, `steps.<id>.failure` is that field when the step succeeded, as it always was, and the `when:` decides; `plugin lint` warns about such a flow, since the same words then mean two things.

- Cancelling the flow. A flow cancelled while a step runs ends cancelled: the step's cancellation is not a failure a later step handles, and no step runs after it. A job that delegates ends the same way when it is cancelled while its child runs.
- When nothing handles it. A failure that no later step's `when:` reads ends the flow as before, with the step's ID in the error, and its kind in the job's `failure`. So does one that later steps read but whose conditions are all false, once the flow reaches its end.
- What fails with a kind. Agent, tool and approval steps. An expression that cannot be evaluated, a safe output that cannot be queued and a wait for an event that fails end the flow at once; `plugin lint` warns about a `when:` that reads the failure of a step that cannot fail with a kind.

A child's words that came without its labels taint the flow, as a [flow tool's](https://docs.oz0.ai/build/parts/flows/flow-tools#flows-as-tools) do.

## Expressions

Expressions are [CEL](https://cel.dev). They see `input` (the flow's input), `steps` (the outputs of the steps so far), `job` (the job's ID), `trigger` (for a webhook, its `headers` and `body`; null otherwise) and `uses` (the other plugins the flow's plugin may reach now, each `true`).

- `when:` is one expression, true or false.
- Everywhere else, a value is literal unless it contains `${{ ... }}`. A string that is exactly one `${{ expression }}` becomes the expression's value, with its type: `input: ${{ input.ticket }}` passes the whole ticket. Other strings get each expression's value filled in where it stands, however many they hold: `agent: acme-${{ steps.classify.team }}`, or two in one string, as in this approval:```yaml
- id: confirm
  approval:
    details: "${{ steps.look.amount }} EUR back to ${{ input.customer }}?"
```

<br />

With an amount of 40 and the customer `Ada`, the person approving reads `40 EUR back to Ada?`. A map or a list filled into a string is written as JSON, with the keys of maps sorted. An expression ends at the first `}}` outside its quotes and its own braces, so `${{ {'a': {'b': 1}} }}` and `${{ '}}' }}` are each one expression.

Mind two things:

- Quote what YAML would misread. An expression with `: ` or a leading `{` needs quotes, like the `output:` above.
- CEL is strict about types. `2.0 * 3` is an error; write `2.0 * 3.0`. Comparing an int with a double (`1 >= 0.8`) is fine.

`plugin lint` checks every expression, and that each `steps.<id>` names an earlier step, so a mistake fails the install, not the first run.

### Steps of an optional plugin

A plugin that works with whichever of several systems a tenant has marks them `optional: true` under
[`uses:`](https://docs.oz0.ai/reference/plugin-manifest/dependencies#uses). A tenant may then lack one, or not have granted it, or have revoked it, so
each step that calls it says so with `uses`, and the step is skipped instead of failing the job. `uses` holds a plugin
while any of its granted functions or agents is in reach: after a revoke of only some of them it is still there, and
a step that calls a revoked function fails the job as an ungranted call does.

```yaml [flows/support-triage.yaml]
steps:
  - id: read_sn
    when: '"servicenow" in uses && input.system == "servicenow"'
    tool: servicenow.get_incident
    input: {number: "${{ input.ticket }}"}
  - id: read_dc
    when: '"atlassian-dc" in uses && input.system == "jira-dc"'
    tool: atlassian_dc_jira.get_issue
    input: {issueIdOrKey: "${{ input.ticket }}"}
```

Write it as `"<plugin>" in uses` (a plugin's name may hold a `-`, which CEL would read as a minus). `plugin lint`
warns about a step that calls an optional plugin's tool or agent without such a `when:`, and about a `when:` that asks
for a plugin `uses:` does not list. A [recording of the flow's steps](https://docs.oz0.ai/improve/evals/recordings#recorded-step-outputs-of-flows)
keeps the flow's `uses` when it was recorded, and its replay reads them instead of the tenant's grants, so a flow eval
takes the same path in any tenant.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
