LearnQuickstart: Concert App

Quickstart: Concert App

⚠️

Requires a Causet runtime. This quickstart uses Causet Cloud (Early access). Self-hosted Docker is Coming soon. To learn the model without a runtime, try the 5-Minute Quickstart first. See What runs today?.

In this guide you build the concert app used throughout the getting-started tutorials: users follow artists, artists announce shows, and a query returns upcoming shows for artists a user follows.

You will author every layer of the Causet DSL in order:

  1. State (Entities) — what exists
  2. Events — immutable facts
  3. Intents (Actions) — commands and rules
  4. Projections — read models from events
  5. Queries — named reads over projections
  6. Relationships — graph edges between entities

Then compile, deploy, submit intents, and run a query.

Prerequisites: Install Causet (CLI + Causet Cloud early-access account).


App setup

causet init

When prompted:

PromptChoose
Project nameconcert-app
TemplateConcert app
cd concert-app

The Concert app template scaffolds app.causet and starter DSL files for every layer below. This page walks through what each file does — open them in your new project as you read.

# app.causet
dsl_version: 1
app: concert_app
 
includes:
  states:        [./states/**/*.state.causet]
  events:        [./events/**/*.events.causet]
  actions:       [./actions/**/*.actions.causet]
  projections:   [./projections/**/*.projections.causet]
  queries:       [./queries/**/*.queries.causet]
  relationships: [./relationships/**/*.relationships.causet]

Files use the .causet extension. The actions: key in the manifest defines intents — the docs call them intents; the DSL section is named actions:.


1. State (Entities)

Entities are typed records the runtime stores per ID. Define state before events or intents — everything else references these types.

# states/user.state.causet
state:
  user:
    entity_key: user_id
    fields:
      - name: username
        type: string
        default: ""
      - name: following_count
        type: int
        default: 0
# states/artist.state.causet
state:
  artist:
    entity_key: artist_id
    fields:
      - name: name
        type: string
        default: ""
      - name: follower_count
        type: int
        default: 0

Each entity instance is addressed by (stream, entity_id) — e.g. user user-1 on the user stream.

See State & Memory for snapshots, replay, and entity keys.


2. Events

Events are past-tense facts appended to the ledger. Register every event type before referencing it in intents or projections.

# events/follow.events.causet
events:
  ARTIST_FOLLOWED:
    state: user
    entity_expr: event.user_id
    payload:
      user_id:   string
      artist_id: string
 
  ARTIST_UNFOLLOWED:
    state: user
    entity_expr: event.user_id
    payload:
      user_id:   string
      artist_id: string
# events/show.events.causet
events:
  SHOW_ANNOUNCED:
    state: artist
    entity_expr: event.artist_id
    payload:
      artist_id: string
      show_id:   string
      venue:     string
      date:      string
      title:     string

Never use reserved payload names: type, ts, entity_id — the runtime sets those.

See Events for naming, metadata, and versioning.


3. Intents (Actions)

Intents are commands your app submits. Rules run in three phases: preflightcoreside_effects.

# actions/follow.actions.causet
actions:
  FOLLOW_ARTIST:
    state: user
    entity_id_expr: intent.user_id
    input:
      user_id:   { type: string, required: true }
      artist_id: { type: string, required: true }
 
    preflight:
      rules:
        - name: reject_self_follow
          when: { expr: "intent.user_id == intent.artist_id" }
          then:
            - op: reject
              code: CANNOT_FOLLOW_SELF
 
    core:
      rules:
        - name: increment_following
          when: {}
          then:
            - op: add
              path: /following_count
              value: 1
 
    side_effects:
      rules:
        - name: emit_followed
          then:
            - op: emit
              event_type: ARTIST_FOLLOWED
              payload:
                user_id:   intent.user_id
                artist_id: intent.artist_id
# actions/show.actions.causet
actions:
  ANNOUNCE_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 }
 
    core:
      rules:
        - name: emit_announced
          when: {}
          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

See Intents for rule phases and the full operations reference.


4. Projections

Projections are PostgreSQL read models maintained from events. Always declare fields: with explicit types.

