Skip to content

Workflow YAML reference

Ferrith ChatFor authorsFor integratorsVerified 26 Aug 2026

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.

yaml
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. full switches 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 with route:.
yaml
- 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 a seed naming 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: input step takes the same seed field — it's the same idea applied at the workflow's start.
  • seedModeappend (default) or replace. replace makes 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. replace requires a non-empty seed (seed_mode_requires_seed).
yaml
- 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 be kind: input with a format: "document" field (coverage_requires_input_documents). kb reads the step agent's knowledge base instead — no upload field needed.
  • coverageSelection — optional, kb only: 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). No loop (coverage_no_loop). route works — 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 with sourceDocument / sourcePages automatically. 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.
yaml
- 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-level type must be object; list required field names in required.
  • 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: input step appears anywhere other than the entry position.

Step shape:

yaml
- 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:

json
{
  "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 title drives the form label; description renders as helper text under the input.
  • String + enum → dropdown. String + format: date / date-time → date pickers. String with maxLength > 200 or format: 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: object render 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: object renders as a sub-fieldset; deeper nesting falls back to a JSON textarea.
  • Schemas using oneOf / anyOf / $ref at the root fall back to a JSON textarea — the validator still runs server-side.

Document upload fields (format: "document"). A string property with format: "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". Optional contentMediaType pins one MIME type — omit it to accept PDF, DOCX, Markdown, or plain text. The format value is exactly documentfile, upload, and binary are not recognised and would render as a plain text box.

json
{
  "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: agent step's retrieval automatically — you do not wire them through outputSchema or inputField. 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's type: "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 a seed naming the topics the downstream agents will look for; the runner prepends it to what the first step receives, steering that first retrieval.

yaml
- 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 next is a kind: agent or kind: fanOut step — the seed primes their retrieval. It is ignored when the input feeds a render step (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), and user:<userId> (a named member). Omitting reviewers defaults 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 (an outputSchema on 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 a format: json render or an inputField: after it.
yaml
- 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.

yaml
- 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.
yaml
- 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).
  • mergeconcat (joins outputs as plain text in declared order) or structured (emits a JSON object keyed by leaf step id).
yaml
- 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.

  • formatpdf, docx, or json. 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 an outputSchema and 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). Setting inputField requires the upstream step to be structured (an agent step with outputSchema, or a kind: input step) — 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 inputField pointing at a field that doesn't exist (or pointing at an object/array rather than a string). Easiest test: remove inputField and re-run — if the file now contains your text, your upstream step is emitting plain prose and you should leave inputField off. 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:

yaml
- 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:

yaml
- 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: json writes 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 an outputSchema:, OR a kind: input entry step (the form's schema: bounds the submitted JSON, so it counts). Validator refusal render_json_requires_structured_upstream otherwise. Omit inputField to 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).

yaml
- 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 with render_json_invalid_upstream.