Expose and explain flows
For: plugin authors.
Expose a flow as a tool, save a job as a flow or explain its behaviour.
Flows as tools
A plugin can expose a flow as a tool. An agent that lists the tool decides whether to call it and with what input; it never sees the flow's steps, and it cannot skip, reorder or repeat one of them. Use it for a fixed procedure, such as a refund that must look the order up, wait for a person and then pay, in that order every time.
tools:
- id: billing
mcp: ["python", "-m", "billing.server"]
requires_approval: [billing.refund]
- id: refunds
flow: issue-refund # a flow of this plugin; the tool's one function is refunds.issue-refund
flows: [flows/]
name: issue-refund
description: Refunds an order: looks it up, then pays it back.
input_schema: schemas/refund-in.json # required: the tool's arguments, an object
output_schema: schemas/refund-out.json # recommended: what the agent reads back
steps:
- id: look
tool: billing.lookup
input: { order: "${{ input.order }}" }
- id: pay
tool: billing.refund
input: { order: "${{ input.order }}", amount: "${{ steps.look.amount }}" }
output: "${{ {'order': input.order, 'refunded': steps.pay} }}"
tools: [refunds.issue-refund]
What the agent gets and what happens:
- The tool is the flow's contract. Its description is the flow's, its arguments are the flow's
input_schema(whose root must be an object), and what it returns is the flow's output, checked againstoutput_schema. A flow's output that breaks its schema fails the flow. - Calling it starts the flow as a child job of the agent's job,
<job>/<flow>-<8 hex>: the same job tree, so the root's budget, a cancel, the kill switches and the audit log cover it. A cancel of the agent's job stops the step that runs, at its tool. The agent waits and reads the flow's output, or a tool error that names the step that failed and why, such asstep pay: user:kim denied it: not this one. - Each step goes through its own gates. The tenant's tool policy decides each step's function by its own annotations, its
requires_approvaland the hooks that match it apply, and each step is audited as its own call, naming the flow tool and the job that called it. An approval for the flow tool itself is optional (listrefunds.issue-refundunder the tool'srequires_approval) and never stands for a step's.
The tool's annotations come from its steps
The annotations are at least as strict as the most consequential step: it is read-only only when every step reads only, destructive when any step is (a step without annotations, a remote server's step, an agent step and a routine's output count as destructive), and open-world when any step is. The tenant's defaults and autonomy levels see what the flow can do: a refund flow is a destructive tool, which asks a person before it starts unless the tenant's policy says otherwise, and its pay step still asks for its own approval.
Its action class hides no step's either: when a step may be a host tool (its function declares host.write, say, or no listing names it, or it runs an agent), the flow tool declares the matching host class (oz0/class: host.write), and a host step beside one that reaches outside makes it host.destructive.
- The agent cannot reach the steps around the flow. The steps' functions are not among the agent's tools unless its definition lists them, so a call of one is refused;
oz0 tool call refunds.issue-refundis refused too, and says to start the flow withoz0 run issue-refund.
Depth and children are bounded
An agent's delegation.max_depth bounds where its flow tool's flow may start, as it bounds a delegation, and no flow tool starts its flow deeper than 8. Each call is a child job: it counts against the agent's delegation.max_children with its delegations, and waits for the starter's quota as a delegation does. A flow may not reach itself: plugin lint refuses a flow tool whose flow calls the same flow tool, or runs an agent that lists it (through delegation too), and at run time a job inside a flow, its tree's top job included, is refused that flow's tool.
- An approval holds for what was approved. When a person approved the call, the flow starts only if the flow tool still lists itself as it did and the tenant's tool policy is the same; otherwise the agent reads why and may call again, which asks again.
plugin lint refuses a flow tool whose flow is not in the plugin, is exposed by another flow tool, or has no input schema, and a flow tool with server fields or affinity, and warns when the flow has no output_schema. Another plugin's agent may call a flow tool when its uses: lists the function and a tenant admin granted it; the flow then runs with its own plugin's grant, so granting a flow tool grants what the flow does.
A flow tool's listing is approved with its plugin's tools (oz0 plugin approve-tools), which covers its description and schemas. Its derived annotations are left out of the listing's hash: a node that does not run a step's plugin counts that step as the worst, so two nodes may derive differently, and each step's own listing is approved in its own plugin and checked when the step runs.
The job's trace shows the call as one flow step with the flow's steps nested under it; see History.
Save a job as a flow
When an agent has done a task well once, you can turn that job into a draft flow instead of typing the steps out. From the job's page, More, then Save as flow, or:
oz0 job save-as-flow 0192f6c4-... --into acme --preview # look first
oz0 job save-as-flow 0192f6c4-... --into acme --confirm-shared # write it into your workspace draft
The draft is made from the job's trace, one path, the one the job took:
- Each tool call that succeeded becomes a tool step with its recorded arguments, and each child agent job an agent step. Failed calls, calls whose result was an error, hooks, approvals, the platform's own functions and flow tools' calls are left out, and the draft's notes say which.
- Values the job was given become the flow's inputs. A value of the job's input becomes
${{ input.<path> }}(the whole input passed on becomes${{ input }}, and the schema then names its properties), and a value from a text request (an order number in "Refund order 4711") becomes a new input property, never named like one the job's input already has. The input schema holds their names and types, never their values. - Values an earlier step returned become references to that step, such as
${{ steps.find_customer.id }}, so the flow passes on what the step returns at each run. - Text the model wrote becomes an agent step that writes it again at each run, and the tool step (or the child agent) reads its answer.
- Every other value is kept as it was, with a comment on its step to review it: an ID or a status the model picked may need to be an input.
- Values of some shapes are never kept. An email address, a phone number, an IBAN, a payment card number, a Swedish personal number, an IP address, one of the tenant's secrets, or text written like a flow expression (
${{ }}, which would run at each run) becomes an input and is flagged in the preview, which names where it was and never the value. A map with such a key becomes one input. Names, street addresses and other free text are not recognised: review every value kept as it was. - A job about a data subject keeps no value. Every text and number of it becomes an input, and so does every map with a key other than a lowercase field name such as
order_id(a person's name used as a key, say), so nothing the platform erases with the subject stays in plugin source. A key written as a lowercase field name is taken for a field's name and kept.
No conditions are invented: the job took one path, so the flow has no when:, and no approval steps are added (each step meets its own gates when the flow runs). The manifest gets the flow and a flow tool for it, tools: [{id: acme_flows, flow: <agent>-flow}] by default, or the first free acme_flows_2, acme_flows_3 and so on when you save another (--name and --tool choose others); list it in an agent's tools: to offer it. Add an output_schema so the calling agent knows what it gets back; lint warns until you do.
Nothing is a revision until you save one in the studio, and nothing runs until that revision is installed. The draft becomes plugin source, which every reader of the workspace reads and an installed version and a Git export carry: look at it first, then confirm. So a job that holds a compartment or a level above the lowest under the tenant's label policies, or whose labels cannot be read, is not saved as a flow: write the flow by hand from what may be shared.
Only the tenant's admins with the content permission may save a job as a flow; a job about a data subject needs --confirm-subject, a chat's turn is never saved, nor is a job the platform ran to test an agent (an eval, a reproduction, a comparison or a rollout's shadow run, whose writes were not made), and each preview and save is in the audit log. --dir writes into a plugin folder on disk instead of the workspace.
Explain a plugin for an auditor
oz0 plugin explain writes a plugin, or one of its flows, as files someone who does not read YAML can review, print and file:
oz0 plugin explain shop --out review/ # the version installed in the tenant
oz0 plugin explain shop --flow issue-refund --out review/refund/
oz0 workspace explain shop --revision 3059a65 --out review/ # a workspace revision; --draft for your draft
oz0 plugin explain --dir ./shop --out review/ # a local folder, without a server
| File | What it holds |
|---|---|
diagram.svg | The plugin's graph, laid out exactly as the studio's Graph tab draws it, in black and white: every box names its kind in words, boxes defined in another plugin are dashed, and edge kinds differ by dash pattern, with a legend. |
diagram.mmd | The same graph as a Mermaid flowchart, for tools that render or diff Mermaid. |
summary.html | A printable account of the plugin, steps, controls and version. See summary contents. |
explain.json | The same facts as data (oz0-explain/1). |
What the printable summary includes
summary.html includes:
- What the plugin and each flow do, with every step in order and its condition.
- Where people approve: approval steps,
requires_approval, and calls the tenant's policy sends to a person. - Tool-policy rules that may decide each function the agents and flows use, including rule IDs, modes, conditions and expiries.
- The tenant's data rules, purposes and legal bases in its own words, recipients, budgets and limits.
- The policy-path coverage figure when the files contain
evals/coverage.json. - The version, revision or commit, and digests.
Open summary.html in a browser and print it to PDF; the page has print styles, no script, and loads nothing. With --flow, the export keeps that flow, its steps, the functions and agents they run and the tools that provide them.
What goes on the page depends on what you may read. You need tenant.read to explain a plugin; the tool policy and data rules are included when you also hold governance.read, and purposes, legal bases and recipients when you hold inventory.read. The built-in reader role holds all three. A section left out says which permission it needs. Recipients are the recipient map's rows for the plugin's agents and flows, so they appear for installed versions only: an installed plugin, or a workspace revision that is the installed one.
A draft or another revision is explained from its own files, with its own tool listing, and the page says why its recipients are left out. A function whose listing was never measured or approved is read as undeclared, and the page says so. The decision shown for each function is the one the policy gives today: a rule past its expires_at decides nothing, and the page lists it as ended. Rules by caller source or publisher are matched as the edge matches them, so a plugin installed from a hub is a hub caller.
A local folder's export (--dir) has no tenant settings; --coverage FILE uses a coverage report from elsewhere.
The same files and settings give byte-identical files, so two exports of an unchanged plugin are the same and a change shows as a diff (a rule that expires in between changes the decision, as it does for the platform). A graph too large to lay out on one page, such as one over 1,500 boxes or 3,000 edges, or a flow whose steps each read many earlier steps, is described on the page and in the Mermaid source, and its diagram.svg says to export one flow at a time.
A server builds two exports at once; another waits a few seconds and is then refused as busy, so try again. Every export made through the server is in the audit log as plugin.explained. The studio's Explain panel makes the same files; see the studio.