Language reference
The complete reference for the Agent Flow language. Authoritative
implementation: packages/lang/src/language/agent-flow.langium (grammar)
and packages/compiler/src/checker.ts (static semantics).
Lexis
Comments and whitespace
- Line comments
// ...; block comments/* ... */(multiline, no nesting) - Whitespace and comments are hidden tokens; newlines are not significant
Identifiers
ID = /[_a-zA-Z][\w$]*/
Strings
| Form | Notes |
|---|---|
"..." | single-line; escapes \" \\ \n \t \r; other \x reads as x |
"""...""" | multiline; content verbatim; used for task; only leading/trailing newlines trimmed |
Numbers and durations
| Token | Regex | Notes |
|---|---|---|
NUMBER | /\d+(\.\d+)?/ | decimal; no negative literals |
DURATION | /\d+(ms|s|m|h)(?![a-zA-Z0-9_])/ | 500ms 5s 10m 2h; normalized to ms |
Separator conventions
| Structure | Commas |
|---|---|
| Arrays, tools, dependencies, call args, retry codes | required; trailing where applicable |
| Object/type/limits/parallel fields | required; trailing allowed |
| workflow/stage/agent/verify statements | no commas |
Types
TypeRef := PrimaryTypeRef Suffix*
PrimaryTypeRef := 'member' '<' Id '>' | Id
Suffix := '[' RangeBounds? ']'
RangeBounds := NUMBER '..' NUMBER
Repeated suffixes express nested arrays (path[][]); optional fields use
field?: Type. Reading one yields internal T?; use value ?? fallback to
resolve absence, with null as the explicit empty value.
Type kinds, resolution order, assignability and the JSON Schema mapping: see the full tables on the Type system page.
Top-level declarations
Workflow := 'workflow' ID '(' 'input' ':' TypeRef (',' 'context' ':' TypeRef)? ')' '->' TypeRef '{' Member* '}'
UseStatement := 'use' ('agent' | 'team') STRING 'as' ID
LimitsBlock := 'limits' '{' LimitField+ '}'
LimitField := ('concurrency' | 'agent_runs' | 'duration') ':' (NUMBER | DURATION)
TypeDeclaration := 'type' ID '{' (FieldName '?'? ':' TypeRef (',' ...)* ','?)? '}'
Pipelines and stages
Pipeline := 'pipeline' ID? '{' PipelineStatement* '}'
Stage := 'stage' ID ('after' ID (',' ID)*)? '->' TypeRef '{' StageStatement* '}'
- The pipeline's top-level last statement must be
return; typed early returns are allowed insideif - A stage's name is its result variable; dependencies must be declared and earlier in source order; bodies read only declared dependencies
- V1 executes in source order; dependencies flow into the IR for DAG scheduling
Statements
| Statement | Grammar | Constraint |
|---|---|---|
| let | let ID = expr | no duplicates in scope |
| require | require expr else fail STRING | condition must be bool |
| emit | emit progress objectLiteral | progress events only |
| return | return expr | type must match the declaration |
| if | if expr { stmts } (else { stmts })? | condition must be bool; block scopes |
Expressions
Precedence (tightest first)
postfix .member [*]projection f(args)
comparison == != < <= > >= / in a..b (non-chainable)
not not (scopes over the whole comparison level)
and and (left-assoc)
or or (left-assoc)
coalesce ?? (right-assoc, loosest)
not a == b parses as not (a == b); parentheses group explicitly.
optional ?? fallback returns a compatible non-optional type.
Literals and basic forms
- Strings / multiline strings / numbers / booleans / durations
- Array literals
[e1, e2](commas required; infer a compatible common element type) - Object literals
{ f1, f2: e2 }: explicitname: exprfields;nameshorthand equalsname: namewithnamea visible variable - Member access
expr.member:- object types → field type (missing →
invalid-member-access) - arrays → only
.count(number),.empty/.any(bool) Verification→.passed/.failed/.checks- external types / context (any) → any member, result any
- team bindings →
.main(agent-target) - Dangerous member names
constructorprototype__proto____defineGetter____defineSetter____lookupGetter____lookupSetter__hasOwnPropertyisPrototypeOfpropertyIsEnumerabletoLocaleStringtoStringvalueOf— always rejected (dangerous-member-access)
- object types → field type (missing →
- Projections
xs[*].field:xsmust be an array; equivalent toxs.map(x => x.field); result is the field type's array; a bare projectionxs[*]is invalid - Calls
f(args): builtins only, plusteam.member(...)(legal only in an agent() target position)
Builtins
| Function | Parameter | Result | Semantics |
|---|---|---|---|
unique(array) | an array | bool | elements (stringified) have no duplicates |
disjoint(arrayOfArrays) | arrays of arrays | bool | subarrays pairwise disjoint |
union(arrayOfArrays) | arrays of arrays | array | concatenates all subarrays in order |
range and in
a .. b: both sides numeric/duration; only legal as the right operand ofinx in a..b: closed interval,x >= a && x <= b
agent() calls
AgentExpression := 'agent' '(' Expression ')' '{' AgentOption* '}'
AgentOption :=
'task' TaskString # required, unique
| 'input' ObjectLiteral
| 'tools' 'none' | '[' (','? Id)* ']'
| 'write' Expression # must be path[]
| 'expect' TypeRef # required, unique; no Verification/external types
| 'timeout' DURATION # positive
| 'retry' '{' 'attempts' ':' NUMBER ('on' ':' '[' Code (',' Code)* ']')? '}'
Code := 'timeout' | 'rate_limit' | 'transient'
Three target forms: alias / team.main / team.member(expr).
attempts counts extra tries; on defaults to ['transient'];
decisions are by error code.
parallel / parallel map / verify
ParallelExpression := 'parallel' '{' (','? ID '=' Expression)+ '}'
ParallelMapExpression := 'parallel' 'map' Expression 'as' ID ('limit' NUMBER)? '{' Expression '}'
VerifyExpression := 'verify' '{' (','? VerifyCheck)* 'pass' 'when' Expression '}'
VerifyCheck := 'check' ID '=' Expression | 'parallel' '{' (','? 'check' ID '=' Expression)+ '}'
- parallel: concurrent branches, barrier semantics; result
{branchName: type} - parallel map: source must be an array;
limita positive integer; result order matches input order; typingT[] → U[] - verify: check names unique; initializers evaluate in the outer scope,
pass whenin the verify scope; resultVerification<{checks...}>
Scopes
workflow : input, context, use aliases
pipeline : + completed stage results (source order)
stage : + declared dependencies
block : if then/else
map : loop variable
verify : check names (pass when only)
Reserved words and soft field keywords
The following are reserved in variable, stage, type, resource alias, parallel/check/map, and tool-name positions:
agent agent_runs and as attempts
check concurrency else emit expect
fail false if in let
limit limits map none not
on or parallel pass pipeline
progress rate_limit require return retry
stage task team timeout tools
transient true type use verify
when write workflow
They are soft keywords in type fields, object fields, and .field member
access, so type Event { type: text, retry?: bool } is legal. input,
context, duration, and member are also accepted as ordinary value
identifiers. This keeps external JSON field names free while protecting names
that enter generated-code namespaces.
JS-style while, for, function, class are not reserved — but V1
has no such statements, so they parse as errors anyway.
Frequent collisions
| Position | ✗ | ✓ |
|---|---|---|
| map loop variable | as task / as check / as map | item / job / slot |
| check name | check task = ... | tests / security / policy |
| parallel branch | tools = ... | frontend = ... |
| explicit object field | { type: value } | legal |
| let/shorthand | let expect = ... / { expect } | avoid keywords |
Appendix: EBNF grammar
grammar AgentFlow
hidden terminal WS: /\s+/;
hidden terminal SL_COMMENT: /\/\/[^\n\r]*/;
hidden terminal ML_COMMENT: /\/\*[\s\S]*?\*\//;
terminal STRING: /"(\\.|[^"\\\n\r])*"/;
terminal ML_STRING: /"""[\s\S]*?"""/;
terminal DURATION: /\d+(ms|s|m|h)(?![a-zA-Z0-9_])/;
terminal NUMBER: /\d+(\.\d+)?/;
terminal ID: /[_a-zA-Z][\w$]*/;
Id returns string: ID | 'input' | 'context' | 'duration' | 'member';
FieldName returns string:
Id | 'workflow' | 'use' | 'type' | 'pipeline' | 'stage' | 'agent' | 'parallel'
| 'verify' | 'require' | 'emit' | 'return' | 'if' | 'else' | 'let' | 'not'
| 'and' | 'or' | 'in' | 'true' | 'false' | 'task' | 'tools' | 'write'
| 'expect' | 'timeout' | 'retry' | 'pass' | 'when' | 'check' | 'map' | 'as'
| 'after' | 'limit' | 'limits' | 'team' | 'none' | 'on' | 'attempts'
| 'progress' | 'fail' | 'concurrency' | 'agent_runs' | 'rate_limit'
| 'transient' | 'null';
TaskString returns string: STRING | ML_STRING;
BoolKeyword returns string: 'true' | 'false';
LimitValue returns string: NUMBER | DURATION;
RetryErrorCode returns string: 'timeout' | 'rate_limit' | 'transient';
entry FlowFile: elements+=Workflow+;
Workflow:
'workflow' name=ID '(' param=WorkflowParam (',' contextParam=WorkflowContextParam)? ')' '->' outputType=TypeRef
'{' members+=WorkflowMember* '}' ;
WorkflowParam: name=Id ':' type=TypeRef ;
WorkflowContextParam: 'context' ':' type=TypeRef ;
TypeRef:
({infer MemberTypeRef} 'member' '<' team=Id '>' | {infer NamedTypeRef} name=Id)
suffixes+=TypeSuffix* ;
TypeSuffix: '[' bounds=RangeBounds? ']' ;
RangeBounds: min=NUMBER '..' max=NUMBER ;
TypeDeclaration: 'type' name=ID '{' (fields+=TypeField (',' fields+=TypeField)* ','?)? '}' ;
TypeField: name=FieldName (optional?='?')? ':' type=TypeRef ;
UseStatement: 'use' kind=('agent' | 'team') resourceId=STRING 'as' name=ID ;
LimitsBlock: 'limits' '{' fields+=LimitField (',' fields+=LimitField)* ','? '}' ;
LimitField: name=('concurrency' | 'agent_runs' | 'duration') ':' value=LimitValue ;
Pipeline: 'pipeline' name=ID? '{' body+=PipelineStatement* '}' ;
Stage:
'stage' name=ID
('after' dependencies+=ID (',' dependencies+=ID)*)?
'->' returnType=TypeRef
'{' body+=StageStatement* '}' ;
LetStatement: 'let' name=Id '=' value=Expression ;
RequireStatement: 'require' condition=Expression 'else' 'fail' message=STRING ;
ReturnStatement: 'return' value=Expression ;
EmitStatement: 'emit' 'progress' payload=ObjectLiteral ;
IfStatement: 'if' condition=Expression '{' thenBody+=StageStatement+ '}' ('else' '{' elseBody+=StageStatement+ '}')? ;
Expression: CoalesceExpression ;
CoalesceExpression infers Expression:
OrExpression ({infer BinaryExpression.left=current} operator='??' right=CoalesceExpression)? ;
OrExpression infers Expression:
AndExpression ({infer BinaryExpression.left=current} operator='or' right=AndExpression)* ;
AndExpression infers Expression:
NotExpression ({infer BinaryExpression.left=current} operator='and' right=NotExpression)* ;
NotExpression infers Expression:
{infer UnaryExpression} 'not' operand=NotExpression
| ComparisonExpression ;
ComparisonExpression infers Expression:
PostfixExpression (
{infer BinaryExpression.left=current} operator=('==' | '!=' | '<' | '<=' | '>' | '>=') right=PostfixExpression
| {infer BinaryExpression.left=current} operator='in' right=RangeExpression
)? ;
RangeExpression infers Expression:
{infer RangeExpression} start=PostfixExpression '..' end=PostfixExpression ;
PostfixExpression infers Expression:
PrimaryExpression (
{infer MemberAccess.target=current} '.' member=FieldName
| {infer Projection.target=current} '[*]'
| {infer CallExpression.target=current} '(' (args+=Expression (',' args+=Expression)*)? ')'
)* ;
PrimaryExpression infers Expression:
AgentExpression
| ParallelExpression
| ParallelMapExpression
| VerifyExpression
| {infer VariableRef} name=Id
| {infer StringLiteral} value=STRING
| {infer MLStringLiteral} value=ML_STRING
| {infer NumberLiteral} value=NUMBER
| {infer BoolLiteral} value=BoolKeyword
| {infer NullLiteral} 'null'
| {infer DurationLiteral} value=DURATION
| ArrayLiteral
| ObjectLiteral
| '(' Expression ')'
;
ArrayLiteral infers Expression: {infer ArrayLiteral} '[' (elements+=Expression (',' elements+=Expression)*)? ']' ;
ObjectLiteral infers Expression: {infer ObjectLiteral} '{' (fields+=ObjectField (',' fields+=ObjectField)* ','?)? '}' ;
ObjectField: name=FieldName (':' value=Expression)? ;
AgentExpression infers Expression: {infer AgentExpression} 'agent' '(' target=Expression ')' '{' options+=AgentOption* '}' ;
AgentTask: 'task' value=TaskString ;
AgentInput: 'input' payload=ObjectLiteral ;
AgentTools: 'tools' (none?='none' | '[' names+=ID (',' names+=ID)* ','? ']') ;
AgentWrite: 'write' paths=Expression ;
AgentExpect: 'expect' type=TypeRef ;
AgentTimeout: 'timeout' value=DURATION ;
AgentRetry: 'retry' '{' 'attempts' ':' attempts=NUMBER (',' 'on' ':' '[' codes+=RetryErrorCode (',' codes+=RetryErrorCode)* ','? ']')? ','? '}' ;
ParallelExpression infers Expression: {infer ParallelExpression} 'parallel' '{' branches+=ParallelBranch (',' branches+=ParallelBranch)* ','? '}' ;
ParallelBranch: name=Id '=' value=Expression ;
ParallelMapExpression infers Expression: {infer ParallelMapExpression}
'parallel' 'map' source=Expression 'as' item=Id ('limit' limit=NUMBER)? '{' body=Expression '}' ;
VerifyExpression infers Expression: {infer VerifyExpression} 'verify' '{' checks+=VerifyCheck+ 'pass' 'when' condition=Expression '}' ;
CheckItem: 'check' name=Id '=' value=Expression ;
ParallelCheckGroup: 'parallel' '{' checks+=CheckItem+ '}' ;
WorkflowMember: UseStatement | LimitsBlock | TypeDeclaration | Pipeline ;
PipelineStatement: Stage | LetStatement | RequireStatement | EmitStatement | ReturnStatement | IfStatement ;
StageStatement: LetStatement | RequireStatement | EmitStatement | ReturnStatement | IfStatement ;
AgentOption: AgentTask | AgentInput | AgentTools | AgentWrite | AgentExpect | AgentTimeout | AgentRetry ;
VerifyCheck: CheckItem | ParallelCheckGroup ;
Appendix: diagnostic catalog
Compile time
| Code | Severity | Meaning |
|---|---|---|
syntax / lex | error | parse / lex errors |
duplicate-workflow | error | duplicate workflow name |
duplicate-alias | error | duplicate use alias |
duplicate-limits / duplicate-pipeline | error | multiple limits / pipeline |
duplicate-type / duplicate-type-field | error | duplicate type / field |
recursive-type | error | recursive type reference |
unknown-team | error | member<T> referencing a non-team alias |
invalid-bounds | error | invalid array bounds |
external-type / unknown-type | info / error | boundary external; internal unknown rejected |
duplicate-stage | error | duplicate stage name |
unknown-stage / duplicate-dependency | error | missing or duplicated dependency |
forward-dependency | error | depending on a later stage |
undeclared-dependency | error | reading an undeclared stage result |
missing-pipeline / missing-input | error | missing pipeline / input param |
workflow-param-name | error | parameter not named input |
missing-return / pipeline-return-position | error | pipeline return missing / misplaced |
stage-missing-return / stage-return-position | error | stage return missing / misplaced |
stage-return-mismatch / workflow-return-mismatch | error | return type mismatch |
duplicate-variable | error | same-scope variable reuse |
unknown-variable | error | undefined variable (incl. shorthand fields) |
heterogeneous-array | error | incompatible array literal element types |
require-not-bool / if-not-bool / verify-predicate-not-bool | error | non-bool condition |
dangerous-member-access | error | accessing a dangerous member |
invalid-member-access | error | member absent on the type |
optional-member-access | error | member access attempted before handling absence with ?? |
invalid-projection | error | [*] on a non-array, or bare projection |
unknown-builtin | error | calling a non-builtin |
invalid-builtin-call | error | builtin argument shape mismatch |
invalid-agent-target | error | illegal agent() target form |
invalid-member-argument | error | team.member() argument not a member id |
agent-missing-task / agent-missing-expect | error | missing required option |
duplicate-agent-option / duplicate-tool | error | duplicated option / tool name |
expect-invalid-type | error | expect type not Schema-generatable |
write-not-path-array | error | write not path[] |
invalid-timeout / invalid-retry / invalid-limit / invalid-limits | error | invalid option value |
invalid-emit | error | emit missing its payload object |
duplicate-branch / duplicate-check / duplicate-object-field | error | duplicate branch / check / field |
operator-type | error | operator operand type mismatch |
parallel-map-source-not-array | error | parallel map source not an array |