worked example · crm

A profile that maintains itself.

Avery takes ownership of a migration, asks for written updates, and gets a new role. Those ordinary CRM events become the current profile on the right—while a low-value email open stays out. Every line can be traced back to the event that supports it.

Simulation · example walkthrough. Avery Chen, the CRM events, and the derived profile are fictional examples that show the workflow step by step. No live CRM or customer record is being changed.

postgres + one worker · 5 keys · every slot cited

document · contact:avery-chen current
roleSVP Product at Acme Cloud — product and platform2 cited
commitmentsAtlas migration launches in August · SSO before the renewal signature2 cited
preferencesConcise written updates the day before any call1 cited
open_threadsImport defect from the June release, still unresolved1 cited
goals— no supporting evidence yet
Nothing in this document was typed by a person. Every line names the CRM events it was fused from, and the run that wrote it.

00 · the problem

Why is the CRM always out of date?

Because keeping it current is somebody’s unpaid second job. The emails, tickets and calls all arrive on their own; turning them into what is true about this account right now is manual, so it happens late, partially, or never — and the rep joins the call reading a field someone typed two quarters ago.

the usual options

Type it in, or summarise everything.

  • A human keeps it current, until the week gets busy.
  • Summarise everything, and an opened email sits beside a contract.
  • No provenance. You cannot tell a fact from a guess.
with memseek

Append the events, and let the profile follow.

  • Your app only appends what happened.
  • Each event is scored before anything durable is written.
  • Every line names the events behind it.
  1. an event arrives

    Your app reports what happened. It does not decide what the profile should say.

    “Avery now owns the Atlas migration and committed to an August launch.”
  2. the profile follows

    Nobody edits a field. The commitment appears, with the event behind it.

    commitments — Atlas migration launches in August
  3. noise stays out

    A marketing email gets opened. The profile does not change.

    “Opened the release-notes email.” Still searchable, still on file — just not something a rep should act on.
  4. what you write

    Append the event, read the profile. That is the whole integration.

    await memseek.records.ingest(collection="crm_events",
        entity="contact:avery-chen", text=note)
    
    doc = await memseek.document(entity="contact:avery-chen")
    doc["beliefs"]   # each one with its citations
4 events in, three fields changedThe email open changes nothing — for that event, silence is the correct output.
An empty field stays emptyThere is no evidence for goals, so nothing is invented to fill it.
Ask any field whyField → the run that wrote it → the events it was fused from.

New to Memseek? You write one small configuration file describing what your application should remember. After that your application only appends what happened; Memseek does the deriving, keeps every conclusion linked to the evidence underneath it, and hands your agent a bounded briefing instead of a pile of text. Everything below is that file and what it produces — how it works.

the words the rest of this page usesplain english
event
Something that happened, stored once and never edited.
slot, keyed
A named profile field with one current value and its older versions kept.
derivation
The automatic step that reads the new events plus the current profile and rewrites only what moved.
citation
The stored link from a profile line back to the events behind it.
1
the evidence arrives

A commitment is history first—not a profile edit.

The application reports that Avery owns the Atlas migration and committed to August. It appends exactly what happened; it does not decide which profile field should change.

observed eventSalesforce · commitment · “Avery now owns the Atlas migration and committed to an August launch.”
collections/crm.yamlthe evidence contract
name: crm_events
mode: event
schema:
  required: [text, source, event_kind]
required_processors:
  - crm_embedding
  - importance
mode: event

Append a new fact to history. Never overwrite the event that later conclusions must prove themselves against.

required

Force every write to say what happened, where it came from, and what kind of event it is.

importance

Score salience after the write so low-value interactions can remain searchable without becoming durable profile knowledge.

writes now

One immutable crm_events record holding the sentence as it arrived — “Avery now owns the Atlas migration and committed to an August launch.” The current profile is still untouched.

2
the contract decides

The derivation sees new evidence and the profile that already exists.

The YAML makes the maintenance rule explicit: consider only new CRM events, read the five current slots, and emit no more than those same five keys. The prompt explains the semantic boundary between a commitment, a preference, a role, an open thread, and a goal.

derivations/crm_profile.yamlthe update rule
trigger:
  accumulator:
    metric: importance
    threshold: 1
sources:
  new_crm_events:
    kind: changes
    collections: [crm_events]
  current_profile:
    kind: current
    collections: [user_profiles]
emit:
  collection: user_profiles
  type: profile
  keys: [role, commitments,
         preferences, open_threads, goals]
trigger.accumulator

