Write a flow
For: plugin authors.
Define a flow in YAML to run a sequence of tool calls, agent jobs and approvals, with conditions where needed. The platform's flow workflow executes the steps and preserves Temporal determinism. Release a new flow version with its plugin.
name: acme-triage
description: Routes a ticket to the right team
input_schema: schemas/ticket.json # optional
steps:
- id: classify
tool: acme_triage.classify
input:
ticket: ${{ input.ticket }}
teams: [billing, technical, sales]
- id: route
when: steps.classify.confidence >= 0.8
agent: acme-${{ steps.classify.team }}
input: ${{ input.ticket }}
- id: ask_human
when: steps.classify.confidence < 0.8
approval:
details: "Route ${{ input.ticket.subject }} to ${{ steps.classify.team }}?"
timeout: 4h
output: "${{ has(steps.route) ? steps.route : 'a person decides' }}"
List flows in the manifest with flows: [flows/], a folder of *.yaml files or single files.
Run one
A flow runs as a job, by name, like an agent:
oz0 run acme-triage '{"ticket": {"subject": "Refund", "body": "..."}}'
job watch, job get, job cancel, the console's job page, the runtime API and cost per branch work for flow jobs as for agent jobs; oz0 catalog lists flows with their steps. A flow job has flow: true.
Start one from a webhook
A flow with a webhook trigger starts when a service posts to the edge:
name: acme-triage
trigger:
webhook: { secret: ZENDESK_WEBHOOK } # a tenant secret, which only the edge reads
steps:
- id: classify
tool: acme_triage.classify
input: { ticket: "${{ trigger.body.ticket }}" }
oz0 secret set ZENDESK_WEBHOOK --from-file ~/.secrets/zendesk-webhook --signing --host edge.example.com:7443
Set it as a signing key bound to the edge's address the service posts to: the edge API's advertised address, its --public-edge-url or a --san name at the edge API's port, which the operator sets, never the host a request names. A secret a connection of the flow's plugin saved is taken as it is, unless that connection sends it as a credential (its token, the key its tokens are signed with, its client's secret or its refresh token), which goes only where the edge sends it.
Any other secret, a credential above all, checks nothing, and every request gets the 401 a wrong signature gets; the edge's log says why.
The service posts JSON to https://<edge>:7443/v1/flows/<tenant>/<flow>, here https://edge.example.com:7443/v1/flows/default/acme-triage, and proves it knows the secret in one of two ways:
Authorization: Bearer <secret>, as Zendesk and most services can send;- an HMAC-SHA256 signature of the body with the secret as its key, in
X-Hub-Signature-256: sha256=<hex>(GitHub's header) orX-Oz0-Signature.
The body becomes the flow's input and trigger.body; the request's headers, without credentials and signatures, are trigger.headers. The edge answers 202 with the job's ID. A signed request is named by what its signature covers, the job hook-<flow>-sha256-<digest>: the same signed body sent again is the same job, whatever its delivery headers say, and one sent after the job ended gets 200 with that job's ID instead of a second run.
A sender that sends the same body again on purpose signs a timestamp with it: X-Oz0-Timestamp: <Unix seconds> and X-Oz0-Signature: sha256=<hex HMAC-SHA256 of "<timestamp>.<body>">, refused (401) more than five minutes from the edge's clock. A bearer secret is the secret itself, so whoever holds one can send anything: with one, a delivery ID (Idempotency-Key, X-GitHub-Delivery or X-Request-Id) names the job; prefer a signature. Bodies are at most 1 MB. The jobs a webhook starts are charged to service:webhook/<flow>, which can have a quota; a delivery over it gets 429.
While a kill switch stops the flow, a delivery gets 503 with the switch's reason (oz0_killed). A flow whose webhook secret is not set yet refuses every request with the 401 a wrong signature gets; the edge's log says which it was.
Bash, Write or Edit, may process that input only in tier 1 without the plugin's secrets. plugin install refuses the flow unless a tenant tier-1 node has the labels that agent requires. Pydantic AI agents also pass hooks and approvals on each tool call.plugin lint reports paths from a webhook flow to agents whose harness sessions need tier 1, through both flow steps and delegation. The extra shell-or-write install check does not apply to:- Pydantic AI and other framework agents;
- agents on the Claude harness whose
harness.toolslist onlyRead,Glob,Grep,LS,NotebookReadorTodoWrite.
Bash, Write and Edit), needs an eligible tier-1 node. Without one, install refuses the path. Lint names the path and warns that the sessions need tier 1:flow fix-issue, which a webhook starts, can reach agent fixer (fix-issue -> fixer): it lists no harness tools, so it gets Read, Write, Edit, Glob, Grep, Bash, which include Write, Edit, Bash
acme-${{ steps.classify.team }}, can reach every agent its fixed parts match. The check runs across the tenant's plugins at install: adding either the flow or the agent is refused when the required tier-1 node is missing. A path that was installed before the check existed is reported as a warning.Start one from an event
A flow can start on the events of an event source instead, such as GitHub's pull requests:
name: github-ci-watch
trigger:
event: { source: github-events, types: [pull_request], filter: "event.action == 'opened'" }
steps:
- id: ci
wait_for_event: { source: github-events, types: [check_suite], subject: "${{ trigger.event.subject }}", timeout: 4h }
The event is the flow's input and trigger.event. The job is event-<flow>-<event id>, charged to service:webhook/<flow> like a webhook's, and kill switches stop it the same way. An event's text is outside text, as a webhook's body is: everything in the warning above holds for event-triggered flows too. A wait_for_event step waits for a further event; see Waiting for events.
Related tasks
- Flow steps and expressions: Look up step types, error branches and expression behaviour.
- Expose and explain flows: Expose a flow as a tool, save a job as a flow or explain its behaviour.
Previous section links
- Section: a built in in a step
- Section: error branches
- Section: explain a plugin for an auditor
- Section: expressions
- Section: flows as tools
- Section: save a job as a flow
- Section: steps
- Section: steps of an optional plugin