Skip to content

DSL grammar

This page is the normative grammar reference, derived 1:1 from the parser and serializer. Any divergence between this page and the parser is a bug. The parser is the ground truth.

A Flow DSL file is one header line, followed by a sequence of node blocks, sub-flow blocks, and edge statements separated by newlines. Order is significant only for the header; nodes, sub-flows, and edges may be interleaved freely.

flow "<name>" v<version>?
description "<text>"?
<node-block>
<edge-statement>
...

The v<version> segment is optional, and it defaults to 1.0.0. The lexer reads the literal text after v and stores it verbatim. v1 parses as version "1", and v2.3.4 parses as "2.3.4".

description "<text>" is optional and, when present, must appear on the line immediately after the flow header. It captures a one-paragraph human summary that round-trips through the canvas and is included in the agentic generation prompt. Anywhere else, description is treated as a regular identifier and may be used as a node id.

<id>[<kind>: "<label>"] {
<key>: <scalar>
...
}
  • <id> is a bare identifier (alphanumeric, -, _).
  • <kind> is exactly one of: action, ai, agentic, utility, service. Any other identifier is rejected.
  • <label> is a quoted string. Standard escapes: \", \\, \n, \t, \r. Other backslash sequences are an error.
  • The body block { ... } is optional. A node with no body is just <id>[<kind>: "<label>"] followed by a newline.
  • Body keys are bare identifiers. The literal key label is rejected inside the body, because the label belongs in the header. All other key names pass through to data verbatim.
  • Body values are a scalar, meaning a quoted string, a number, true / false, or null. They can also be an array of scalars (["a", "b"], possibly empty). Nested objects have no syntax. Set those via the canvas inspector.
  • Fields are separated by newlines; an optional comma between fields is permitted but not required.
  • Duplicate node ids within the same flow are rejected at parse time with the line and column of the second definition and a pointer to the first.
<source>[.<outcome>]? --> <target> [<hint>]? [: "<label>"]? [when <expr>]?
  • <source> and <target> are node ids declared somewhere in the file.
  • .outcome is one of pass, fail, success, failure. pass and success are aliases, as are fail and failure. Omitting the outcome means always.
  • <hint> is a single bare identifier on the arrow line, used as the edge label when no explicit : "..." follows.
  • : "<label>" is an optional quoted edge-label string.
  • when <expr> captures a free-form condition expression as an opaque string. The executor’s routing layer interprets it.

The edge id is auto-generated as e-<source>-<outcome>-<target>; the parser does not accept a hand-written one.

subflow "<label>" [retry <n>]? {
<node-block>
<edge-statement>
...
}

A sub-flow groups its member nodes into a single execution unit. It has a single entry and a single exit, both derived from the edges that cross the boundary. With retry <n>, the whole unit re-runs up to <n> times if it fails.

  • <label> is a quoted string naming the sub-flow. Its id derives from the label.
  • The body holds node blocks and edge statements, parsed exactly as at the top level. Member nodes are also part of the flat node list.
  • A sub-flow must contain at least one node. Sub-flows do not nest.
TypeSyntaxExamples
string"...""hello", "line\nbreak"
numberdigits, optional . and exponent42, 3.14, 2.5e-3
booltrue or false (lowercase)true
nullnullnull
array[<scalar>, ...] (scalars only)["fs", "shell"], []

The parser rejects these, and the flow generator is trained never to emit them:

  • Nested objects. key: { sub: "x" } does not parse. Set it via the canvas.
  • Multi-line strings. A literal newline inside a quoted string raises unterminated string.
  • Comments. There is no comment syntax.
  • Custom node types. Anything outside the five node types is rejected.
  • A label field inside a body block. Labels live in the header only.

Parse failures produce an error with a 1-indexed line and column. The Generate UI surfaces this directly as Parse error line N:M - <message>.

The serializer produces byte-stable output:

  • Body keys are emitted in alphabetical order, except label, which is always lifted into the header.
  • A blank line separates each node block from the next.
  • Strings escape ", \, \n, \t, \r. Numbers are emitted as parsed.

Round-trip property: serialize(parse(s)) produces a canonical form of s (key reorder plus whitespace normalization), and that canonical form is itself a fixed point of parse-then-serialize. This property is preserved across every DSL change.