Run when enough meaningful evidence accumulates. The example uses a deliberately low threshold so each seeded case step is visible.

kind: changes

Give the model the new evidence that caused this run—not an unbounded CRM history.

kind: current

Also show the present profile so list-like slots can preserve still-valid claims and replace only what new evidence supersedes.

keys

These are the only legal outputs. The model cannot invent a “personality” or “hobbies” field.

writes next

A new current version of commitments — “Atlas migration launches in August” — citing the Salesforce event. The prior version remains in history.

3
the case resolves

Three useful events change three slots. The email open changes none.

The same rule handles Avery’s written-update preference and promotion. The product login remains in the event log but supports no action-worthy slot, so the correct output is silence. A second derivation then rewrites one summary from the complete current slot set.

salesforce · commitment importance 9

Avery owns Atlas and committed to an August launch.

support · preference importance 7

Send a concise written recap the day before every call.

hubspot · role importance 8

Avery was promoted to SVP Product; platform is now in scope.

product · interaction importance 2

Opened the release-notes email.

crm_profile
route by meaning
user_profiles · commitments

Atlas migration launches in August.

user_profiles · preferences

Concise written updates the day before any call.

user_profiles · role

SVP Product at Acme Cloud — product and platform.

email open · no profile write

The event stays searchable in history, but it supports none of the five durable profile slots.

Visible effect: 4 events remain in history; only 3 change current profile state.

derivations/crm_summary.yamlthe readable result
trigger:
  changed:
    collections: [user_profiles]
    types: [profile]
sources:
  changed_slots:
    kind: changes
  current_slots:
    kind: current
emit:
  collection: user_profiles
  type: summary
  keys: [summary]
changed_slots

Explains why the summary must be regenerated and advances a cursor so settled profiles do not loop forever.

current_slots

Build the sentence from the whole live profile, not only the field that changed most recently.

type: summary

Keep the presentation layer separate from the durable slot records it cites.

final state

Three slots now hold cited values: role “SVP Product at Acme Cloud — product and platform”, commitments “Atlas migration launches in August · SSO before the renewal signature”, preferences “Concise written updates the day before any call”. goals stays absent, and one summary record points at the slots it synthesized.

Continue into the full event-by-event run →

01 · the write

Your application only ever appends.

One call per event, replay-safe by dedupe_key. The events are immutable: nothing here is ever edited or deleted, so the profile can always be rebuilt from them.

crm_events · append-onlyimportance scored
salesforce · commitmentimportance 9
Avery now owns the Atlas migration and committed to an August launch.
support · preferenceimportance 7
Asked for a written recap the day before every call.
hubspot · roleimportance 8
Avery promoted to SVP Product; platform now in scope.
product · interactionimportance 2
Opened the release-notes email.
what happens on writeno code of yours
crm_embeddingprocessor→ every record findable by meaning

Declared as a required processor, so a record is not ready until it has run.

importancescore · llm→ scores.importance, scale 1–10

One number per event: should this change a durable profile? It is what the accumulator sums.

deal_signalsjson · llm→ stage + churn_risk

Optional structured enrichment; churn_risk is promoted into scores for ranking.

embedscoreaccumulator tripsderive

02 · the derivation

One file decides what the profile is.

Sources bound what the run may see. Limits bound what it may spend. The emission bounds which keys it may touch — and the schema makes citations mandatory, so an uncited belief is rejected rather than stored.

  • 01
    accumulatorSummed importance crosses the threshold, so the run fires itself. The importance-2 interaction above never trips it alone.
  • 02
    changes + currentThe run sees only the new events plus the profile it already holds — that is what makes it incremental instead of a full rebuild.
  • 03
    view + recordTwo more sources: older commitment history through a named view, and the account playbook as one guarded current record.
  • 04
    emit keyedFive keys, each replaced in place. A rewrite supersedes the old version; the old version stays on file.
derivations/crm_profile.yamlbounded
name: crm_profile
trigger:
  accumulator: {metric: importance, threshold: 6}
  cooldown_s: 1
sources:
  new_crm_events:     {kind: changes, collections: [crm_events]}
  current_profile:    {kind: current, keys: [role, commitments, …]}
  account_playbook:   {kind: record,  key: playbook}
  commitment_history: {kind: view,    view: crm_history}
model: strong
limits:
  max_llm_calls: 2
  max_visible_records: 100
  max_total_tokens: 20000
  max_wall_s: 60
tasks:
  - id: result
    use: llm
    with:
      output_schema:        # citations are schema, not etiquette
        citations: {type: array, minItems: 1,
                     items: {format: uuid}}
