LearnYour First Workflow

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

PatternUse for
SagaRecording current step when matching events arrive
submit chainEnqueueing the next intent after the current one commits
scheduleDelayed 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:     string

SHOW_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: true

Under 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.title

The 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_id

Add 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_stream

Check _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.causet

Next steps