Grammar
This page is the formal reference for ORBIT syntax. It reproduces the EBNF
grammar accepted by the orbit-syntax parser, walks through it section by
section, and describes the lexical rules: tokens, comments, contextual
keywords and the line-break rule. Semantic restrictions that the compiler
enforces after parsing are summarized at the end, with their diagnostic codes.
EBNF
The grammar uses ISO EBNF: { x } means zero or more, [ x ] means optional,
and , separates the items of a sequence.
(* ORBIT grammar: the language accepted by orbit-syntax 0.2.0.
Notation: ISO EBNF. { x } = zero or more, [ x ] = optional.
Lexical notes
- Keywords are contextual: every word lexes as IDENTIFIER and the parser
treats it as a keyword only in keyword position. `input`, `version`,
`status` ... remain valid field names. Only true/false/null are reserved.
- Whitespace and newlines are insignificant, with one rule: an infix
operator (and a postfix `[`, `.` or `(`) must be on the same line as its
left operand. `{ a: 1 b: -2 }` therefore has two fields.
- Separators `,` and `;` between members, fields and list items are optional.
- Member keywords may be followed by an optional `:`
(`timeout 5s` == `timeout: 5s`). *)
program = { declaration } ;
declaration = workflow_decl | schema_decl | task_decl | import_decl | test_decl ;
import_decl = "import" , STRING ; (* path relative to the importing file *)
schema_decl = "schema" , IDENTIFIER , record_body ;
record_body = "{" , { field , [ sep ] } , "}" ;
field = IDENTIFIER , ":" , type , [ "=" , expression ] ; (* default must be constant *)
task_decl = "task" , STRING , "{" , { task_member , [ sep ] } , "}" ;
task_member = "version" , [ ":" ] , STRING
| "description" , [ ":" ] , STRING
| "input" , [ ":" ] , type
| "output" , [ ":" ] , type
| "compensated_by" , [ ":" ] , STRING ; (* the task that undoes this one *)
workflow_decl = "workflow" , IDENTIFIER , "{" , { workflow_member , [ sep ] } , "}" ;
workflow_member = "version" , [ ":" ] , STRING
| "description" , [ ":" ] , STRING
| "timeout" , [ ":" ] , DURATION
| "input" , record_body
| "output" , record_body
| step_decl
| "signal" , IDENTIFIER , record_body (* typed message the execution accepts *)
| "query" , IDENTIFIER , [ "=" | ":" ] , expression (* read-only view; written with "=" *)
| "return" , expression
| "continue_as_new" , continue_as_new
(* at most one (E0106); `with` is required (E0323) and typed
against the workflow input; `when` must be Bool (E0324) *)
| "limits" , "{" , { IDENTIFIER , ":" , INTEGER , [ sep ] } , "}"
(* max_fan_out, max_history_events, max_payload_bytes; positive
integers (E0320, E0321); the stricter of this and the
server's limit applies *)
| "on" , "event" , IDENTIFIER , "{" , { step_decl , [ sep ] } , "}"
(* runs once per arriving signal of that name, alongside the
main steps; `event` is the payload. The signal must be
declared (E0315) and not awaited (E0322); one block per
signal (E0319). Same step rules as outcome handlers. *)
| "on" , ( "failure" | "cancel" | "timeout" ) , "{" , { step_decl , [ sep ] } , "}" ;
(* outcome handler: at most one block per kind (E0319); other
kinds are rejected (E0105). Handler steps take a worker,
compute or sleep action; no await, child workflow, for,
max_parallel or compensate (E0307, E0308); `after` names
steps of the same block (E0303). *)
continue_as_new = [ "when" , [ ":" ] , expression ] , "with" , [ ":" ] , expression
| "with" , [ ":" ] , expression , "when" , [ ":" ] , expression ;
step_decl = "step" , IDENTIFIER , "{" , { step_member , [ sep ] } , "}" ;
step_member = action
| "using" , [ ":" ] , IDENTIFIER
| "after" , [ ":" ] , IDENTIFIER , { "," , IDENTIFIER }
| "with" , [ ":" ] , expression
| "timeout" , [ ":" ] , DURATION (* per attempt, start-to-close *)
| "queue_timeout" , [ ":" ] , DURATION (* time allowed waiting for a worker *)
| "retry" , "{" , { IDENTIFIER , ":" , expression , [ sep ] } , "}"
| "idempotency_key" , [ ":" ] , expression
| "when" , [ ":" ] , expression
| "for" , IDENTIFIER , "in" , expression (* fan out over a list *)
| "max_parallel" , [ ":" ] , INTEGER (* only with `for` *)
| "compensate" , "{" , { step_member , [ sep ] } , "}" ;
action = "worker" , [ ":" ] , STRING (* a task contract name *)
| "workflow" , [ ":" ] , STRING (* a child workflow name *)
| "sleep" , [ ":" ] , DURATION
| "await" , [ ":" ] , IDENTIFIER (* a signal declared in the workflow *)
| "compute" , [ ":" ] , expression ; (* evaluated by the runtime *)
(* Restrictions checked by the compiler (see docs/src/reference/diagnostics.md):
- `workflow` steps take no `using`, `retry`, `idempotency_key` or `queue_timeout` (E0307);
- `compensate` blocks call a `worker` and take no `when`, `after`, `for` or `max_parallel`;
- `for` is not allowed on `sleep` steps; the loop variable is in scope in `with`,
`idempotency_key` and `compensate`, but not in `when` (evaluated once per step);
- `await` steps take only `after`, `when` and `timeout`; `compute` steps take only
`after`, `when`, `for` and `max_parallel`; neither takes `compensate` (E0307, E0308). *)
(* retry settings: attempts: INTEGER, backoff: fixed | exponential,
initial_delay: DURATION, max_delay: DURATION, non_retryable: [STRING, ...] *)
type = base_type , { "?" } ;
base_type = "List" , "<" , type , ">"
| "Map" , "<" , type , "," , type , ">" (* key must be String *)
| record_body (* anonymous record *)
| IDENTIFIER ; (* primitive or schema name *)
(* primitives: String Int Float Bool Bytes Duration Timestamp UUID JSON *)
expression = or_expr ;
or_expr = and_expr , { "||" , and_expr } ;
and_expr = eq_expr , { "&&" , eq_expr } ;
eq_expr = cmp_expr , { ( "==" | "!=" ) , cmp_expr } ;
cmp_expr = add_expr , { ( "<" | "<=" | ">" | ">=" ) , add_expr } ;
add_expr = mul_expr , { ( "+" | "-" ) , mul_expr } ;
mul_expr = unary , { ( "*" | "/" | "%" ) , unary } ;
unary = ( "!" | "-" ) , unary | postfix ;
postfix = primary , { "." , IDENTIFIER | "[" , expression , "]" } ;
primary = literal
| IDENTIFIER , "(" , [ argument , { "," , argument } ] , ")" (* built-in call *)
| IDENTIFIER (* path root: input | execution | step | loop variable *)
| "{" , { ( IDENTIFIER | STRING ) , ":" , expression , [ sep ] } , "}"
| "[" , [ expression , { [ "," ] , expression } ] , "]" (* items: "," or newline, not ";" *)
| "if" , expression , "then" , expression , "else" , expression
| "(" , expression , ")" ;
argument = IDENTIFIER , "=>" , expression (* lambda: only as argument 2 of map, filter, find, any, all (E0415) *)
| expression ;
literal = STRING | INTEGER | FLOAT | DURATION | "true" | "false" | "null" ;
block = "{" , { ANY_TOKEN | block } , "}" ;
sep = "," | ";" ;
(* Tokens *)
IDENTIFIER = ( letter | "_" ) , { letter | digit | "_" } ;
INTEGER = digit , { digit | "_" } ; (* signed 64-bit *)
FLOAT = digit , { digit | "_" } , "." , digit , { digit | "_" } ;
DURATION = ( INTEGER | FLOAT ) , ( "ms" | "s" | "m" | "h" | "d" ) ;
STRING = '"' , { char | escape } , '"' ; (* single line *)
escape = "\n" | "\t" | "\r" | '\"' | "\\" | "\u{" , hex , { hex } , "}" ;
(* comments: // to end of line, and nestable /* ... */ *)
(* Tests, run by `orbit test` and never compiled into workflow IR.
Values in `input`, `signal` and `expect ... output` must be constants;
in `returns`, `input` is the mocked task's input. *)
test_decl = "test" , STRING , "{" , { test_member , [ sep ] } , "}" ;
test_member = "workflow" , [ ":" ] , IDENTIFIER
| "version" , [ ":" ] , STRING
| "input" , [ ":" ] , expression
| "mock" , STRING , mock_behaviour , { "times" , INTEGER , "then" , mock_behaviour }
| "signal" , IDENTIFIER , [ expression ] (* object payload on the same line; default {} *)
| "expect" , expectation ;
mock_behaviour = "returns" , expression
| "fails" , STRING , [ STRING ] , [ "retryable" ] (* code, message *)
| "hangs" ; (* never answers *)
expectation = "status" , IDENTIFIER (* COMPLETED FAILED CANCELLED TIMED_OUT *)
| "output" , expression
| "error" , STRING
| "step" , IDENTIFIER , ( IDENTIFIER (* a unit status, e.g. SKIPPED *)
| "output" , expression
| "attempts" , INTEGER )
| "compensated" , IDENTIFIER
| "runs" , INTEGER ; (* the final run, >= 1 (E0712, E0713) *)
The source of this grammar is specification/grammar.ebnf in the repository.
Walkthrough
Programs and declarations
A program is a sequence of declarations: workflow, schema, task,
import and test, in any order. Separators (, or ;) between declarations are
allowed and ignored. Anything else at the top level is E0104. See
Files and imports.
Records and types
record_body is shared by schemas, inline record types and the workflow
input and output blocks. Each field is name: type, optionally followed
by = expression for a constant default.
A type is a base type followed by any number of ?. Base types are
List<T>, Map<K, V> (where K must be String), an inline record, or an
identifier naming a primitive or a schema. See Types.
Task contracts
A task declaration has a string name and up to five members: version,
description, input, output and compensated_by. See
Task contracts.
Workflows and steps
A workflow has an identifier name and members: version, description,
timeout, input, output, any number of step, signal and query
declarations, return, continue_as_new, and on handlers. continue_as_new takes with <input> and an optional when <condition>, in either order; see Continue-as-new. A signal has a name and a record body, like
input. A query has a name, = and an expression; the parser also accepts
: or nothing in place of =, but = is the documented form. See
Signals and queries. An on member is an outcome handler:
on failure, on cancel or on timeout, followed by a block of step
declarations. Any other word after on is rejected with E0105. See
Outcome handlers.
A step has an identifier name and members. action is one of worker,
workflow, sleep, await (followed by a signal name) or compute
(followed by an expression). The compensate block contains step members, with the
restrictions listed under
Restrictions after parsing. See
Steps.
Tests
A test declaration has a string name and members: workflow (an
identifier), version, input, any number of mock, signal and expect
members. A mock chains behaviours with times N then. expect runs N checks the run an execution ended in. Tests are checked by
the compiler but never compiled into workflow IR. See
Testing workflows.
Retry blocks
retry { ... } contains name: expression pairs. The grammar accepts any
identifier and expression; the compiler then requires the five known settings
with literal values (E0410). See Retries and timeouts.
Expressions
The expression grammar is layered by precedence, from or_expr (lowest) to
postfix and primary (highest). All binary operators are
left-associative. A unary - directly before a number literal is folded into
a negative literal. See Expressions.
primary covers literals, function calls (an identifier immediately followed
by (), path roots (input, execution, a step name, a loop variable or a
lambda parameter), object literals, list literals, if ... then ... else ...,
and parenthesized expressions.
A function argument can be a lambda, IDENTIFIER => expression. The parser
recognizes name => wherever a primary expression can start, so a misplaced
lambda is reported by the compiler (E0415) rather than as a syntax error.
The compiler accepts lambdas only as the second argument of map, filter,
find, any and all. See Expressions.
Notes on the grammar
The parser differs from the EBNF in a few details:
- List items are separated by commas or line breaks. A semicolon between list
items is a syntax error (
E0100), althoughsepin the EBNF includes;. A trailing comma is allowed. - Function arguments are separated by commas only, and a trailing comma is
allowed:
max(1, 2,). - In
after a, b, the commas are required.after a bparsesbas an unknown member (E0102). retry,compensateandfordo not accept the optional:that other members accept.- In a lambda,
=>must be on the same line as the parameter.
Lexical rules
Character set
Source files are UTF-8. Identifiers and keywords are ASCII; non-ASCII
characters are allowed only in string literals and comments. Any other
character outside a string or comment is E0001.
Tokens
| Token | Form | Notes |
|---|---|---|
| identifier | [A-Za-z_][A-Za-z0-9_]* | Case-sensitive. |
| integer | [0-9][0-9_]* | Underscores are ignored. Must fit in a signed 64-bit integer (E0006). |
| float | [0-9][0-9_]*.[0-9][0-9_]* | Digits are required on both sides of .. No exponent. |
| duration | an integer or float immediately followed by ms, s, m, h or d | Normalized to milliseconds, rounded. Any other letters after a number are E0005. |
| string | "..." on one line | Escapes: \n, \t, \r, \", \\, \u{hex}. Unknown escapes are E0004; an unterminated or multi-line string is E0002. |
| punctuation | { } ( ) [ ] < > , : ; . ? = ! + - * / % | |
| operators | == != <= >= && || => | Two-character operators are lexed greedily. => separates a lambda parameter from its body. |
| reserved literals | true, false, null | The only reserved words. |
Comments
// starts a comment that runs to the end of the line. /* starts a block
comment that ends at the matching */; block comments nest. An unterminated
block comment is E0003. Comments are whitespace to the parser, except that a
block comment spanning lines counts as a line break.
Whitespace and line breaks
Whitespace and line breaks are insignificant, with one rule: an infix
operator, and a postfix ., [ or (, must be on the same line as its left
operand. A line break before such a token ends the expression. That is why
workflow Fields {
version "1.0.0"
return {
a: 1
b: -2
}
}
has two fields, a: 1 and b: -2, rather than one field a: 1 - 2. See
Expressions.
Separators and colons
,and;between members, fields and object entries are optional.- Member keywords that take a value may be followed by an optional
:.timeout 5sandtimeout: 5sare the same, and so areidempotency_key "k"andidempotency_key: "k".
Keywords
ORBIT keywords are contextual. The lexer produces an identifier for every
word except true, false and null, and the parser treats a word as a
keyword only in a position where that keyword is expected. Everywhere else
the word is an ordinary identifier, so input, version, status, step
or timeout can be field names.
| Position | Words with special meaning |
|---|---|
| top level | workflow, schema, task, import, test |
| task body | version, description, input, output, compensated_by |
| workflow body | version, description, timeout, input, output, step, signal, query, return, on |
| step body | worker, workflow, sleep, await, compute, using, after, with, timeout, queue_timeout, retry, idempotency_key, when, for, in, max_parallel, compensate |
| type | List, Map, and the primitive type names |
| expression | if, then, else; input and execution as path roots |
retry values | fixed, exponential |
| test body | workflow, version, input, mock, returns, fails, retryable, hangs, times, then, signal, expect, status, output, error, step, attempts, compensated |
This workflow is valid, because each word is used outside its keyword positions:
schema step {
workflow: String
input: Int
timeout: Int
}
workflow workflow {
version "1.0.0"
input { step: step, for: Int }
return { input: input.step.workflow, version: input.for }
}
A few names are reserved in specific positions because expressions would otherwise be ambiguous:
| Position | Reserved names | Diagnostic |
|---|---|---|
| step names | input, execution, output, if, then, else | E0306 |
for loop variables | input, execution, and the names of the workflow’s steps | E0413 |
| lambda parameters | input, execution, the names of the workflow’s steps, loop variables and enclosing lambda parameters | E0413 |
| schema names | the primitive type names, List, Map | E0200 |
| task names | names starting with orbit. | E0200 |
An object key that is not a valid identifier, or that is true, false or
null, must be written as a string: { "null": 1, "content-type": "x" }.
Record type fields must be identifiers, so a contract field cannot be named
true, false or null.
Restrictions after parsing
The grammar accepts some programs that the compiler then rejects:
| Restriction | Code |
|---|---|
A workflow has a version. | E0500 |
A step has exactly one of worker, workflow, sleep, await or compute. | E0301, E0106 |
A step member appears at most once (except after). | E0106 |
workflow steps take no using, retry, idempotency_key or queue_timeout. | E0307 |
sleep steps take only after and when. | E0307, E0308 |
await steps take only after, when and timeout, and name a declared signal. | E0307, E0308, E0315 |
compute steps take only after, when, for and max_parallel. | E0307, E0308 |
| Signal and query names are unique within a workflow. | E0317, E0318 |
compensated_by names a declared task. | E0316 |
Lambdas appear only as the second argument of map, filter, find, any and all. | E0415 |
max_parallel requires for. | E0307 |
compensate calls a worker and takes no when, after, for or max_parallel. | E0308, E0307 |
The loop variable is not in scope in when. | E0401 |
retry settings are known names with literal values. | E0410 |
Timeouts are greater than zero. max_parallel is 1 to 10000. | E0410 |
| Defaults are constants of the field’s type. | E0401, E0400, E0205 |
Map keys are String. | E0204 |
| Schemas are not recursive. | E0202 |
| The step graph and the workflow call graph are acyclic. | E0304, E0311 |
on is followed by failure, cancel or timeout. | E0105 |
A workflow has at most one on block of each kind. | E0319 |
Handler step names differ from workflow step names, reserved names and failure. | E0302 |
after in a handler names a step of the same block. | E0303 |
Handler steps take no await, child workflow, for or max_parallel, and no compensate. | E0307, E0308 |
failure is read only inside handler steps. | E0401 |
continue_as_new appears at most once, has with, a Bool when, and an input of the workflow’s input type. | E0106, E0323, E0324, E0400, E0403 |
expect runs is at least 1, and more than 1 only for a workflow with continue_as_new. | E0712, E0713 |
The full list of codes is in Compiler diagnostics.