emit:
  collection: user_profiles
  keys: [role, commitments, preferences, open_threads, goals]

03 · the glass box

Ask any slot why it believes that.

A belief names the run that wrote it; the run names the events it fused; each event is an immutable record a human or a system actually produced. The descent bottoms out — there is no layer below it that only the model saw.

belief
user_profiles/profile · key commitments

Atlas migration launches in August · SSO before the renewal signature

b704e1c2-…
↳ run
crm_profile · trigger accumulator · model strong

2 LLM calls · 41 visible records · 9.4s wall · keyed divergence on 2 of 5 keys

8770fd3a-…
↳ event
crm_events/crm_event · salesforce · importance 9

"Avery now owns the Atlas migration and committed to an August launch."

9f1c55e8-…
↳ event
crm_events/crm_event · support · importance 8

"SSO has to ship before they sign the renewal."

4d18a07b-…
bottomed out at 2 authored events · depth 0 → 1 · every hop dereferenced by ID

04 · the reviewed path

When you want a rebuild, you approve it.

The incremental run maintains the profile on its own. A full reconstruction from the complete corpus is a different, explicitly-enqueued derivation — and it stages a draft instead of activating, because its emission declares review: required.

crm_profile_rebuild · candidatereview: required
divergence · 5 keys covered
− role: VP of Product at Acme Cloud
+ role: SVP Product at Acme Cloud — product and platform
= preferences, open_threads unchanged
+ goals: retire the legacy import path this year
your call promote → nothing is live until you do
why promotion can be refusedcompare-and-set
stale guardcheck→ input heads re-checked

Promotion re-reads the heads captured before the task ran. If the corpus moved underneath the rebuild, activation is rejected as stale rather than clobbering newer evidence.

complete keyscontract→ crm_profile_candidate@1

The reviewed artifact names the exact key set a candidate must cover, so a partial rebuild cannot be promoted over a full profile.

decliningno-op→ the draft stays a draft

There is nothing to undo: an un-promoted candidate remains in the audit trail and live memory was never touched.

05 · from your app

Publish once. Then write and read.

The application never triggers enrichment, never schedules the derivation, and never assembles the prompt. It appends events and reads a document.

the whole integration4 calls
catalog.publishonce→ crm_user_profile@2.0.0

Versioned definitions into a workspace. Re-publishing is how the memory design changes.

records.ingest_manyper event→ returns immediately

The worker owns everything after the write.

documentper read→ beliefs + freshness

Current keyed state, plus whether the derivation is caught up.

render_artifactper prompt→ rendered + manifest

A token-budgeted block with a hashed input manifest — replayable.

app.pypublic SDK
async with MemseekClient(BASE_URL, API_KEY) as memseek:
    await memseek.catalog.publish(
        package="crm_user_profile@2.0.0",
        directory="examples/crm_profile_catalog",
    )

    # append an event — that is the whole write path
    await memseek.records.ingest(
        collection="crm_events", type="crm_event",
        entity="contact:avery-chen",
        text="SSO has to ship before they sign the renewal.",
        content={"source": "support", "event_kind": "commitment",
                 "account_id": "acme-cloud"},
        dedupe_key="support:evt:5512",
    )

    # read what is true now, and how fresh it is
    doc = await memseek.document(
        entity="contact:avery-chen", collections="user_profiles",
    )
    for belief in doc["beliefs"]:
        print(belief["key"], belief["text"], belief["citations"])

    doc["freshness"]   # {"crm_profile": {"stale": false, …}}

    # or hand the agent a deterministic prompt block
    brief = await memseek.render_artifact(
        "crm_profile_brief", entity="contact:avery-chen",
        query="commitments and launch dates",
    )
    brief["manifest"]["rendered_sha256"]  # same inputs → same bytes

The catalog, the five keys, the processors, and the SDK calls above are the checked-in examples/crm_profile_catalog and examples/sdk_crm_profile.py. Model-written text — the profile wording, the divergence — is representative of a real provider run rather than a captured transcript, because that text varies by model.

start building

Run this profile in about a minute.

The walkthrough seeds its own disposable workspace, waits while the service derives the profile from nothing, then hands you a prompt so you can type a CRM note and watch it fold in.

examples/sdk_crm_profile.py
# postgres, the api, and a worker
$ make database && source .env.sh
$ uv run memseek migrate
$ uv run uvicorn memseek.api:app &
$ uv run memseek worker &
# then talk to the profile
$ uv run python examples/sdk_crm_profile.py
profile ▸ Avery wants renewals opened a quarter early