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

Workflows

A workflow declares what should happen, in which order, and with which guarantees: its typed input, the steps that do the work, and the result it returns. The compiler turns each workflow into an immutable, versioned definition that the runtime executes durably. This page covers the workflow declaration and each of its members.

Declaring a workflow

workflow OrderProcessing {
    version "1.0.0"
    description "Validate, charge and confirm one order"
    timeout 5m

    input {
        order_id: String
        customer_id: String
        amount: Float
        currency: String = "USD"
        items: List<LineItem>
    }

    output {
        status: String
        transaction_id: String
    }

    step validate {
        worker "orders.validate"
        with { order_id: input.order_id, amount: input.amount, items: input.items }
    }

    step charge {
        worker "payments.charge"
        after validate
        with { order_id: input.order_id, amount: input.amount, currency: input.currency }
    }

    return { status: "completed", transaction_id: charge.transaction_id }
}

The workflow name is an identifier. It is unique in the compilation (E0200) and is what you pass to orbit run and to the start API.

Members

MemberSyntaxRequiredDefaultMeaning
versionversion "1.0.0"yes (E0500)noneThe definition version. name@version is immutable once registered.
descriptiondescription "..."nononeHuman-readable description, shown by tooling.
timeouttimeout 5mnono limitDeadline for the whole execution, measured from its creation.
inputinput { fields }no{}The type of the execution input.
outputoutput { fields }noinferred from returnThe type of the execution result.
stepstep name { ... }nononeA unit of work. See Steps.
signalsignal name { fields }nononeA typed message the execution accepts; await steps receive it. See Signals and queries.
queryquery name = expressionnononeA typed, read-only view of the execution state, evaluated on demand. See Signals and queries.
returnreturn expressionwhen output has required fields (E0501){}The result of a successful execution.
continue_as_newcontinue_as_new when cond with { ... }nononeWhen every step has finished and cond holds, end the run and start a new run of the execution with this input and an empty history, instead of returning. See Continue-as-new.
on failureon failure { steps }nononeSteps that run after compensation when a step fails permanently or a resource limit is reached. See Outcome handlers.
on cancelon cancel { steps }nononeSteps that run after compensation when the execution is cancelled.
on timeouton timeout { steps }nononeSteps that run after compensation when the workflow timeout passes.
on eventon event signal { steps }nononeSteps that run every time that signal arrives, alongside the main steps; they read the payload as event. See Event handlers.
limitslimits { max_fan_out: 100 }nothe server’s limitsStricter fan-out, history and payload limits for this workflow’s executions. See Per-workflow limits.

Members can appear in any order. version, description and timeout accept an optional : after the keyword. input, output and signal name are followed directly by a record body. A workflow can declare any number of steps, signals and queries; step, signal and query names are separate namespaces, each unique within the workflow (E0302, E0317, E0318).

A workflow has at most one on block of each kind, and one on event block per signal (E0319); any other kind, such as on success, is rejected with E0105. Handler blocks contain only step declarations. Use compensation to undo work, and outcome handlers for what should happen after it, such as notifying a customer.

Version

Every workflow declares a version. A version that is not MAJOR.MINOR.PATCH (optionally with a -pre or +build suffix) produces warning W0201. A missing version is an error:

workflow Nightly {
    step pause { sleep 1h }
}
error[E0500]: workflow `Nightly` has no version
 --> nightly.orbit:1:10
  |
1 | workflow Nightly {
  |          ^^^^^^^
  = help: add `version "1.0.0"`; running executions stay pinned to the version they started with

error: compilation failed: 1 error(s), 0 warning(s)

See Versioning for how versions and definition hashes interact.

Input

input declares the fields an execution starts with, using the same field syntax as a schema. Expressions read them as input.<field>.

When an execution starts, the runtime:

  1. treats a missing or null input as {},
  2. applies the declared defaults, recursively,
  3. validates the result strictly: missing required fields, values of the wrong type and fields the workflow does not declare are rejected.

A rejected input fails the start request with INVALID_INPUT, and no execution is created:

orbit run order.orbit --input '{"order_id": "A-1", "amount": "12"}'
error: INVALID_INPUT (400): workflow input is invalid at $.amount: expected Float, found a string

Child workflows started by a workflow step get their defaults applied too, but their input is validated leniently, because the parent’s with expression was already type-checked against the child’s input.

Output and return

return maps step results to the workflow result. It is evaluated once, after every step has finished (succeeded or been skipped), and it can read input, execution and every step.

  • With output and return, the return expression is checked against output, with the same rules as a task input: object literals must be exact (E0404) and complete (E0403).
  • With output but no return, the result is {}, which is accepted only if output has no required fields. Otherwise the compiler reports E0501.
  • With return but no output, the output type is the inferred type of the return expression.
  • With neither, the result is {}.
workflow Quote {
    version "1.0.0"
    input { order_id: String, items: Int }

    step quote {
        worker "shipping.quote"
        with { order_id: input.order_id, items: input.items }
    }

    // No `output` block: the output type is inferred as
    // { carrier: String, cost: Float, expensive: Bool }.
    return { carrier: quote.carrier, cost: quote.cost, expensive: quote.cost > 50.0 }
}

The runtime validates the result against the output type before it completes the execution. A failure there, or an expression error while evaluating return, fails the workflow with OUTPUT_CONTRACT_VIOLATION or EXPRESSION_ERROR. No compensation runs in that case, because every step succeeded.

Timeout

The workflow timeout is a deadline for the whole execution, counted from the moment the execution was created. When it passes, the runtime:

  1. withdraws steps that have not started,
  2. waits for attempts that are in flight,
  3. runs the compensations of the steps that succeeded,
  4. ends the execution as TIMED_OUT.

Step timeouts, retries, sleeps and waits for signals all count against the workflow deadline. A workflow timeout also bounds every await step that has no timeout of its own (otherwise the compiler reports W0606). Compensations run after the deadline and are not limited by it. See Retries and timeouts.

Signals and queries

A workflow can declare the signals it accepts and the queries it answers:

workflow Approval {
    version "1.0.0"
    timeout 14d
    input { request_id: String }

    signal review { approved: Bool, reviewer: String }

    step decision { await review }

    query status = { decided: !is_null(decision), approved: decision.approved }

    return { approved: decision.approved }
}

orbit signal <id> review --data '{...}' sends a validated payload, which the decision step receives as its output. orbit query <id> status evaluates the query against the current state; in a query every step output is optional, because the step may not have run yet. A workflow that declares a signal or a query compiles to orbit-ir/2. See Signals and queries.

Execution lifecycle

An execution is RUNNING until it completes, or until it is aborted by a step failure, a cancellation or the workflow timeout:

RUNNING ──► COMPLETED
   │
   └──► COMPENSATING ──► FAILED | CANCELLED | TIMED_OUT

A failed, cancelled or timed-out execution records a compensation outcome: none (nothing to compensate), completed or failed. See Compensation and Executions and history.

Execution metadata

Expressions can read three fields of the running execution:

ExpressionTypeValue
execution.idStringthe execution id
execution.workflowStringthe workflow name
execution.versionStringthe workflow version

These are often used to build idempotency keys or correlation ids:

workflow Welcome {
    version "1.0.0"
    input { email: String }
    step send {
        worker "emails.send"
        with { to: input.email, template: "welcome" }
        idempotency_key: "welcome-" + execution.id
    }
}

See also