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

Compute steps

A compute step evaluates an expression in the runtime and makes the value its output. It needs no worker, so it suits the small, deterministic pieces of logic that sit between tasks: deriving a flag, totalling a list, reshaping a record, picking the items that need attention. This page covers when to use a compute step instead of a worker, the syntax and members, how the runtime evaluates it, lambdas and collection functions, fan-out, conditions and errors.

When to use a compute step

Use a compute step whenUse a worker task when
The result depends only on the workflow input and earlier step outputs.The work calls a service, a database, a file system or the clock.
The logic fits in an expression: arithmetic, comparisons, conditionals, the built-in functions.The logic needs a library, a loop with state, or anything an expression cannot say.
You want the value recorded in the history and visible to later steps and queries.You want retries, timeouts, routing to a language, or a compensation.

A compute step can do nothing that an expression in a later step’s with or when could not. What it adds is a name: the intermediate result is computed once, recorded in the history, and read by any number of later steps, the return expression and queries.

Declaring a compute step

step <name> { compute <expression> }
task "payments.charge" {
    input { order_id: String, amount: Float }
    output { transaction_id: String }
}

workflow Checkout {
    version "1.0.0"
    input { order_id: String, amount: Float, coupon_percent: Int = 0 }

    step pricing {
        compute {
            discount: input.amount * float(input.coupon_percent) / 100.0
            needs_review: input.amount >= 1000.0
        }
    }

    step charge {
        worker "payments.charge"
        when !pricing.needs_review
        with { order_id: input.order_id, amount: input.amount - pricing.discount }
    }

    return { charged: !is_null(charge), discount: pricing.discount }
}

The expression can be of any type, not only a record. The compiler infers the step’s output type from it, and later steps, return and queries see that type: here pricing is { discount: Float, needs_review: Bool }, and step total { compute sum(input.prices) } would make total a Float.

The expression can read input, execution, the outputs of other steps and, in a for step, the loop variable. Reading a step’s output makes the compute step depend on it, exactly as in with. See Dependencies.

Members

MemberAllowedNotes
afteryesOrdering, as for any step.
whenyesSkipped steps have output null, and the output type becomes optional.
for, max_parallelyesOne value per element; see Fan-out. All elements are evaluated together, so max_parallel has no effect.
with, using, timeout, queue_timeout, retry, idempotency_keyno (E0307)There is no task, no worker and no attempt to bound or retry.
compensateno (E0308)A compute step has no side effect to undo.
workflow Totals {
    version "1.0.0"
    input { prices: List<Float> }
    step total {
        compute sum(input.prices)
        timeout 10s
        compensate { worker "ledger.reverse" }
    }
}
error[E0307]: `timeout` is not allowed in a `compute` step
 --> totals.orbit:6:17
  |
6 |         timeout 10s
  |                 ^^^

error[E0308]: a `compute` step has nothing to compensate
 --> totals.orbit:7:9
  |
7 |         compensate { worker "ledger.reverse" }
  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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

A step has exactly one action, so compute cannot be combined with worker, workflow, sleep or await (E0106).

How it runs

When every dependency of a compute step is done and its when condition is true, the runtime evaluates the expression during the same decision and records the value as the step’s output in a StepSucceeded event, with attempt: 1. There is no StepScheduled or StepDispatched event, no worker, no idempotency key and no retry.

AspectBehavior
DeterminismExpressions are pure: they perform no I/O and cannot read the clock or random numbers. The same input and step outputs always give the same value.
ReplayThe value is stored in the history. Recovery and replay read it from there and never evaluate the expression again.
WorkersNone. A compute step runs even when no worker is connected.
CostEvaluation happens inside the runtime’s decision loop. Keep expressions to data mapping; do heavy work in a worker.
StatusSUCCEEDED, SKIPPED, FAILED (on an expression error) or CANCELLED. A compute step never stays SCHEDULED or DISPATCHED.

In this history, assess is a compute step and charge a worker task:

$ orbit logs order-A-100
+     0ms   #1  WorkflowCreated           
+     0ms   #2  WorkflowStarted           
+     1ms   #3  StepSucceeded             step=assess attempt=1
+     1ms   #4  SignalAwaitStarted        step=decision signal=review
...
+   540ms   #7  StepScheduled             step=charge attempt=1
+   547ms   #8  StepDispatched            step=charge attempt=1 worker_id=orders-python
+   556ms   #9  StepSucceeded             step=charge attempt=1

Lambdas and collection functions

Collection functions take a list and a lambda, a one-parameter function written name => expression:

FunctionResult
map(list, x => expr)a list of expr for each element
filter(list, x => cond)the elements for which cond is true
find(list, x => cond)the first element for which cond is true, or null
any(list, x => cond)true if cond is true for at least one element
all(list, x => cond)true if cond is true for every element
sum(list)the sum of a list of numbers

