FlowsReference

The flow file format (YAML / JSON)

Everything on a flow’s canvas is also a plain text file. View file shows the flow as YAML or JSON, you can edit it there and watch the canvas follow, and the same shape is what a connected Git repository stores as .flow.yaml. The file holds only meaning — steps, links and settings — never positions: the canvas lays itself out from the structure.

What you will learn
  • Open, read and edit a flow as YAML or JSON
  • Know the keys: trigger, a, b, c… and onError
  • Write each binding form and each structural node
  • Copy a flow between flows, and keep it in Git

A flow is a file

Open the flow’s ⋯ menu and pick View file (“Read this flow as YAML”). A drawer opens with the whole definition and a YAML / JSON switch. Changes on the canvas show up in the file at once; edits in the file reach the canvas about half a second after you stop typing, as long as the text parses — otherwise the drawer says YAML parse error and the canvas keeps the last good version. Edits are unsaved until you click Save, exactly like edits on the canvas.

The canvas is drawn from the file’s structure: no coordinates are stored, so the same file always lays out the same way.

Branch pills, end caps, the join under parallel branches and the shaded loop body are not in the file either. They are derived from the structure each time the canvas is drawn, which is why a file written by hand — or by an AI assistant, or pulled from Git — renders as neatly as one built by dragging.

A complete example

A flow that loads orders over a minimum total, scores each one in a parallel loop, reports when there were any, and has an On error step — exactly as View file writes it:

YAMLorders.flow.yaml
trigger:
  parameters:
    - name: minTotal
      type: number
      default: 100
  next: a
a:
  kind: script
  label: Load orders
  alias: loadOrders
  lang: postgresql
  code: |
    -- @param min_total number = 100
    SELECT id, email, total FROM orders WHERE total >= :min_total
  integration: 9b1c6e2a-5d1f-4c1e-9a7e-2f0c3b8d4e61
  inputs:
    min_total:
      from: trigger
      path: minTotal
  next: b
b:
  kind: loop
  label: Each order
  alias: eachOrder
  iterate:
    from: a
    path: rows
  mode: parallel
  concurrency: 5
  body:
    - c
  next: d
c:
  kind: script
  label: Score
  alias: score
  lang: python
  code: |
    def main(total: float):
        return {"vip": total > 500}
  inputs:
    total:
      from: b
      path: item.total
  retry:
    attempts: 2
    delay: exponential
    delaySec: 2
d:
  kind: condition
  label: Any orders?
  alias: anyOrders
  if: nodes.loadOrders.output.rowCount > 0
  then:
    label: "Yes"
    nodes:
      - e
  else:
    label: "No"
    nodes: []
e:
  kind: script
  label: Report
  alias: report
  lang: typescript
  code: |
    export async function main() {
      return { ok: true }
    }
onError:
  kind: script
  label: Notify
  alias: notify
  lang: python
  code: |
    def main(message: str = "", step: str = ""):
        print(f"{step} failed: {message}")
  • trigger is always the Start node; its parameters are the flow’s inputs and next names the first step.
  • Every other step gets a short key — a, b, … z, aa, ab — in breadth-first order from Start. Keys are local to the file and reassigned each time it is written; the step keeps its real id behind the scenes, so runs and bindings stay attached to it.
  • alias is the name expressions use (nodes.loadOrders.output); it is derived from the label and shown for reference.
  • onError (and preprocessor) are top-level entries with no next — they are not on the chain.

Node kinds and their fields

kindFields
scriptlang (python, typescript, javascript, c, cpp, postgresql, mysql), code for an inline step or script (its path) for a linked one, integration and rowLimit for SQL, dependencies for inline Python.
conditionif, then: {label, nodes}, optional elseIfs: [{id, label, if, nodes}], else: {label, nodes}. nodes lists every step inside that branch.
loopiterate (a binding, a list, or an expression), mode: parallel + concurrency or mode: while + maxIterations, breakIf, body (every step inside).
parallelbranches: [{id, name, mayFail?, nodes}], optional sequential: true.
flowflowRef (the child’s id — the real reference) and flowName (a readable hint, never read back).
aiScraperscraper (id), scraperName (hint), optional timeoutMs.
emailprovider (absent = the project’s default), template; recipients and values are inputs (to + one per template value).
approvalfields, approvers, timeoutSec, onTimeout, notify, allowLinks; the message is inputs.message. Defaults are omitted.
YAMLfragment.flow.yaml
c:
  kind: parallel
  label: Fan out
  branches:
    - id: b1x9k2
      name: Send e-mail
      nodes: [d]
    - id: b7qq01
      name: Notify team
      mayFail: true
      nodes: [e]
  next: f
m:
  kind: flow
  label: Enrich customer
  flowRef: 5f0e8a3c-2b7d-4e19-8c55-1a2b3c4d5e6f
  flowName: Enrich customer
  inputs:
    customerId: { from: a, path: id }
    retries: { manual: "3" }
    apiKey: { var: CRM_API_KEY }
  next: n

Any executable step can also carry its run settings under the same names as on the step — retry, continueOnError, skipIf, stopIf, sleepAfterSec, cacheTtlSec, timeoutMs — and a short note. Anything off is simply absent, so a flow without settings has none of these keys.

Binding forms

FormMeans
{ from: a, path: rows.0.total }A value from step a’s output, at that path (no path = the whole output). Inside a loop, from the loop with path: item.total reads the current item.
{ manual: "42" }A constant, converted to the parameter’s declared type.
{ var: NAME }A project or global variable, read when the step runs.
{ expr: "vars.RETRIES * 2" }An expression. View file currently shows it as { manual: "__expr__:vars.RETRIES * 2" }; Git files and the MCP server write expr.

Copy, export and keep in Git

  1. Export

    Open View file, choose YAML or JSON and copy the text. That text is the whole flow; keep it, share it, review it in a pull request.

  2. Import into another flow

    Create an empty flow, open its View file, replace the text with the copy and click Save. Steps get fresh ids; linked scripts, connections and child flows are referenced by id, so they must exist in the target project.

  3. Keep it in Git

    With the project connected to a repository (Git), every flow is a <folder>/<name>.flow.yaml file in the same shape. A commit writes the flow; a pull lands the file as the flow’s draft — never published — so you review and publish it yourself.

Git files differ from View file in small ways: expressions are written { expr: … }, constants keep their JSON type, default branch labels and alias are left out, and the file is validated when it is read — an unknown key or kind, a missing if or iterate, or a step no path reaches is reported instead of guessed.

What the file does not hold

  • No positions, no colours, no zoom: layout is always derived.
  • No version pin on a flow or aiScraper step: callers always run the callee’s latest published version.
  • Not the flow’s own settings — its time limit, concurrency limit, triggers and state live outside the definition.