Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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), although sep in 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 b parses b as an unknown member (E0102).
  • retry, compensate and for do 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

TokenFormNotes
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.
durationan integer or float immediately followed by ms, s, m, h or dNormalized to milliseconds, rounded. Any other letters after a number are E0005.
string"..." on one lineEscapes: \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 literalstrue, false, nullThe 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 5s and timeout: 5s are the same, and so are idempotency_key "k" and idempotency_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.

PositionWords with special meaning
top levelworkflow, schema, task, import, test
task bodyversion, description, input, output, compensated_by
workflow bodyversion, description, timeout, input, output, step, signal, query, return, on
step bodyworker, workflow, sleep, await, compute, using, after, with, timeout, queue_timeout, retry, idempotency_key, when, for, in, max_parallel, compensate
typeList, Map, and the primitive type names
expressionif, then, else; input and execution as path roots
retry valuesfixed, exponential
test bodyworkflow, 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:

PositionReserved namesDiagnostic
step namesinput, execution, output, if, then, elseE0306
for loop variablesinput, execution, and the names of the workflow’s stepsE0413
lambda parametersinput, execution, the names of the workflow’s steps, loop variables and enclosing lambda parametersE0413
schema namesthe primitive type names, List, MapE0200
task namesnames 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:

RestrictionCode
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.

See also