A lambda is only allowed as the second argument of map, filter, find, any and all (E0415 elsewhere), and its parameter must not reuse the name of a step, a loop variable, an enclosing lambda parameter, input or execution (E0413). Lambdas and these functions work in every expression, not only in compute steps. See Built-in functions for signatures and details, and Expressions for the syntax.

schema Line {
    sku: String
    quantity: Int
    unit_price: Float
}

workflow Totals {
    version "1.0.0"
    input { lines: List<Line> }

    step totals {
        compute {
            count: len(input.lines)
            units: sum(map(input.lines, l => l.quantity))
            subtotal: sum(map(input.lines, l => float(l.quantity) * l.unit_price))
            bulk: filter(input.lines, l => l.quantity >= 10)
            first_free: find(input.lines, l => l.unit_price == 0.0)
            all_in_stock: all(input.lines, l => l.quantity > 0)
        }
    }

    step line_totals {
        for line in input.lines
        compute { sku: line.sku, total: float(line.quantity) * line.unit_price }
    }

    step per_unit {
        compute totals.subtotal / float(totals.units)
        when totals.units > 0
    }

    return {
        units: totals.units
        subtotal: totals.subtotal
        bulk_skus: map(totals.bulk, l => l.sku)
        free_sku: totals.first_free.sku
        line_totals: line_totals
        per_unit: per_unit
    }
}
$ orbit run totals.orbit --id totals-1 --input '{"lines": [{"sku": "A", "quantity": 12, "unit_price": 2.5}, {"sku": "B", "quantity": 1, "unit_price": 0.0}]}'
deployed Totals@1.0.0 (sha256:01f95045bc38)
started execution totals-1
  ✓ line_totals                    succeeded 0ms
  ✓ line_totals[0]                 succeeded 
  ✓ line_totals[1]                 succeeded 
  ✓ per_unit                       succeeded 
  ✓ totals                         succeeded 

COMPLETED totals-1  Totals@1.0.0
  elapsed: 2ms
{
  "units": 13,
  "subtotal": 30.0,
  "bulk_skus": [
    "A"
  ],
  "free_sku": "B",
  "line_totals": [
    {
      "sku": "A",
      "total": 30.0
    },
    {
      "sku": "B",
      "total": 0.0
    }
  ],
  "per_unit": 2.3076923076923075
}

first_free has type Line?, because find may find nothing, so totals.first_free.sku is a String?.

Fan-out

With for x in list, a compute step evaluates its expression once per element, with x bound to the element. As for task steps, the runtime records StepFanOut, then one unit per element (line_totals[0], line_totals[1], …), each with its own StepSucceeded, and finally the step’s output: the list of element values, in input order. The output type is List<T>, where T is the type of the expression.

+     2ms   #4  StepFanOut                step=line_totals
+     2ms   #5  StepSucceeded             step=line_totals[0] attempt=1
+     2ms   #6  StepSucceeded             step=line_totals[1] attempt=1
+     2ms   #7  StepSucceeded             step=per_unit attempt=1
+     2ms   #8  StepSucceeded             step=line_totals attempt=1

If any element fails, the step fails. See Fan-out.

Conditions

A compute step with when is skipped when the condition is false or null, and its output is null. Its output type is optional (T?, or List<T>? with for), so readers use coalesce or is_null. In the example above, per_unit is skipped for an empty order, which avoids a division by zero. See Conditions.

Errors

The type checker reports type errors in the expression as usual, mostly E0400 (type mismatch), E0405 (optional value where a value is required), E0406 (operator not defined) and E0407 (unknown function or wrong number of arguments).

Some errors can only happen at runtime: division by zero, integer overflow, a failed conversion such as int("abc"), or an operation on a JSON value of the wrong shape. Such an error fails the step (or the element) with the non-retryable code EXPRESSION_ERROR and the message evaluating `compute`: <cause>. The execution then aborts and compensates like after any other step failure.

workflow Discount {
    version "1.0.0"
    input { subtotal: Float, discount_percent: Int = 0 }

    step factor {
        compute input.subtotal * float(100 / (100 - input.discount_percent))
    }
}
$ orbit run discount.orbit --id discount-1 --input '{"subtotal": 10.0, "discount_percent": 100}'
deployed Discount@1.0.0 (sha256:910bcb4107fa)
started execution discount-1
  ✗ factor                         failed evaluating `compute`: division by zero

FAILED discount-1  Discount@1.0.0
  elapsed: 1ms
  compensation: none
  error: EXPRESSION_ERROR step `factor` failed: evaluating `compute`: division by zero

IR version

A workflow with a compute step, or with a lambda anywhere, compiles to orbit-ir/2. See Versioning.

See also