Story: Observe a run while it works
Table of Contents
This page documents a story in Product backlog — inbox, carried unfinished from Sprint 26 at close. It captures the goal, current status, acceptance criteria, and the tasks that compose it.
1. Goal
A person watching a run sees, at every moment, what each step is, how far into it the work has come, and what it has done so far; a person reading a run that has rested sees the same narrative, and why it stopped if it did.
The capability belongs to the workflow component, because every workflow in the system needs it, and the setup journeys are its first consumer. Today a step answers once, when it ends: a long step shows nothing while it runs, and what it did on the way is written to a service log that no user will ever read.
2. Status
| Field | Value |
|---|---|
| State | BACKLOG |
| Parent sprint | Sprint 26 |
| Now | Not yet started. |
| Waiting on | Nothing. |
| Next | Break the story into tasks. |
| Last touched | 2026-09-29 |
3. Acceptance
- Every step states what it is doing in the reader's words from the moment it is dispatched: a name, a description, and the arguments it acts on.
- A step that is working states how far it has come, as a completed count against a total, or says that the total is not yet known.
- A step's work is readable while it runs and after it rests: an ordered journal of what happened, each entry carrying its level.
- A step that delegates to another run names that run, and that run's own progress is visible without the nested run's record being copied.
- Nothing about progress or the journal reaches the audit trail or the history of the step's own entity: progress is a register and the journal is a log, and neither is a business fact.
- A run that has rested keeps its journal, and the diagnostics that explain a failure land with the step's state rather than only on the live channel.
- Every sentence a person reads is either a fixed sentence the client may translate or a key with values, never a sentence with values baked into it.
4. Plan
The design is one distinction. Four things travel with a step, they have four different lifetimes, and today they share one home: the step's own versioned row, written once, at the end.
| What | Lifetime | Home | Audited |
|---|---|---|---|
| State: the step's machine, its result, its error, its timings | transition-driven, must survive a restart | the step's temporal row, as today | yes |
| Context: what the step is, what it will do to the reader's data, what it waits on, whether it can be retried | fixed when the run starts | the run's materialised step snapshot, which already carries the name and description | yes, with the run |
| Progress: how far into the step | overwritten while the step works, worthless once it rests | a latest-value register keyed by step | no |
| Journal: what happened, in order | appended while the step works and kept after it rests | an append-only log with a per-step sequence | no |
The rules the split gives. A step's four things are stored once each, and never in two places. The engine is the only writer of a step's record: progress and journal entries reach it as events, because a handler that wrote directly would put two writers on one step and let a service claim an instance or a tenant the engine knows better. Nothing a person needs after the fact travels only on the live channel, so the diagnostics that explain a failure land with the terminal state in the same write, as they do today; the live channel carries the narrative of work in flight, and losing its last line is acceptable. Progress is never audited: it is a reading about work in flight, and a versioned history of every tick would swamp the history and make the audit trail say something it does not mean.
The shapes. Progress is one non-temporal row per step — the step, its instance and tenant, a completed count, a total where zero means indeterminate, a key with its values, and when it was last written — overwritten in place. The journal is append-only rows in sequence, each carrying its tenant, instance and step, the time, a level that separates narration from warning and error, a key with its values, and a message. The read that a page already polls carries, per step, the state as it does today, the context from the run's own snapshot, the live progress, and the tail of the journal; a second read pages the whole journal for a run that has rested, by sequence, so a client can fetch only what it has not seen.
The channels. Progress and journal entries are published while a step works and the engine records them, coalesced: one write a second, or one per threshold crossed, rather than one per item, because a step that reports per party would otherwise write a row per party. They are best effort, which is why the authoritative diagnostics stay with the completion. The read is polled, as it is today; the journal is the one thing here that would push cleanly, because it has a sequence and a cursor.
Delegation answers itself. A step that waits on another run — the bundle publication is one, and it waits on a run of thirty-seven steps — states that run as part of its context. The nested run is already a run with its own steps and its own read, and the wait already counts them, so nothing is duplicated: the page either follows that run or says how far into it the step has come.
Where it lives. The entities, the widened step contract and the widened read belong to the workflow component, which owns runs and steps. The journeys consume them, and this story is not part of the setup journeys' work.
The order. The two stores and their channels first, engine-written and coalesced, with the read carrying progress and the journal tail. Then the step contract: a progress call and a note call beside the calls that end a step, and the completion's per-item entries absorbed into the journal. Then the context in the snapshot — the arguments rendered, what the step waits on, whether it can be retried — most of which is a rendering of what the definition and the starting point already declare. Then the screen: a running step shows its progress and its latest line, and a rested or failed run expands into its journal.
5. Tasks
| Task | State | Start | End | Description |
|---|---|---|---|---|
6. Decisions
- Four things travel with a step, and each has one home: state in the step's temporal row, context in the run's materialised snapshot, progress in a latest-value register, and the journal in an append-only log.
- The engine is the only writer of a step's record. Handlers report; they do not write the step.
- Progress is never audited, versioned or replayed. It is a reading about work in flight, and the step's outcome replaces it.
- Nothing a person needs after the fact travels only on the live channel.
- The live channels are coalesced and best effort; the durable write happens once, with the outcome.
- A step that delegates to another run states which run it waits on instead of restating that run's progress.
- The capability belongs to the workflow component, not to a journey. Every workflow needs it, and the setup journeys are its first consumer.
- Text a person reads is either a fixed sentence, which a client may translate by looking up the sentence, or a key with its values. A sentence with values baked into it cannot be translated and is not written.
7. Open questions
These are settled when the story is broken into tasks; each names the answer I would argue for.
- Does the journal absorb the per-item entries a completion already carries, or sit beside them? Two homes for one thing is how they drift, so I would absorb them: one store, one read, and the completion still appends in the same write as the terminal state.
- Who coalesces progress, the engine or the handler, and by rate or by threshold? I would coalesce by rate in the engine, because the engine is the single writer and can see the churn it is creating, and a handler that forgets to throttle should not be able to melt the store.
- Is a journal kept for the life of the run, or purged on a policy? Its value is support, and support has a shorter life than the audit trail the run keeps.
- Does the journal push, or is it polled with the rest? It has a sequence and a cursor, so it is the one thing here that pushes cleanly; polling first costs nothing.
- Does the screen state run-level progress — the step the run is on out of the steps it declares — or is the rail's own shape enough?
- When a step waits on a run, does the screen follow that run, state how far into it the step has come, or both?
8. Out of scope
- The journey screens that consume this: the setup journeys own their own screens, and this story owns what those screens can read.
- Backfilling journals for runs that have already happened.
- The engine's change events: a run's own event payload stays as it is, and the progress read, not the event, remains the contract a page follows.
- A general telemetry pipeline. The journal is about one step of one run, and the service logs stay where they are.