BEX is Blue Expression Objects: deterministic logic written as Blue-compatible data. In the current Coordination examples, BEX usually appears inside Coordination/Compute steps, where it reads the current document and delivered event, computes values, appends patch operations, appends event payloads, and returns a result.
BEX is intentionally not JavaScript. It is not WASM. It is not a network runtime. It is not a database trigger. It does not mutate the document by itself. It computes a deterministic result, and the host decides how to use that result.
Business rules often need to travel with the document they govern. If the logic lives only in private service code, a receiver can inspect the current state but not the rule that produced it. If the logic is embedded as arbitrary code, replay becomes difficult and the security boundary becomes vague.
BEX gives Blue documents a smaller surface:
This is enough for many workflow tasks: validation, guarded state transitions, patch construction, event construction, projections, and idempotency checks.
A BEX operator is an object with exactly one key that starts with $:
1$add:2 - 23 - 3
Objects with normal keys are literal Blue-compatible objects:
1status: confirmed2amountMinor: 499003currency: PLN
This means BEX programs are still Blue data. They can be stored in documents, referenced, hashed, inspected, and transported like other Blue content.
BEX reads data through explicit operators.
| Operator | Reads from |
|---|---|
$document | The current document view. |
$event | The delivered event binding, commonly the current timeline entry or triggered event. |
$binding | A named host-provided binding. |
$currentContract | The current contract binding when the host provides it. |
$steps | Prior step results when the host provides them. |
$var | A local variable. |
$const | A declared program constant. |
Counter uses the two most common reads:
1$add:2 - $document: /counter3 - $event: /message/request/amount
The first operand reads the current counter value from the document. The second reads the requested increment amount from the delivered event.
A Compute step often uses $appendChange:
1- $appendChange:2 op: replace3 path: /counter4 val:5 $add:6 - $document: /counter7 - $event: /message/request/amount
This appends a patch entry to the BEX result changeset. It does not directly edit the document in-place. Later, the Compute host can retrieve the accumulated changes with $changeset:
1- $return:2 changeset:3 $changeset: true
In a Coordination/Compute step, the Coordination processor applies the returned changeset directly. That is why Compute is the right surface for dynamic document updates.
Coordination/Update Document is useful for literal patch lists. Coordination/Compute is the dynamic surface when patch contents depend on document state or event data.
A BEX program can accumulate events:
1- $appendEvent:2 type: Counter/Incremented3 amount:4 $event: /message/request/amount
And return them:
1- $return:2 events:3 $events: true
In a Compute step, returned events are emitted by the host processor. The BEX program does not publish to a queue or call another system directly.
This is what makes event construction inspectable. The event payload is part of the document-owned logic.
1- name: IncrementAndEmit2 type: Coordination/Compute3 do:4 - $let:5 name: nextCounter6 expr:7 $add:8 - $document: /counter9 - $event: /message/request/amount10 - $appendChange:11 op: replace12 path: /counter13 val:14 $var: nextCounter15 - $appendEvent:16 type: Counter/Incremented17 amount:18 $event: /message/request/amount19 counter:20 $var: nextCounter21 - $return:22 changeset:23 $changeset: true24 events:25 $events: true
Read it line by line:
$let computes a local value called nextCounter.$appendChange adds a JSON-patch-like replace operation for /counter.$appendEvent records a domain event payload.$return returns the accumulated changeset and events.There is no hidden state. The only inputs are the document, event, constants, variables, and host bindings.
The weekend package example uses BEX guards heavily. A guard checks current state and returns unchanged when an action has already happened or is not allowed.
For payment token attachment, the first guard is simple:
1- $if:2 cond:3 $eq:4 - $document: /payment/tokenAttached5 - true6 then:7 - $return:8 changeset:9 $changeset: true10 events:11 $events: true
If /payment/tokenAttached is already true, the program returns the current accumulated result. Since no changes or events have been appended yet, this is an unchanged result.
This pattern is one reason the weekend package source document is a strong example. Real distributed flows retry. A document-owned guard makes repeated delivery safe at the business layer, not just the transport layer.
Another weekend package pattern checks whether a component order is already attached. If the incoming hotel order snapshot refers to the same existing document or session, the workflow may replace the existing snapshot. If it refers to a different order, the workflow emits a rejection event instead of overwriting the package.
The small core of that logic is:
1- $if:2 cond:3 $and:4 - $var: hasExisting5 - $not:6 $var: sameExisting7 then:8 - $appendEvent:9 type: Conversation/Event10 kind: Component Order Attachment Rejected11 orderKind: hotel12 reason: component_order_already_attached13 - $return:14 changeset:15 $changeset: true16 events:17 $events: true
This is not just validation. It is audit-friendly validation. The document can emit a structured explanation of why it did not accept an entry.
The PayNote inside the weekend package should request payment completion only after both merchant orders confirm.
The BEX shape is:
1- $if:2 cond:3 $or:4 - $ne:5 - $document: /embeddedDocs/orders/hotelOrder/confirmation/status6 - confirmed7 - $ne:8 - $document: /embeddedDocs/orders/restaurantOrder/confirmation/status9 - confirmed10 - $truthy:11 $document: /completionRequested12 then:13 - $return:14 changeset:15 $changeset: true16 events:17 $events: true18- $appendChange:19 op: replace20 path: /completionRequested21 val: true22- $appendEvent:23 type: PayNote/Complete Payment Requested24 amount: 49900
The guard returns unchanged if either confirmation is missing or completion was already requested. Only when both child confirmations are present does the program update /completionRequested and emit PayNote/Complete Payment Requested.
This is the kind of rule that should be visible in the document. It is central to the checkout safety story.
BEX uses JSON Pointer paths for document and value access. When building paths from dynamic segments, use $pointerJoin rather than string concatenation:
1$pointerJoin:2 - embeddedDocs3 - orders4 - $var: orderSlot5 - confirmation6 - status
If a segment contains / or ~, $pointerJoin escapes it correctly. This prevents bugs where a value intended as one field name is interpreted as multiple pointer segments.
Use static pointers when possible. Use $pointerJoin when the path is data-dependent.
Use $let for local values that are read more than once:
1- $let:2 vars:3 request:4 $event: /message/request5 currentStatus:6 $document: /status
When one local depends on another in the same block, use ordered binding:
1- $let:2 order: [request, amount]3 vars:4 request:5 $event: /message/request6 amount:7 $var:8 name: request9 path: /amountMinor
Use constants for values that belong to the program definition rather than the delivered event:
1constants:2 expectedAmount: 499003expr:4 $const: expectedAmount
Functions are useful when the same validation or payload construction appears in several branches. Keep functions small. A BEX program should remain inspectable by readers who are not language implementers.
BEX core has no network, filesystem, clock, randomness, database, JavaScript, or WASM access. A host can register intrinsics, but the program must name the intrinsic through static Blue type data, and the engine must have a matching processor.
That boundary is deliberate. For example, signature verification can be modeled as a host intrinsic, but the intrinsic must define exactly what bytes are verified and must charge gas. It should not become a generic escape hatch for arbitrary host code.
A good intrinsic is:
BEX execution accounts for gas. Gas gives hosts a way to measure and limit computation. A BEX result can include gasUsed and metrics in addition to the returned value, changeset, and events.
Gas should not be described as a magical billing system. It is execution accounting. A host may enforce limits, reject programs that exceed budgets, expose metrics to tests, or use gas to compare fixture behavior.
For documentation examples, the important rule is simple: BEX computation should be bounded and deterministic. A program that loops over delivered request items should have predictable iteration behavior. Object iteration should be deterministic. Host intrinsics should charge gas explicitly.
BEX should fail deterministically when required inputs are missing or malformed and the program says they are required.
Use $failIf for explicit validation:
1- $failIf:2 cond:3 $not:4 $exists:5 $event: /message/request/id6 message: request id is required
Use rejection events when the business process should continue and record a rejected action. Use deterministic failure when the computation itself cannot proceed validly.
That distinction matters in workflows. A malformed event might be a deterministic failure. A valid but unauthorized or duplicate component order might be a business rejection event.
Test BEX in two layers.
First, run the program directly with a known document view and bindings. Assert the returned value, changeset, events, gas, and failure behavior.
Second, run the same logic through Coordination/Compute in a document processor. Assert that the changes are actually applied, events are emitted, and checkpoints are written.
For Counter, the direct BEX test is tiny. For the weekend package, direct tests are valuable for the tricky guards: payment token attachment, PayNote attachment, component order attachment, and completion gates.
BEX is the document-owned deterministic logic layer. It lets a Blue document say how to compute a transition without giving that logic hidden authority over the outside world.
When reading examples, look for three things:
If you can answer those questions, the workflow is inspectable.