Debugging with Ledger Events
Every committed intent leaves a permanent record in the ledger. Where timeline show how rules evaluated, the ledger shows what actually changed: entity state updates, domain events emitted, and commit metadata.
The ledger is append-only. Nothing is edited or deleted. If you need to know the authoritative truth for an entity at any point in time, the ledger is it.
Why it is helpful
Application logs tell you a request arrived. Decision routes tell you which branches ran. The ledger tells you what the system committed — the durable outcome that snapshots, projections, and downstream listeners all derive from.
The ledger helps you:
- Verify state changes — see the exact field paths and values written at commit time
- Confirm events were emitted — check that domain events exist before chasing projection or Kafka issues
- Trace by intent — follow one intent ID across every entity it touched
- Audit history — reconstruct the full timeline of an entity on a fork
- Distinguish runtime vs downstream — if an event is in the ledger but missing from a read model, the problem is after commit (projection, query, cache)
What gets stored
Each ledger commit is one cursor advance on an entity fork. A single intent may produce one commit on the target entity and additional commits on other entities if rules wrote cross-entity state or emitted events bound to other streams.
| Stored as | What it is |
|---|---|
| Cursor | Monotonic position on this entity’s timeline — shared with decision routes and snapshots for the same commit |
| Intent ID | The intent that produced this commit |
| Intent type | The action name (e.g. PURCHASE_TICKET, FOLLOW_ARTIST) |
| Patch | The merged state change applied at this cursor — a list of add, replace, and remove operations on entity fields |
| Emits | Domain events appended at this cursor — event type and payload for each |
| Metadata | Commit context: ruleset version, correlation IDs, and other envelope fields |
| Timestamp | When the commit was recorded |
Patch
The patch is the authoritative state mutation. It records operations like “set /cart/total to 42” or “push into /items.” Decision routes show these changes step by step as effects; the patch is the single merged result written to the ledger.
Emits
Emits are the domain events your rules produced — the same events declared in your .events.causet files. They feed projection workers and listeners. If an emit appears here, the runtime did its job; if it is missing from a dashboard, look downstream.
Metadata
Metadata carries commit-level context that is not entity state and not a domain event — versioning, tracing identifiers, and similar envelope data useful when correlating with external systems.
Intent outcome
Alongside the ledger, every intent has a status that tracks its lifecycle:
| Status | Meaning |
|---|---|
| Processing | Accepted and rules are running — not finished yet |
| Committed | Succeeded; ledger commit exists at the recorded cursor |
| Rejected | Validation or rule rejection — includes a code and message |
| Failed | Runtime error — includes error details |
For async submits, wait until status leaves processing before concluding success or failure.
The submitted payload is stored with the intent status so you can compare what was sent with what the rules evaluated.
How decision routes and the ledger fit together
They answer different questions about the same execution:
- Decision routes — per-step: “the
IFtook else, thenFINDreturned not found” - Ledger — per-commit: “here is the final patch and all emits that were persisted”
- Intent status — per-intent: “committed at cursor 1842” or “rejected: INSUFFICIENT_BALANCE”
All three share the intent ID. Committed routes and ledger entries for the same entity also share the cursor.
How to use it
In the control plane
Use the Entity Browser or Decision Log to inspect an entity’s history. Each committed intent appears as a timeline entry. Open a commit to see:
- The state patch applied at that cursor
- Emits produced
- Linked decision route steps for the same intent
- Current snapshot state (materialized from the ledger)
Filter by intent type, time range, or cursor when narrowing down a problem.
When debugging an intent
- Check intent status — did it commit, reject, or fail?
- If committed, open the ledger entry at that cursor — do the patch and emits match your expectation?
- If something looks wrong in the patch, go back to timeline and walk the effects on each step to find where the value diverged.
- If emits are present in the ledger but missing elsewhere, the issue is downstream of commit — projection lag, consumer error, or query cache.
When debugging an entity
Scroll the entity’s ledger history in cursor order. Each entry is one intent’s commit. Compare consecutive patches to see how state evolved. Cross-reference with decision routes when you need to know why a particular value appeared.
Common debugging patterns
| Symptom | What to check |
|---|---|
| Intent seems stuck | Status still processing — runtime or queue issue |
| Intent rejected | Status rejection message + decision route for the last branch or rejection step |
| State is wrong | Ledger patch at cursor vs decision route effects — find the step where values diverged |
| Event missing from UI | Ledger emits first — if present, debug projections or listeners; if absent, debug rules |
| Nothing happened | Decision route no-op — intent type may not match any rule; ledger may have an empty patch |
| Read model stale | Ledger has the event — rebuild or catch up the projection; see Replay |
Durability and ordering
The runtime writes to the ledger before publishing to Kafka. If messaging is temporarily unavailable, the commit still succeeds and events remain durable. Projection workers catch up from the ledger when connectivity returns.
The ledger is the source of truth. Snapshots and projection tables are derived — they can be rebuilt by replaying ledger history.
See also
- Timeline — step-by-step rule evaluation
- Debugging overview — end-to-end forensics workflow
- Events — defining domain events in the Product DSL
- Entity Inspector — browse and scrub entity state
- Replay — rebuild snapshots and projections