Workflows
Declarative orchestration that chains blocks together with data mapping, conditions, and error handling.
Workflows are the glue of BOA. Instead of writing imperative code to call blocks in sequence, you declare the
steps, their data dependencies, and any conditions — and the workflow engine handles the rest. Every
workflow is defined in a .boa file with a clear, human-readable syntax.
Basic Workflow
A workflow declares a sequence of steps, each invoking a specific block version. Data flows between steps
through explicit MAP declarations.
workflow.boa
WORKFLOW OrderProcessing 1.0.0
DESC Calculate the value and tax for a customer order.
STEP calcValue = CalculateOrderValue@1.0.0
MAP items <- _initial.items
STEP calcTax = ComputeTax@1.0.0
MAP subtotal <- calcValue.subtotal
MAP taxRate <- _initial.taxRate
STEP format = FormatString@1.0.0
MAP template <- _initial.template
MAP total <- calcTax.total
Each keyword serves a specific purpose:
| Keyword | Purpose | Example |
|---|---|---|
WORKFLOW |
Declares the workflow with a name and semantic version | WORKFLOW OrderProcessing 1.0.0 |
DESC |
A one-line description of what the workflow does | DESC Calculate value and tax. |
STEP |
Defines a step, assigning a step ID and referencing a block at a pinned version | STEP calcTax = ComputeTax@1.0.0 |
MAP |
Maps an input field of the current step from a source — either _initial or a
previous step's output |
MAP subtotal <- calcValue.subtotal |
ComputeTax@1.0.0). This ensures your workflow
produces the same results even if newer versions of a block exist. Upgrades are always intentional.
Data Flow
The workflow engine maintains a context object that accumulates the output of every step. MAP declarations use dot notation to reference values from this context.
Source References
| Reference | Resolves To | Used In |
|---|---|---|
_initial.field |
The workflow's initial input data | Backend & UI |
stepId.outputField |
The output of a previous step (shorthand) | Backend & UI |
$steps.stepId.output.field |
Full path to a step's output field | UI workflows |
$config.key |
A configuration value declared in the workflow | UI workflows |
How Data Flows Through Steps
Consider this workflow with an input of {"items": [...], "taxRate": 0.08}:
# Step 1: _initial.items is passed as "items" to CalculateOrderValue
STEP calcValue = CalculateOrderValue@1.0.0
MAP items <- _initial.items
# Output: { subtotal: 150.00 }
# Step 2: calcValue.subtotal resolves to 150.00
STEP calcTax = ComputeTax@1.0.0
MAP subtotal <- calcValue.subtotal
MAP taxRate <- _initial.taxRate
# Output: { total: 162.00, tax: 12.00 }
Each step can only reference data from _initial or from steps that have already completed. The
engine resolves all MAP references before invoking the block.
Conditional Execution (WHEN)
The WHEN keyword makes a step conditional. If the condition evaluates to false, the step is
skipped entirely and the workflow continues with the next step.
workflow.boa
STEP derive = DeriveOrderTotals@1.0.0
MAP items <- _initial.items
STEP discount = ComputeDiscount@1.0.0
WHEN $steps.derive.output.itemCount > 3
MAP itemCount <- derive.itemCount
MAP subtotal <- derive.subtotal
STEP finalize = FinalizeOrder@1.0.0
MAP subtotal <- derive.subtotal
MAP discount <- discount.amount
In this example, the discount step only runs if the derived item count is greater than 3. If the
customer orders 3 or fewer items, the step is skipped and no discount is applied.
Supported Operators
| Operator | Meaning | Example |
|---|---|---|
> |
Greater than | WHEN $steps.calc.output.total > 100 |
< |
Less than | WHEN $steps.calc.output.total < 0 |
>= |
Greater than or equal | WHEN $steps.calc.output.qty >= 10 |
<= |
Less than or equal | WHEN $steps.calc.output.qty <= 0 |
== |
Equal to | WHEN $steps.check.output.status == "active" |
!= |
Not equal to | WHEN $steps.check.output.status != "cancelled" |
Parallel Execution
When steps are independent of each other, you can run them concurrently using the PARALLEL /
END_PARALLEL grouping. The engine executes all steps inside the group via
Promise.all().
workflow.boa
WORKFLOW LoadDashboard 1.0.0
DESC Fetch user data and cart data in parallel, then merge.
PARALLEL
STEP fetchUser = FetchUser@1.0.0
MAP userId <- _initial.userId
STEP fetchCart = FetchCart@1.0.0
MAP cartId <- _initial.cartId
END_PARALLEL
# This step waits for both parallel steps to complete
STEP merge = MergeData@1.0.0
MAP user <- fetchUser.user
MAP cart <- fetchCart.cart
Steps inside PARALLEL run concurrently. The workflow engine waits for all
parallel steps to complete before proceeding to the next sequential step after END_PARALLEL.
Workflow Composition (SUB)
Workflows can call other workflows using the SUB keyword. This lets you compose complex
orchestrations from smaller, reusable workflow units.
workflow.boa
WORKFLOW FullCheckout 1.0.0
DESC Run order processing as a sub-workflow, then send confirmation.
STEP processOrder = OrderSubWorkflow
SUB OrderSubWorkflow
MAP orderId <- _initial.orderId
STEP confirm = SendConfirmation@1.0.0
MAP email <- _initial.customerEmail
MAP orderTotal <- processOrder.total
The SUB directive tells the engine to recursively run the referenced workflow. The sub-workflow
receives the mapped inputs and its final output becomes available to subsequent steps, just like any other
block.
Error Handling (ON_ERROR)
The ON_ERROR keyword defines a workflow-level error handler. If any step throws an error, the
engine routes the error to the specified block.
workflow.boa
WORKFLOW CheckoutFlow 1.0.0
DESC Process checkout with error handling.
ON_ERROR HandleCheckoutError@1.0.0
STEP validate = ValidateCart@1.0.0
MAP cartId <- _initial.cartId
STEP charge = ProcessPayment@1.0.0
MAP amount <- validate.total
MAP paymentMethod <- _initial.paymentMethod
The error handler block receives the error details and must return a structured response:
// HandleCheckoutError block output
{
"userMessage": "Payment could not be processed. Please try again.",
"code": "PAYMENT_FAILED",
"recoverable": true
}
| Field | Type | Description |
|---|---|---|
userMessage |
string |
A human-readable error message suitable for display |
code |
string |
A machine-readable error code for programmatic handling |
recoverable |
boolean |
Whether the error is recoverable (e.g., user can retry) |
Configuration (CONFIG)
The CONFIG keyword lets you define key-value pairs that are available to all steps in the
workflow. This is useful for environment-specific values or shared constants.
workflow.boa
WORKFLOW InternationalOrder 1.0.0
DESC Process an order with configurable currency and retry settings.
CONFIG currency = "USD"
CONFIG maxRetries = 3
CONFIG taxEnabled = true
STEP calcValue = CalculateOrderValue@1.0.0
MAP items <- _initial.items
MAP currency <- $config.currency
STEP calcTax = ComputeTax@1.0.0
WHEN $config.taxEnabled == true
MAP subtotal <- calcValue.subtotal
Configuration values are accessed using the $config.key reference in MAP declarations and WHEN
conditions. They provide a clean way to parameterize workflows without hardcoding values into block logic.
CONFIG for static, environment-level settings (currency, feature flags, retry limits). Use
_initial for dynamic, per-execution input data (order items, user IDs, form data).
Backend vs UI Workflows
BOA supports two execution environments for workflows. Both use the same .boa syntax and data
mapping — the difference is in how blocks are invoked at runtime.
Backend Workflows
Blocks execute as child processes via the Universal Runtime Protocol (URP). Each block receives JSON on STDIN and writes JSON to STDOUT. Supports any language — Node, Python, Go, Rust, and more.
UI Workflows
Blocks execute as ES module function calls in the browser. No child processes, no STDIN/STDOUT — just direct function invocation. Ideal for client-side logic, form validation, and UI state management.
Comparison
| Feature | Backend | UI |
|---|---|---|
| Block invocation | Child process (URP) | ES module function call |
| Language support | Any (polyglot) | JavaScript / TypeScript |
| Data mapping syntax | stepId.field |
stepId.field or $steps.stepId.output.field |
| Configuration access | Via input mapping | $config.key |
| WHEN conditions | Supported | Supported |
| PARALLEL | Supported | Supported |
| ON_ERROR | Supported | Supported |
| SUB workflows | Supported | Supported |
.boa format, you can design your workflow once
and the engine handles execution differences transparently. A workflow can even be migrated from server to
client (or vice versa) without rewriting the orchestration logic.