Skip to main content

Workflows

File: config/agents/workflows/*.yaml · Schema: schemas/workflow.schema.json

A graph of task-kind nodes and the edges between them, compiled at boot. The agent list, the approval card and the run itself all come from this file. Each task kind's with parameters are listed under Task kinds.

Fields​

FieldTypeDescription
descriptionstringShown under the title in the agent picker. Default: "".
edgeslist of EdgeDefRequired. How the steps connect. Each node has at most one outgoing edge; flow.end nodes have none, every other node needs one.
enabledbooleanA disabled workflow is loaded and checked but not offered or run. Default: true.
entrystringRequired. The id of the first node.
idstringRequired. Unique id; the workflow is served at /agui/{id}.
limitsLimitsPer-run limits. Default: {"max_steps":50,"task_timeout_sec":180}.
nodeslist of NodeDefRequired. The steps. Every node must be reachable from entry.
state_whitelistlist of stringThe only context keys that ever reach the browser. Default: [].
titlestringRequired. Shown in the web app's agent picker.
uianyFree-form UI hints served by GET /api/agents (the approval card).
versioninteger (uint32)Required. Recorded with every run, so a run can be traced to the definition it ran.

Unknown fields are refused at boot.

Types​

Types referred to by the fields above, from the same schema.

Condition​

A test on the run's context. Comparing a string numerically is false, not an error.

One of these shapes:

1. Holds when every condition holds.

FieldTypeDescription
alllist of ConditionRequired.

2. Holds when at least one condition holds.

FieldTypeDescription
anylist of ConditionRequired.

3. Compares the value at key with value.

FieldTypeDescription
keystringRequired. A dotted path whose first segment is a context key, e.g. approval.approved.
opOpRequired. The comparison.
valueanyThe value to compare with; a list for in; unused by empty and not_empty.

EdgeDef​

FieldTypeDescription
elsestring (optional)The node taken when when does not hold.
fromstringRequired. The node this edge leaves.
tostringRequired. The next node; with when, the node taken when the condition holds.
whenCondition (optional)Makes the edge a branch. Must be given together with else.

Unknown fields are refused at boot.

Limits​

FieldTypeDescription
max_stepsinteger (uint)Required. Most steps one run may take before it is stopped.
task_timeout_secinteger (uint64)Required. Longest a single step may take. Also how long an unfinished connector call may stay reserved before the run is marked needs_reconciliation.

Unknown fields are refused at boot.

NodeDef​

FieldTypeDescription
idstringRequired. Unique within the workflow; named by edges, tenant overrides and interrupt ids ({thread}:{node}).
kindstringRequired. Must exist in the task-kind registry.
retryRetry (optional)Retry transient failures (network errors, HTTP 429 and 5xx) of this step.
withanyKind-specific parameters; the kind validates them when it is built.

Unknown fields are refused at boot.

Op​

One of:

  • "eq" — Equal.
  • "ne" — Not equal.
  • "gt" — Greater than (numbers only).
  • "gte" — Greater than or equal (numbers only).
  • "lt" — Less than (numbers only).
  • "lte" — Less than or equal (numbers only).
  • "empty" — Missing, null, or an empty string, list or object.
  • "not_empty" — Present and not empty.
  • "in" — One of the values in the value list.

Retry​

FieldTypeDescription
backoff_secinteger (uint64)Required. Waits backoff_sec × attempt seconds between tries.
maxinteger (uint32)Required. Most attempts after the first.

Unknown fields are refused at boot.

In this repository​

config/agents/workflows/invoice_v1.yaml

# Invoice processing: parse → extract → validate → match → approve → post.
# Every node's `kind` is a Rust task kind (app/src/kinds); everything else is parameters.
id: invoice_v1
version: 3
enabled: true
title: Invoice processing
description: Extract, validate, match, approve and post supplier invoices.
entry: parse
limits:
task_timeout_sec: 180
max_steps: 50
state_whitelist: [invoice, validation_issues, match, awaiting_approval, approval, posted]

ui: # served by GET /api/agents; drives the approval card
approval_card:
title: "Approve invoice {{ invoice.invoice_number }}"
fields: [invoice.vendor_name, invoice.total, invoice.currency, invoice.due_date, validation_issues]
editable: [invoice.po_number, invoice.due_date]

nodes:
- id: parse
kind: document.parse
with: { parser: docling, input: document_ref, output: document_md }
retry: { max: 3, backoff_sec: 5 }

- id: extract
kind: llm.extract
with:
model: chat_default # key in models.yaml
prompt: invoice_extract # key in prompts/
schema: invoice # key in schemas/
input: document_md
output: invoice
retry: { max: 2, backoff_sec: 3 }

- id: validate
kind: rules.validate
with:
input: invoice
output: validation_issues
rules:
- { check: sum_equals, left: [subtotal, tax], right: total, tolerance: 0.01, message: "subtotal + tax != total" }
- { check: lines_sum_equals, lines: lines, field: amount, right: subtotal, tolerance: 0.01, message: "lines != subtotal" }
- { check: required, fields: [vendor_name, invoice_number, total, currency] }
- { check: matches, field: currency, pattern: "^[A-Z]{3}$" }

- id: match
kind: records.match
with:
input: invoice
output: match
strategies:
- { by: vendor_tax_id }
- { by: po_number }
- { by: vendor_name_fuzzy, min_score: 0.85 }
- { by: vector, model: embed_default, top_k: 5, min_score: 0.80 }

- id: approval
kind: human.approval
with:
reason: confirmation # never a tool-bound interrupt (CopilotKit issue #8080)
roles: [approver]
four_eyes: true
expires_in_hours: 72
auto_approve:
enabled: false # tenants may enable it (tenants/*.yaml)
when:
all:
- { key: validation_issues, op: empty }
- { key: match.score, op: gte, value: 0.95 }
- { key: invoice.total, op: lte, value: 500 }
always_require_when:
any:
- { key: match.bank_details_changed, op: eq, value: true }

- id: post
kind: connector.invoke
with:
connector: accounting
action: post_bill
input: invoice
output: posted
idempotency_key: "{{ session_id }}:post_bill"
retry: { max: 5, backoff_sec: 10 }

- id: done
kind: flow.end
with: { message: "Posted to accounting" }

- id: reject
kind: flow.end
with: { message: "Closed as rejected" }

edges:
- { from: parse, to: extract }
- { from: extract, to: validate }
- { from: validate, to: match }
- { from: match, to: approval }
- from: approval
when: { key: approval.approved, op: eq, value: true }
to: post
else: reject
- { from: post, to: done }

Editor support​

The schemas are generated from the Rust definition structs (task schema_export), and a test fails when they drift. Point a YAML language server at the schema to get completion and validation while editing:

# yaml-language-server: $schema=../../../schemas/workflow.schema.json