Workflow YAML reference
The full field-level reference for workflow YAML — every step kind, every field, and the validation rules behind them. This is the same content the workflow builder's Reference drawer shows in the app.
Top-level structure
A workflow declares a name, an optional version, and a list of steps.
entry is optional — if omitted, the first step in the list is the entry point.
name: my-workflow
version: '1'
entry: step-1
steps:
- id: step-1
kind: agent
agentId: writer
agentVersion: active
next: step-2
- id: step-2
kind: agent
agentId: reviewer
agentVersion: active
A step without
next:is terminal — the workflow stops there.
Agent steps (kind: agent)
An agent step calls one agent by stable id. agentVersion: active binds to
whichever version is current at run time; an explicit version id pins the run to
that exact version forever.
agentId— required. References an agent in the same tenant.agentVersion— optional.active(default) or an explicit version id.instructions— optional. Overrides the agent's system prompt for this step.outputSchema— optional JSON Schema (draft 2020-12). When set, the agent's reply must validate against it; the runner runs a single repair attempt on schema-validation failure.seed— optional retrieval seed (see below).coverage— optional.fullswitches the step from searching the documents to reading every page of every one — see Coverage mode below.coverageSource/coverageSelection— optional, coverage-only: which documents the sweep reads, and an optional relevance filter — see Coverage mode below.next— optional. Step id to run next. Mutually exclusive withroute:.
- id: classify
kind: agent
agentId: classifier
agentVersion: active
outputSchema: |
{
"type": "object",
"properties": {
"category": { "type": "string" },
"openIssues": { "type": "array", "items": { "type": "string" } }
},
"required": ["category"]
}
next: finalise
Retrieval seed (
seed). This step's document-retrieval query is normally the previous step's output — which, when steps pass structured JSON between each other, is a poor search query and drags in fields meant for later steps. Set aseednaming the topics this step should retrieve on; it is appended to the embedding query only. The agent still receives the previous step's output verbatim as its input — the seed never changes the data passed between agents, only what this step searches its knowledge base (and any uploaded documents) for.
- Useful when the step has a knowledge base, or the workflow carries input documents, and its input is structured JSON. Omit it when the step has nothing to retrieve, or when its input is already plain prose (the prose is a fine query on its own).
- Shapes what the search matches, not how much comes back. Large documents honour it; small documents are read in full and ignore it. (For guaranteed page-by-page reading of everything, use Coverage mode below instead.)
- The entry
kind: inputstep takes the sameseedfield — it's the same idea applied at the workflow's start. seedMode—append(default) orreplace.replacemakes the seed the entire retrieval query, discarding the upstream text for retrieval. Use it on fan-out specialist branches: every branch receives the same upstream input, so an appended seed dilutes into the shared text and the dominant topic crowds the specialists out of their own retrieval.replacerequires a non-emptyseed(seed_mode_requires_seed).
- id: assess
kind: agent
agentId: employment-law-agent
seed: redundancy collective consultation threshold notice pay holiday
next: draft
# Fan-out specialists: each branch replaces the shared query with its own topic.
- id: ip
kind: agent
agentId: ip-extractor
seed: intellectual property rights IP assignment inventions publicity image rights
seedMode: replace
next: merge
Coverage mode (
coverage: full). A field on agent steps — not a separate step type. It flips the step from searching the documents (retrieval — right for questions and focused analysis, but it can miss items) to reading them: the step runs once per section of every target document with that section injected in full, and the results are merged mechanically. Use it for exhaustive tasks — "extract every clause", "analyse all obligations" — where a search that misses one item is a wrong answer.
coverageSource— which documents to read.input(the default) reads the documents uploaded with the run, so the workflow's entry must bekind: inputwith aformat: "document"field (coverage_requires_input_documents).kbreads the step agent's knowledge base instead — no upload field needed.coverageSelection— optional,kbonly: read just the most relevant knowledge-base documents.query(required inside the block) describes the topic in your own words — it is never taken from a previous step's output;topDocuments(optional, default 5) is how many of the best-matching documents to read. Each selected document is still read in full. Omit the block to read the whole knowledge base.- No
seed/seedMode— there is no retrieval query to shape (coverage_ignores_seed). Noloop(coverage_no_loop).routeworks — it evaluates the merged output. - With an
outputSchema, every top-level property must be"type": "array"— per-section results are concatenated per property (coverage_requires_array_output), and each object item is stamped withsourceDocument/sourcePagesautomatically. Without a schema, the merged output is the per-section prose under--- filename, pages A–B ---headers. - Write the agent's prompt for a single excerpt ("extract every X from the excerpt you are given; empty results are valid") — the runner tells it which document and pages it is reading.
- id: sweep
kind: agent
agentId: pay-extractor
coverage: full
outputSchema: |
{ "type": "object",
"properties": {
"payTerms": { "type": "array", "items": { "type": "object",
"properties": { "clause": { "type": "string" },
"summary": { "type": "string" } } } } } }
next: merge
# Knowledge-base sweep, narrowed to the most relevant documents:
- id: policy-sweep
kind: agent
agentId: policy-analyser
coverage: full
coverageSource: kb
coverageSelection:
query: holiday entitlement and sick pay
topDocuments: 5
next: report
Input steps (kind: input)
An input step is the alternative entry-step type to kind: agent. The chat
surface renders an auto-generated form built from the step's schema; the user
fills it in and clicks Submit, and the validated JSON payload becomes step 2's
input. The runner pre-completes the input step so step 2 receives the payload
directly.
title— optional. Shown above the form on the chat page.description— optional. Helper text under the title.schema— required. JSON Schema 2020-12 as a YAML string (pipe-block). Top-leveltypemust beobject; list required field names inrequired.seed— optional. Author text prepended to what the first step receives, to prime its document retrieval — see Retrieval seed below.next— optional. Step id to run after the form is submitted. Omit for a single-step "form-only" workflow.
Input steps are entry-only — the validator rejects any workflow where a
kind: inputstep appears anywhere other than the entry position.
Step shape:
- id: intake
kind: input
title: NDA intake
description: Capture the parties and headline terms.
schema: |
{ "type": "object", "properties": { ... }, "required": [ ... ] }
next: drafter
JSON Schema example covering every field type the form supports:
{
"type": "object",
"properties": {
"clientName": { "type": "string", "title": "Client name", "minLength": 1 },
"jurisdiction": { "type": "string", "title": "Jurisdiction",
"enum": ["England", "Scotland", "NI"] },
"matterType": { "type": "string", "title": "Matter type",
"description": "Short label (e.g. 'commercial NDA')." },
"termYears": { "type": "integer", "title": "Term (years)",
"minimum": 1, "maximum": 25, "default": 3 },
"fee": { "type": "number", "title": "Fee (GBP)" },
"mutual": { "type": "boolean", "title": "Mutual NDA?", "default": true },
"startDate": { "type": "string", "title": "Start date", "format": "date" },
"tags": { "type": "array", "title": "Tags",
"items": { "type": "string" }, "minItems": 0 },
"fees": { "type": "array", "title": "Fee instalments",
"items": { "type": "number" } },
"parties": { "type": "array", "title": "Parties",
"items": {
"type": "object",
"title": "Party",
"properties": {
"name": { "type": "string", "title": "Full name" },
"role": { "type": "string", "title": "Role",
"enum": ["Discloser", "Recipient"] }
},
"required": ["name"]
} },
"notes": { "type": "string", "title": "Internal notes",
"format": "textarea" },
"address": { "type": "object", "title": "Client address",
"properties": {
"line1": { "type": "string", "title": "Line 1" },
"city": { "type": "string", "title": "City" }
},
"required": ["line1"] }
},
"required": ["clientName", "jurisdiction", "matterType"]
}
- Each property's
titledrives the form label;descriptionrenders as helper text under the input. - String +
enum→ dropdown. String +format: date/date-time→ date pickers. String withmaxLength > 200orformat: textarea→ multi-line. - Arrays of primitives —
string(incl.format: date/date-time),integer,number,boolean— render as an add/remove list with the appropriate per-item input control. - Arrays of
type: objectrender as a repeating fieldset of the item's properties. Item objects must be flat — arrays or further-nested objects inside an item are rejected and the schema falls back to a JSON textarea. - One level of nested
type: objectrenders as a sub-fieldset; deeper nesting falls back to a JSON textarea. - Schemas using
oneOf/anyOf/$refat the root fall back to a JSON textarea — the validator still runs server-side.
Document upload fields (
format: "document"). A string property withformat: "document"renders as a file drop-zone instead of a text box; the user uploads a file and the workflow run binds it. Use an array of them for "upload several documents". OptionalcontentMediaTypepins one MIME type — omit it to accept PDF, DOCX, Markdown, or plain text. Theformatvalue is exactlydocument—file,upload, andbinaryare not recognised and would render as a plain text box.
{
"type": "object",
"properties": {
"contract": { "type": "string", "format": "document",
"title": "Contract",
"contentMediaType": "application/pdf" },
"supporting": { "type": "array", "title": "Supporting documents",
"items": { "type": "string", "format": "document" } }
},
"required": ["contract"]
}
- Uploaded documents propagate to every downstream
kind: agentstep's retrieval automatically — you do not wire them throughoutputSchemaorinputField. Each agent sees the document content as retrieved chunks and can cite it with【page:N, quote:"…"】markers. - The submitted value is a reference object (
{ documentId, documentVersionId }); the schema'stype: "string"is satisfied symbolically.
Retrieval seed (
seed). The document chunks the first agent step retrieves are chosen by embedding the text that step receives — and for a document-only input that text is just the reference object, which carries no topic signal. On a large document (one that won't fit whole in context) this means the first agent's retrieval is driven by noise. Set aseednaming the topics the downstream agents will look for; the runner prepends it to what the first step receives, steering that first retrieval.
- id: intake
kind: input
title: Upload contract
seed: >-
Employment contract for analysis. Extract salary, pay, bonus,
pension, holiday, leave, notice, and termination terms.
schema: |
{ "type": "object",
"properties": { "contract": { "type": "string", "format": "document" } },
"required": ["contract"] }
next: analyse
- Only applied when the input step's
nextis akind: agentorkind: fanOutstep — the seed primes their retrieval. It is ignored when the input feeds arenderstep (prepending it would corrupt the rendered payload). - It improves what the search matches, not how much comes back. Small documents are read in full regardless, so the seed only earns its keep on large documents. (For guaranteed page-by-page reading, use Coverage mode on the agent step instead.)
- The step's audit input stays the raw payload — the seed only affects what's forwarded to the next step.
Review steps (kind: review)
Pause the run for a human reviewer to approve, edit, or reject the upstream step's output before the run continues.
reviewers— a list of one or more target tokens; the eligible audience is their union. Valid tokens:admins(every admin + the owner),owner,role:<roleId>(members holding a role — use the Role ID lookup card), anduser:<userId>(a named member). Omittingreviewersdefaults to[admins].prompt— optional plaintext shown above the editor on the review page.next— required. Where the run continues on approve / approve-with-edit. Reject hard-fails the run.reviewField— optional. Scope the review to ONE field of the upstream output: a dotted path with optional[N]indices (report,sections.summary,items[0].body). The reviewer sees and edits just that field — as markdown text when it is a string, as JSON when it is structured — and their edit is spliced back into the full output at the same path. Not JMESPath: filters, wildcards and functions can't be written back. Needs a structured upstream (anoutputSchemaon the step before, or on this step).outputSchema— optional. What the approved output must satisfy (JSON Schema 2020-12). Omitted = inherited from the previous step's schema (through a chain of reviews too, but only when every step flowing in carries the same schema — otherwise declare one here or normalise the branches with an agent step first). Every approval is checked against the effective schema — plain Approve as well as Approve-with-edit — so a deliberately stricter schema can ask the reviewer to edit before approving. A review with an effective schema counts as a structured step for aformat: jsonrender or aninputField:after it.
- id: r1
kind: review
reviewers:
- admins
- role:<roleId>
prompt: Please check the draft above before it goes out.
reviewField: report # the markdown document inside the structured handoff
next: finalise
Conditional routing (route:)
An agent step's downstream can be a list of branches evaluated in order against
the step's structured output. First truthy when: wins; the last branch must be
default: true as fallback. route: requires the step to declare an
outputSchema: so JMESPath has a structured payload to read.
- id: classify
kind: agent
agentId: classifier
outputSchema: |
{ "type": "object",
"properties": { "openIssues": { "type": "array" } } }
route:
- when: "length(openIssues) > `0`"
next: senior-review
- default: true
next: fast-finalise
JMESPath expressions are compile-checked when you save.
Bounded loops (loop:)
Re-run an agent step until a JMESPath until: evaluates truthy on the latest
output, or until maxIterations caps the loop. Each iteration is recorded as a
separate row in the run viewer.
until— required JMESPath against the structured output.maxIterations— optional. Defaults to the deployment-level cap (5) and is silently clamped to the deployment ceiling (20).- Loops require
outputSchema:on the step.
- id: refine
kind: agent
agentId: refiner
outputSchema: |
{ "type": "object",
"properties": { "qualityScore": { "type": "number" } } }
loop:
until: "qualityScore > `0.8`"
maxIterations: 5
next: publish
Fan-out / fan-in (kind: fanOut / kind: fanIn)
Run several branches concurrently and merge their outputs. Every branch must converge at the same fan-in step; branches may not contain review steps and may not nest further fan-outs.
branches— list of step ids the fan-out spawns in parallel.from— list of leaf step ids the fan-in joins (must cover every branch).merge—concat(joins outputs as plain text in declared order) orstructured(emits a JSON object keyed by leaf step id).
- id: split
kind: fanOut
branches: [a, b, c]
- id: a
kind: agent
agentId: drafter-a
next: merge
- id: b
kind: agent
agentId: drafter-b
next: merge
- id: c
kind: agent
agentId: drafter-c
next: merge
- id: merge
kind: fanIn
from: [a, b, c]
merge: structured
next: synthesise
Limits: the number of branches (and how many run at once) is capped by your workspace's fan-out limit — 10 unless raised for your workspace (the save-time validator names the limit when a fan-out exceeds it; contact support to raise it). Loop iterations default to 5 with a hard ceiling of 20.
Render steps (kind: render)
Render the upstream step's output into a downloadable file — PDF, DOCX (rendered from markdown), or JSON (the structured payload itself). Typically the final step of a workflow — the deliverable. The file is encrypted with the tenant DEK before being stored and only decrypted when an authorised user downloads it from the run viewer.
format—pdf,docx, orjson. Required. PDF for static read-only outputs; DOCX for editable drafts; JSON for structured data exports.filename— display name on download (e.g.contract.pdf,facts.json). Required. No path separators or reserved characters; max 200 chars. The extension is coerced to match the format.inputField— optional JMESPath. For PDF / DOCX: leave empty when the upstream emits plain markdown; set it when the upstream has anoutputSchemaand the markdown body lives on a named field. For JSON: leave empty to emit the whole upstream payload; set it to pick any sub-tree (object, array, or scalar — JSON renders aren't restricted to string fields). SettinginputFieldrequires the upstream step to be structured (an agent step withoutputSchema, or akind: inputstep) — the validator refuses it otherwise (render_input_field_requires_structured_upstream), because a field lookup against prose output would fail at run time.next— optional; render steps are usually terminal.
Empty output debugging tip. If your rendered file is blank, the most common cause is an
inputFieldpointing at a field that doesn't exist (or pointing at an object/array rather than a string). Easiest test: removeinputFieldand re-run — if the file now contains your text, your upstream step is emitting plain prose and you should leaveinputFieldoff. If you want to keep the upstream structured, make sure the JMESPath path matches a string field in the JSON.
Prose upstream (no outputSchema) — omit inputField:
- id: draft
kind: agent
agentId: associate
next: out
- id: out
kind: render
format: pdf
filename: contract.pdf
Structured upstream — set inputField to the markdown-bearing field:
- id: draft
kind: agent
agentId: associate
outputSchema: |
{
"type": "object",
"properties": { "body": { "type": "string" } },
"required": ["body"]
}
next: out
- id: out
kind: render
format: docx
filename: contract.docx
inputField: body
JSON renders.
format: jsonwrites a pretty-printed JSON file directly from the upstream step's structured output. The immediate upstream step must emit a structured payload — either an agent step with anoutputSchema:, OR akind: inputentry step (the form'sschema:bounds the submitted JSON, so it counts). Validator refusalrender_json_requires_structured_upstreamotherwise. OmitinputFieldto emit the whole upstream payload, or set it to a JMESPath that picks a sub-tree (objects and arrays are both fine for JSON, unlike PDF / DOCX which need a string field).
- id: classify
kind: agent
agentId: classifier
outputSchema: |
{
"type": "object",
"properties": {
"category": { "type": "string" },
"openIssues": { "type": "array", "items": { "type": "string" } }
},
"required": ["category"]
}
next: out
- id: out
kind: render
format: json
filename: classification.json
Output size is capped at the deployment limit (default 10 MB). Exceeding the cap fails the step cleanly with
render_size_exceeded. Malformed upstream JSON fails the step withrender_json_invalid_upstream.