# projections/follow.projections.causet
projections:
  user_following:
    source_events: [ARTIST_FOLLOWED, ARTIST_UNFOLLOWED]
    target:
      table: user_following
      primary_key: [user_id, artist_id]
    fields:
      user_id:     TEXT
      artist_id:   TEXT
      followed_at: BIGINT
    derive:
      user_id:     event.user_id
      artist_id:   event.artist_id
      followed_at: event.ts
    mutations:
      ARTIST_FOLLOWED:   { op: upsert }
      ARTIST_UNFOLLOWED: { op: delete }
 
  artist_show_directory:
    source_events: [SHOW_ANNOUNCED]
    target:
      table: artist_show_directory
      primary_key: [show_id]
    fields:
      show_id:    TEXT
      artist_id:  TEXT
      title:      TEXT
      venue:      TEXT
      date:       TEXT
      created_at: BIGINT
    derive:
      show_id:    event.show_id
      artist_id:  event.artist_id
      title:      event.title
      venue:      event.venue
      date:       event.date
      created_at: event.ts
    mutations:
      SHOW_ANNOUNCED: { op: upsert }

See Projections for derive, aggregates, and indexes.


5. Queries

Queries are named, parameterized reads over projection tables — no hand-written SQL.

# queries/follow.queries.causet
queries:
  shows_for_followed_artists:
    from: artist_show_directory
    input:
      user_id: { type: string, required: true }
    joins:
      user_following:
        on:
          artist_show_directory.artist_id: user_following.artist_id
        fields:
          - user_following.user_id
    fields:
      - artist_show_directory.*
    where:
      user_following.user_id: { eq: input.user_id }
    order_by:
      date: asc
    limit: 50

See Queries for filters, joins, and aggregates.


6. Relationships

Relationships declare graph edges between entity types. Here, a user follows an artist — backed by the same follow/unfollow events.

# relationships/follow.relationships.causet
relationships:
  artist_followers:
    from: user
    to:   artist
    cardinality: many_to_many
    unique: true
    emit_events:
      created: ARTIST_FOLLOWED
      removed: ARTIST_UNFOLLOWED

Relationships enable graph traversals and enforce edge uniqueness. Event emission in the intent still drives projections and the ledger.

See Relationships for cardinality and emit event wiring.


Compile

causet build validate --runtime .
causet build compile  --runtime . --out dist/

Expected artifacts in dist/:

ruleset.vrd
causet.projections.json
manifest.json

Fix compiler errors before continuing — common issues: missing projection fields:, undeclared event types in source_events, reserved payload field names.


Deploy

causet context use platform my-platform
causet context use app concert-app
 
causet release create  --input dist/ --tag 1.0.0
causet release publish --tag 1.0.0
causet deploy apply    --tag 1.0.0 --fork main
causet deploy activate --tag 1.0.0 --fork main --mode FULL

Or one command — causet deploy runs compile, release, publish, and activate as a single golden path:

causet deploy --fork main --runtime . --full

Deploy applies projection DDL to schema my_platform_concert_app_main — creating user_following and artist_show_directory.

See Deployments for forks, releases, and staging workflows.


Run it

Follow an artist:

causet intent FOLLOW_ARTIST \
  --fork main \
  --stream user_stream \
  --entity user-1 \
  --payload '{"user_id":"user-1","artist_id":"artist-pearl-jam"}'

Announce a show:

causet intent ANNOUNCE_SHOW \
  --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"}'

Wait a moment for projection materialization, then query:

causet query shows_for_followed_artists \
  --fork main \
  --param user_id=user-1

You should see the Pearl Jam show in the response.


What just happened

  1. Intent FOLLOW_ARTIST ran preflight, mutated entity state, and emitted ARTIST_FOLLOWED.
  2. The projection worker UPSERTed a row into user_following.
  3. Intent ANNOUNCE_SHOW emitted SHOW_ANNOUNCEDartist_show_directory.
  4. Query joined both tables and returned shows for followed artists.

Next steps

Layer-by-layer tutorials (same concert app from causet init):

TutorialPage
Set upYour First Statecauset initConcert app
StateYour First State
EventsYour First Event
IntentsYour First Intent
ProjectionsYour First Projection
QueriesYour First Query
RelationshipsYour First Relationship
WorkflowsYour First Workflow
Full appComplete Concert App
TopicPage
Intent rules in depthIntents
Deploy to stagingDeployments
Extended concert domainConcert App
No CLI/Cloud access yet?5-Minute Quickstart — browser only