Recommendation Memory
For LLM prompt context via semantic search over ledger events, see Vector Memory and the Support Copilot example. This page covers deterministic entity-state memory built in core rules.
Causet’s event-sourced entity state is a natural foundation for personalization features. This example shows how to build rich user memory from concert events — the kind of structured context that powers recommendations, AI features, and “Year in Review” summaries.
The idea
Instead of maintaining separate analytics tables, user preferences, and recommendation signals, everything derives from the event stream:
- A user checks into 48 concerts —
concert_count: 48 - They attend Pearl Jam 7 times — tracked in
top_artists - They travel 1,200 miles to shows —
total_distance_miles - Their favorite venue emerges from check-in frequency — queryable from projection
This data is:
- Replayable — rebuild it at any time from the ledger
- Auditable — every number traces back to a specific event
- Consistent — derived from the same event stream as everything else
- AI-ready — structured context for LLM features
State definition
# states/user.state.causet
state:
user:
entity_key: user_id
fields:
- name: concert_count
type: int
default: 0
- name: unique_artists_seen
type: int
default: 0
- name: unique_venues_visited
type: int
default: 0
- name: top_artists
type: array
item_type: object
item_fields:
artist_id: { type: string, required: true }
play_count: { type: int, required: true }
default: []
- name: genre_counts
type: array
item_type: object
item_fields:
genre: { type: string, required: true }
count: { type: int, required: true }
default: []Building memory via core rules
When a user checks in at a show, core rules update the memory fields:
# actions/checkin.actions.causet
actions:
CHECK_IN:
state: user
input:
show_id: { type: string, required: true }
user_id: { type: string, required: true }
artist_id: { type: string, required: true }
genre: { type: string, required: true }
venue_id: { type: string, required: true }
core:
rules:
- name: increment_concert_count
when: {}
then:
- op: add
path: /concert_count
value: 1
- name: update_artist_memory
when: {}
then:
- op: find
path: /top_artists
where: "it.artist_id == intent.artist_id"
as: existing_artist
- op: if
expr: "existing_artist == null"
then:
- op: push
path: /top_artists
value:
artist_id: intent.artist_id
play_count: 1
else:
- op: map
path: /top_artists
as: a
value: "a.artist_id == intent.artist_id ? {artist_id: a.artist_id, play_count: a.play_count + 1} : a"Memory projection for queries
While entity state holds the memory, a projection makes it queryable at scale:
# projections/user_memory.projections.causet
projections:
user_concert_summary:
source_events: [USER_CHECKED_IN]
target:
table: user_concert_summary
primary_key: [user_id]
fields:
user_id: TEXT
concert_count: BIGINT
unique_artists_count: BIGINT
unique_venues_count: BIGINT
last_checkin_at: BIGINT
derive:
user_id: event.user_id
last_checkin_at: event.ts
aggregates:
USER_CHECKED_IN:
concert_count: { op: add, by: 1, floor: 0 }
indexes:
- columns: [concert_count]
direction: desc
artist_affinity:
source_events: [USER_CHECKED_IN, ARTIST_FOLLOWED]
target:
table: artist_affinity
primary_key: [user_id, artist_id]
fields:
user_id: TEXT
artist_id: TEXT
checkin_count: BIGINT
follow_count: BIGINT
affinity_score: BIGINT
updated_at: BIGINT
derive:
user_id: event.user_id
artist_id: event.artist_id
updated_at: event.ts
aggregates:
USER_CHECKED_IN:
checkin_count: { op: add, by: 3, floor: 0 } # checkin = 3x weight
affinity_score: { op: add, by: 3, floor: 0 }
ARTIST_FOLLOWED:
follow_count: { op: add, by: 1, floor: 0 }
affinity_score: { op: add, by: 1, floor: 0 }
indexes:
- columns: [user_id, affinity_score]
direction: descQueries for recommendations
# queries/recommendations.queries.causet
queries:
top_artists_for_user:
from: artist_affinity
input:
user_id: { type: string, required: true }
where:
user_id: { eq: input.user_id }
order_by:
affinity_score: desc
limit: 10
top_concert_goers:
from: user_concert_summary
fields:
- user_concert_summary.user_id
- user_concert_summary.concert_count
- user_concert_summary.last_checkin_at
order_by:
concert_count: desc
limit: 100Why this matters for AI
When you have a grounded, replayable event history, AI features get reliable context:
User memory for user-1:
concert_count: 48
top_artists: [pearl-jam (×7), radiohead (×5), arcade-fire (×4)]
checkin_venues: [barclays-center (×12), stone-pony (×8)]
affinity_score: pearl-jam=24, radiohead=17, arcade-fire=14This is not a summary generated once and forgotten. It’s derived from immutable events and can be rebuilt, audited, or corrected at any time. An AI recommendation engine reading this context can trust it.