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.
File shape
Section titled “File shape”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.
Node block
Section titled “Node block”<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
labelis rejected inside the body, because the label belongs in the header. All other key names pass through todataverbatim. - Body values are a scalar, meaning a quoted string, a number,
true/false, ornull. 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.
Edge statement
Section titled “Edge statement”<source>[.<outcome>]? --> <target> [<hint>]? [: "<label>"]? [when <expr>]?<source>and<target>are node ids declared somewhere in the file..outcomeis one ofpass,fail,success,failure.passandsuccessare aliases, as arefailandfailure. Omitting the outcome meansalways.<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.
Sub-flow block
Section titled “Sub-flow block”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.
Value types
Section titled “Value types”| Type | Syntax | Examples |
|---|---|---|
| string | "..." | "hello", "line\nbreak" |
| number | digits, optional . and exponent | 42, 3.14, 2.5e-3 |
| bool | true or false (lowercase) | true |
| null | null | null |
| array | [<scalar>, ...] (scalars only) | ["fs", "shell"], [] |
Forbidden constructs
Section titled “Forbidden constructs”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
labelfield inside a body block. Labels live in the header only.
Errors
Section titled “Errors”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>.
Serialization
Section titled “Serialization”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.