Appearance
Run a whole story
One accepted message proves your serialiser works. A whole story proves your state handling works, and that's where most integrations break: every snapshot is checked against the one before it. Why, and the rules in brief: Snapshots, not deltas.
The sequence rules
Build every snapshot from your complete current state of the story, never from what changed.
| Rule | Why | Broken |
|---|---|---|
| Include every top-level member you've sent before | A missing member erases it for every consumer | 409 snapshot.members_missing |
| Keep every asset that's still part of the story | A dropped asset disappears downstream | 409 snapshot.assets_dropped |
Never change an asset's asset_type | An asset_id means one thing for its whole life | 409 asset.type_changed |
sequence_number strictly increases | Consumers keep the highest. An equal value means two writers collided | 409 sequence_number.not_increasing |
updated_at never goes backwards | The same, in time | 409 updated_at.backwards |
A KILLED, SPIKED or ARCHIVED story stays terminal | A dead story doesn't come back | 409 story_type.left_terminal |
Keep one correlation_id for the story | It keeps the story in order for every consumer | Accepted, with correlation_id.changed under tolerated |
Two more things the bus allows:
- A gap in
sequence_number(5, then 7): legal, but a snapshot may be missing. Reported undertoleratedin the202(sequence_number.gap). lifecycledisappearing when a story leavesACTIVE: the schema requires that, so it isn't counted as a missing member. Nothing is reported.
Each snapshot is a new message with a new message_id. Reusing one for a different message is 409message_id.reused.
Walk a breaking story
Publish the seven snapshots of the published hurricane run, in order, as one story: your own story_id, one correlation_id, a new message_id each. Every one should be accepted with 202.
| Seq | What changes |
|---|---|
| 1 | Hurricane approaching: ACTIVE, phase DEVELOPING |
| 2 | Phase BREAKING; a press-conference feed asset is added while still capturing |
| 3 | The feed is captured and ready |
| 4 | Upgraded: new headline, higher priority, a new source |
| 5 | An AI transcript asset, derived from the feed |
| 6 | A claim extracted as an assertion; a lower-third asset behind a pending standards gate |
| 7 | The claim confirmed: the gate approved, a new headline |
Then open the story on the portal's Story timeline (see Send your first message). Select a snapshot to see what changed since the previous one. It should show only what you meant to change: an unintended difference is a bug in your state handling, even though the bus accepted it.
Now do the same through your product: create the story in its UI or API, make the same edits, and let it publish.
End a story
| Send | Expect |
|---|---|
The next snapshot with story_type: KILLED (and no lifecycle) | 202 |
The one after with story_type: ACTIVE | 409 story_type.left_terminal |
The one after with story_type: ARCHIVED | 202: moving between terminal states is allowed |
Messages of other families
Messages about a story share its correlation_id, whatever their family. That keeps them in order with the story's snapshots for every consumer.
| Family | Tie it to the story with | Topic, by convention |
|---|---|---|
link.* | The story's correlation_id; an asset_id from the snapshot | som.link.committed |
telling.* | The story's correlation_id; the link_id it tells | som.telling.started |
delivery.media_available | The story's correlation_id when the story is known | som.delivery.media_available |
skill.warning.raised | The story's correlation_id; causation_id = the snapshot's message_id | som.skill.warning.raised |
system.audit | The story's correlation_id | som.system.audit |
topiconly has to start withsom.. The bus routes onmessage_type, never ontopic, though a consumer can ask for a topic-prefix filter.- Set
causation_idwhenever your message reacts to another one. - Only
story.contextis checked against earlier messages. Pairs such as a telling's start and end aren't checked by the bus, but publishers will expect them to be coherent.
Order and concurrency
- Wait for the answer before you send the next message about the same story. The bus keeps a story in the order it accepts messages: fire two snapshots of one story in parallel, and the second may be accepted first and the other refused as stale.
- Different stories can go in parallel.
- One writer per story. The first system to publish a story owns its snapshots, usually the NCS; other systems react with their own families. A snapshot from another system gets
story.not_owner: a warning by default, a409if your workspace chooses. A workspace can list hand-offs, such as NCS to web CMS (Story ownership). Two writers racing on one story also get409sequence_number.not_increasingorcommit.contention.
Tests and reruns
The bus remembers where every story in your workspace stands (its sequence state, not the payload) with no expiry, so a rerun needs a new story_id. Started again under an old one, snapshot 1 is refused as stale.
Checkpoint
- [ ] My product published a whole story, and every snapshot was accepted.
- [ ] The portal's diff between snapshots shows only the changes I intended.
- [ ] A terminal story can't be revived by my product, and the
409is handled. - [ ] Every other message I produce for a story carries its
correlation_id; reactions carrycausation_id. - [ ] My product never sends two messages for the same story at once.
Next: Handle the bus's answers.