Counter is the smallest useful Blue workflow. It has one piece of state, one channel, one operation, one workflow implementation, one BEX Compute step, one emitted event, and one checkpoint.
That makes it a better first example than a “hello world” document. A static hello world shows Blue as a data format. Counter shows Blue as a deterministic document-processing model.
Counter demonstrates the complete loop:
1current document: counter = 02+ delivered timeline entry: increment by 53+ Coordination contracts4+ BEX Compute step5= new document: counter = 56+ emitted Counter/Incremented event7+ checkpoint for delivered entry
Every larger workflow in these docs uses the same pieces. The weekend package checkout has more actors, more child documents, and more guards, but it still follows this same loop.
1name: Counter2type: Common/Record3counter: 04contracts:5 ownerChannel:6 type: Coordination/Timeline Channel7 timelineId: counter-demo89 increment:10 type: Coordination/Operation11 channel: ownerChannel12 request:13 amount:14 type: Integer1516 incrementImpl:17 type: Coordination/Sequential Workflow Operation18 operation: increment19 steps:20 - name: IncrementAndEmit21 type: Coordination/Compute22 do:23 - $let:24 name: nextCounter25 expr:26 $add:27 - $document: /counter28 - $event: /message/request/amount29 - $appendChange:30 op: replace31 path: /counter32 val:33 $var: nextCounter34 - $appendEvent:35 type: Counter/Incremented36 amount:37 $event: /message/request/amount38 counter:39 $var: nextCounter40 - $return:41 changeset:42 $changeset: true43 events:44 $events: true
Read it in two layers.
The state layer is tiny:
1name: Counter2counter: 0
The contract layer explains how state can change:
1contracts:2 ownerChannel: ...3 increment: ...4 incrementImpl: ...
That split matters. Blue documents do not change because someone writes a new value into the YAML file. They change because delivered evidence is accepted by contracts and processed into a new document.
1ownerChannel:2 type: Coordination/Timeline Channel3 timelineId: counter-demo
The channel says where relevant entries should arrive. In a test, you can provide a local fixture timeline provider that delivers entries with timelineId: counter-demo. In production, the timeline provider would be responsible for ordering, actor attribution, source verification, and completeness.
The channel does not say what the entry means. It only gates delivery.
1increment:2 type: Coordination/Operation3 channel: ownerChannel4 request:5 amount:6 type: Integer
The operation names the action and declares the request shape. A valid request should include an integer amount.
The operation still does not mutate state. It says which action can be requested through the channel. The implementation lives in the workflow.
1incrementImpl:2 type: Coordination/Sequential Workflow Operation3 operation: increment4 steps:5 - name: IncrementAndEmit6 type: Coordination/Compute7 do: ...
This contract binds the increment operation to a sequential workflow. The workflow contains one step, IncrementAndEmit, and that step is a Coordination/Compute step.
A larger workflow might have several steps. Counter keeps only one so you can inspect the full path without scrolling.
The Compute step starts by calculating the next counter value:
1- $let:2 name: nextCounter3 expr:4 $add:5 - $document: /counter6 - $event: /message/request/amount
$document: /counter reads the current document state. $event: /message/request/amount reads the delivered operation request. $add produces the new value. $let stores it in a local variable.
Then it appends a patch:
1- $appendChange:2 op: replace3 path: /counter4 val:5 $var: nextCounter
This does not directly mutate the document inside BEX. It appends a patch entry to the BEX result changeset.
Then it appends an event:
1- $appendEvent:2 type: Counter/Incremented3 amount:4 $event: /message/request/amount5 counter:6 $var: nextCounter
Finally it returns the accumulated changes and events:
1- $return:2 changeset:3 $changeset: true4 events:5 $events: true
The Coordination processor applies the returned changeset and emits the returned events because this BEX runs inside Coordination/Compute.
A timeline entry requesting the operation can look like this:
1type: Coordination/Timeline Entry2timeline:3 timelineId: counter-demo4timestamp: 15message:6 type: Coordination/Operation Request7 operation: increment8 request:9 amount: 5
The shape is deliberately explicit:
type: Coordination/Timeline Entry says this is delivered timeline evidence.timeline.timelineId: counter-demo lets the channel match it.message.type: Coordination/Operation Request says the entry requests an operation.message.operation: increment names the operation.message.request.amount: 5 supplies the request payload.When this entry is processed against the Counter document, the operation and workflow match.
Before processing:
1counter: 0
After processing the entry once:
1counter: 5
The processor also emits an event like:
1type: Counter/Incremented2amount: 53counter: 5
And it records checkpoint metadata so the same delivered entry does not run the mutation twice.
The exact checkpoint shape is runtime/repository-specific, but the behavior is not optional for a robust timeline-driven workflow. Duplicate delivery should not create duplicate business effects.
Use this trace to debug your first implementation:
| Step | What happens | What to inspect |
|---|---|---|
| 1 | Parse the Counter document. | The root has counter: 0 and a contracts map. |
| 2 | Resolve repository types. | Coordination/Operation, Coordination/Compute, and core types resolve. |
| 3 | Deliver the timeline entry. | The entry has timelineId: counter-demo. |
| 4 | Channel matches. | ownerChannel accepts the entry. |
| 5 | Operation matches. | message.operation is increment. |
| 6 | Request validates. | amount is an integer. |
| 7 | Workflow runs. | incrementImpl is bound to increment. |
| 8 | BEX reads state. | $document: /counter returns 0. |
| 9 | BEX reads request. | $event: /message/request/amount returns 5. |
| 10 | BEX returns result. | changeset contains replace /counter with 5; events contain Counter/Incremented. |
| 11 | Processor applies changes. | Output document has counter: 5. |
| 12 | Processor emits events. | Triggered events include Counter/Incremented. |
| 13 | Checkpoint is written. | Replaying the same entry should not increment again. |
If any step fails, debug that layer before moving on.
Counter may look too small, but it covers the essential mechanics:
The weekend package checkout does not introduce a different model. It introduces more state, more contracts, child documents, PayNote semantics, payment safety, and idempotency guards. Counter is how you learn the loop before the business vocabulary arrives.
Some examples use request: 5 instead of request: { amount: 5 }. That is fine if the operation declares the request itself as an integer:
1request:2 type: Integer
Then the BEX read path would be:
1$event: /message/request
Choose one shape and keep it consistent in the docs and tests.
Add a guard:
1- $if:2 cond:3 $lt:4 - $event: /message/request/amount5 - 06 then:7 - $appendEvent:8 type: Counter/IncrementRejected9 reason: negative_amount10 - $return:11 changeset:12 $changeset: true13 events:14 $events: true
This demonstrates a business rejection event. The workflow remains deterministic, and the invalid request is visible rather than silently ignored.
If negative values should be invalid at the request-shape level, constrain the operation request:
1request:2 amount:3 type: Integer4 schema:5 minimum: 0
Use schema for structural validity. Use BEX guards for process-dependent conditions, such as “you cannot attach a payment token before the provider confirms the order.”
A good Counter test should do more than assert the final number.
amount: 5 and assert /counter == 5.Counter/Incremented event was emitted./counter remains 5.amount: 2 and assert /counter == 7.Once Counter passes, move to Weekend package checkout.