Your First Workflow (Saga)
Continuing the concert app: real processes span multiple steps. Sagas track which step an entity is on; submit and schedule drive the next step after each commit.
This tutorial extends the app with a two-step show publish flow on the artist stream — draft first, then publish — while still emitting the same SHOW_ANNOUNCED event the rest of the app expects.
Project: Concert app from causet init. Add the saga and draft/publish intents described below.
Prerequisites: Your First Intent through Your First Relationship.
Saga vs submit
| Pattern | Use for |
|---|---|
| Saga | Recording current step when matching events arrive |
submit chain | Enqueueing the next intent after the current one commits |
schedule | Delayed reminders (e.g. day-before show nudge) |
New event: draft saved
# events/show.events.causet (add to existing file)
events:
SHOW_DRAFT_SAVED:
state: artist
entity_expr: event.artist_id
payload:
artist_id: string
show_id: string
venue: string
date: string
title: stringSHOW_ANNOUNCED stays unchanged — projections and queries keep working.
Saga: show publish flow
# sagas/show.sagas.causet
sagas:
show_publish_flow:
state: artist
state_path: _tmp/show_publish
steps:
- name: idle
set: { status: "idle" }
- name: draft_saved
on: SHOW_DRAFT_SAVED
set: { status: "draft" }
- name: published
on: SHOW_ANNOUNCED
set: { status: "published" }
end: trueUnder the hood, the compiler lowers each step with on: into an ordinary event-triggered core rule that merges the step’s set: values into state_path — it fires when the matching event is committed to this entity’s stream. You don’t need to declare _tmp/show_publish in fields: yourself either — the compiler auto-injects it as an object field on artist, defaulted from the idle step’s set: values.
Ordering note. The saga’s generated rule reacts to SHOW_DRAFT_SAVED as a separate, subsequent intent — not synchronously within SAVE_SHOW_DRAFT. Since SAVE_SHOW_DRAFT also auto-submits PUBLISH_SHOW immediately (below), and both target the same entity, relying on the saga alone to set status before PUBLISH_SHOW’s preflight runs would be a race. That’s why Step 1’s core rule below also merges { status: "draft" } into _tmp/show_publish directly, synchronously, in the same intent that submits PUBLISH_SHOW — the saga’s merge is then a redundant (harmless) confirmation once SHOW_DRAFT_SAVED commits. Use a direct core rule whenever a value must be visible to code that runs immediately afterward in the same intent; use the saga for durable, replay-safe step tracking that other rules and projections read later.
Step 1: save draft
# actions/show.actions.causet (add intent)
actions:
SAVE_SHOW_DRAFT:
state: artist
entity_id_expr: intent.artist_id
input:
artist_id: { type: string, required: true }
show_id: { type: string, required: true }
venue: { type: string, required: true }
date: { type: string, required: true }
title: { type: string, required: true }
core:
rules:
- name: mark_draft
when: {}
then:
- op: merge
path: /_tmp/show_publish
value: { status: "draft" }
side_effects:
rules:
- name: emit_draft_and_enqueue_publish
then:
- op: emit
event_type: SHOW_DRAFT_SAVED
payload:
artist_id: intent.artist_id
show_id: intent.show_id
venue: intent.venue
date: intent.date
title: intent.title
- op: submit
intent_type: PUBLISH_SHOW
target_stream: artist_stream
target_entity: intent.artist_id
payload:
artist_id: intent.artist_id
show_id: intent.show_id
venue: intent.venue
date: intent.date
title: intent.titleThe core rule merges status: "draft" into _tmp/show_publish synchronously, in the same intent — see the ordering note above for why. The saga’s own generated rule fires separately once SHOW_DRAFT_SAVED commits, merging the same value into the same path.
Step 2: publish show
PUBLISH_SHOW:
state: artist
entity_id_expr: intent.artist_id
input:
artist_id: { type: string, required: true }
show_id: { type: string, required: true }
venue: { type: string, required: true }
date: { type: string, required: true }
title: { type: string, required: true }
preflight:
rules:
- name: must_have_draft
when: { expr: "entity._tmp.show_publish.status != 'draft'" }
then:
- op: reject
code: NO_DRAFT_TO_PUBLISH
core:
rules:
- name: mark_published
when: {}
then:
- op: merge
path: /_tmp/show_publish
value: { status: "published" }
side_effects:
rules:
- name: emit_announced
then:
- op: emit
event_type: SHOW_ANNOUNCED
payload:
artist_id: intent.artist_id
show_id: intent.show_id
venue: intent.venue
date: intent.date
title: intent.title
- op: schedule
event_type: SHOW_REMINDER_SENT
delay_seconds: 86400
payload:
artist_id: intent.artist_id
show_id: intent.show_idAdd SHOW_REMINDER_SENT to events/show.events.causet if you use the schedule. Preflight on a future PROCESS_REMINDER intent can no-op if the show was cancelled.
ANNOUNCE_SHOW from Your First Intent still works for one-shot announcements — the workflow is an optional stricter path.
Run via CLI
# Step 1 — save draft (auto-submits PUBLISH_SHOW)
causet intent SAVE_SHOW_DRAFT \
--fork main \
--stream artist_stream \
--entity artist-pearl-jam \
--payload '{"artist_id":"artist-pearl-jam","show_id":"show-pj-brooklyn-2026","venue":"Barclays Center","date":"2026-09-15","title":"Pearl Jam - Dark Matter Tour"}'Inspect saga progress:
causet inspect entity artist-pearl-jam \
--fork main \
--stream artist_streamCheck _tmp/show_publish.status → "published". Then run the query as before.
Use Timeline to trace the submit chain.
Rules of thumb
Sagas observe events; they do not push work. Use submit to advance.
No external I/O in rules. HTTP and ad-hoc LLM calls belong outside the runtime.
Mark terminal saga steps with end: true.
Concert app so far
concert-app/
app.causet
states/ user.state.causet, artist.state.causet
events/ follow.events.causet, show.events.causet
actions/ follow.actions.causet, show.actions.causet
projections/ follow.projections.causet
queries/ follow.queries.causet
relationships/ follow.relationships.causet
sagas/ show.sagas.causetNext steps
- Complete Concert App — full file tree, compile, deploy, run
- Workflows — timers, testing, advanced patterns
- Concert App reference — check-ins, reviews, richer domain