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
Version Pinning Every STEP references an exact block version (e.g., 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.

Explicit Data Dependencies Because every data flow is declared with MAP, the workflow engine knows the exact dependency graph. This makes debugging easy — you can trace any value back to its source.

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"
Skipped Steps Return No Output If a step is skipped due to a WHEN condition, its output will be undefined. Any subsequent step that references the skipped step's output should account for this — or use its own WHEN condition.

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.

When to Use Parallel Use PARALLEL when steps have no data dependencies on each other — for example, fetching data from multiple sources, or running independent calculations.
Error Behavior If any step inside a PARALLEL group fails, the entire group is considered failed and the workflow's ON_ERROR handler (if defined) will be invoked.

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.

Reusability Sub-workflows are a powerful way to encapsulate common patterns. For example, an "OrderProcessing" workflow can be called from "FullCheckout", "RetryOrder", or "BulkProcess" — without duplicating steps.

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)
One Handler Per Workflow Each workflow supports a single ON_ERROR handler declared at the top level. If you need different error handling for different steps, consider splitting them into sub-workflows with their own ON_ERROR handlers.

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 vs _initial Use 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
Same Syntax, Different Runtimes Because both backend and UI workflows use the same .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.