Examples·Counter

Counter

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.

§What the example demonstrates

Counter demonstrates the complete loop:

Snippet
1current document: counter = 0
2+ delivered timeline entry: increment by 5
3+ Coordination contracts
4+ BEX Compute step
5= new document: counter = 5
6+ emitted Counter/Incremented event
7+ 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.

§The document

YAML
1name: Counter
2type: Common/Record
3counter: 0
4contracts:
5 ownerChannel:
6 type: Coordination/Timeline Channel
7 timelineId: counter-demo
8
9 increment:
10 type: Coordination/Operation
11 channel: ownerChannel
12 request:
13 amount:
14 type: Integer
15
16 incrementImpl:
17 type: Coordination/Sequential Workflow Operation
18 operation: increment
19 steps:
20 - name: IncrementAndEmit
21 type: Coordination/Compute
22 do:
23 - $let:
24 name: nextCounter
25 expr:
26 $add:
27 - $document: /counter
28 - $event: /message/request/amount
29 - $appendChange:
30 op: replace
31 path: /counter
32 val:
33 $var: nextCounter
34 - $appendEvent:
35 type: Counter/Incremented
36 amount:
37 $event: /message/request/amount
38 counter:
39 $var: nextCounter
40 - $return:
41 changeset:
42 $changeset: true
43 events:
44 $events: true

Read it in two layers.

The state layer is tiny:

YAML
1name: Counter
2counter: 0

The contract layer explains how state can change:

YAML
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.

§The channel

YAML
1ownerChannel:
2 type: Coordination/Timeline Channel
3 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.

§The operation

YAML
1increment:
2 type: Coordination/Operation
3 channel: ownerChannel
4 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.

§The workflow implementation

YAML
1incrementImpl:
2 type: Coordination/Sequential Workflow Operation
3 operation: increment
4 steps:
5 - name: IncrementAndEmit
6 type: Coordination/Compute
7 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 BEX step

The Compute step starts by calculating the next counter value:

YAML
1- $let:
2 name: nextCounter
3 expr:
4 $add:
5 - $document: /counter
6 - $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:

YAML
1- $appendChange:
2 op: replace
3 path: /counter
4 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:

YAML
1- $appendEvent:
2 type: Counter/Incremented
3 amount:
4 $event: /message/request/amount
5 counter:
6 $var: nextCounter

Finally it returns the accumulated changes and events:

YAML
1- $return:
2 changeset:
3 $changeset: true
4 events:
5 $events: true

The Coordination processor applies the returned changeset and emits the returned events because this BEX runs inside Coordination/Compute.

§The input entry

A timeline entry requesting the operation can look like this:

YAML
1type: Coordination/Timeline Entry
2timeline:
3 timelineId: counter-demo
4timestamp: 1
5message:
6 type: Coordination/Operation Request
7 operation: increment
8 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.

§Expected processing result

Before processing:

YAML
1counter: 0

After processing the entry once:

YAML
1counter: 5

The processor also emits an event like:

YAML
1type: Counter/Incremented
2amount: 5
3counter: 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.

§Step-by-step trace

Use this trace to debug your first implementation:

StepWhat happensWhat to inspect
1Parse the Counter document.The root has counter: 0 and a contracts map.
2Resolve repository types.Coordination/Operation, Coordination/Compute, and core types resolve.
3Deliver the timeline entry.The entry has timelineId: counter-demo.
4Channel matches.ownerChannel accepts the entry.
5Operation matches.message.operation is increment.
6Request validates.amount is an integer.
7Workflow runs.incrementImpl is bound to increment.
8BEX reads state.$document: /counter returns 0.
9BEX reads request.$event: /message/request/amount returns 5.
10BEX returns result.changeset contains replace /counter with 5; events contain Counter/Incremented.
11Processor applies changes.Output document has counter: 5.
12Processor emits events.Triggered events include Counter/Incremented.
13Checkpoint is written.Replaying the same entry should not increment again.

If any step fails, debug that layer before moving on.

§Why this example is enough

Counter may look too small, but it covers the essential mechanics:

  • a document carries state;
  • a channel gates delivered evidence;
  • an operation declares request shape;
  • a workflow implements the operation;
  • Compute executes BEX;
  • BEX returns changes and events;
  • the processor applies and emits;
  • checkpointing prevents duplicate processing.

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.

§Common variations

Increment by a bare integer

Some examples use request: 5 instead of request: { amount: 5 }. That is fine if the operation declares the request itself as an integer:

YAML
1request:
2 type: Integer

Then the BEX read path would be:

YAML
1$event: /message/request

Choose one shape and keep it consistent in the docs and tests.

Reject negative increments

Add a guard:

YAML
1- $if:
2 cond:
3 $lt:
4 - $event: /message/request/amount
5 - 0
6 then:
7 - $appendEvent:
8 type: Counter/IncrementRejected
9 reason: negative_amount
10 - $return:
11 changeset:
12 $changeset: true
13 events:
14 $events: true

This demonstrates a business rejection event. The workflow remains deterministic, and the invalid request is visible rather than silently ignored.

Enforce schema instead of a BEX guard

If negative values should be invalid at the request-shape level, constrain the operation request:

YAML
1request:
2 amount:
3 type: Integer
4 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.”

§Test plan

A good Counter test should do more than assert the final number.

  1. Process amount: 5 and assert /counter == 5.
  2. Assert one Counter/Incremented event was emitted.
  3. Assert the output document has a different BlueId from the input document.
  4. Replay the same timeline entry and assert /counter remains 5.
  5. Deliver a second distinct entry with amount: 2 and assert /counter == 7.
  6. If negative increments are disallowed, assert the rejection path.

Once Counter passes, move to Weekend package checkout.