Cookbook: a coaching app that tracks the journey
The scenario: a leadership-coaching platform where coachees talk to an AI coach between their human sessions. The human coach meets Anna once a month. The AI coach is there the other twenty-nine days. The value of coaching is the longitudinal arc: the goal set in January, the relapse in March, the breakthrough in May. A stateless LLM restarts at zero on every chat. In this product, memory isn't a feature you bolt on, it is the product. This cookbook walks through the four calls that make it work, and why bi-temporal facts fit a coaching journey better than either a vector store or an overwrite-style memory.
Pick your surface. The snippets below use the Python
SDK. Every call has a REST twin
(GET /v1/facts, POST /v1/memories/search,
POST /v1/memories). Same data model on both; see
surfaces.
The shape of the journey
Six months of Anna, as the memory layer sees it:
January · Kickoff
Goal set
- Fact: Anna wants to improve public speaking · valid_from 2026-01-12
Korely
Fact stored, graph linked
- Typed triple · subject: Anna
March · Check-in
The relapse
- "I bailed on the demo again." Recorded as a dated fact; the January goal stays active
May · Breakthrough
Goal achieved, superseded
- January fact invalidated (invalid_at 2026-05-04) · next goal: lead the EMEA pitch
Note what does not happen in May: the January goal is not deleted. It is invalidated, closed with a date, and the chain stays queryable. That distinction is the rest of this page.
Opening a session
Before the AI coach says a word, pull the longitudinal context. Scope the
read to Anna with user_id to get every typed fact known about
her — goals, milestones, and commitments. The
predicate family is a coarse
taxonomy, so narrow with it only when a predicate maps to a real family
(e.g. preferences for how Anna works and takes feedback);
otherwise filter by the person and read the whole arc.
from korely_memory import Korely
korely = Korely(api_key="kor_live_...")
# Goals, milestones, commitments — every active fact about Anna
goals = korely.get_facts(
user_id="coachee-anna", # your end user's identifier
)
# Working style, feedback preferences
style = korely.get_facts(
user_id="coachee-anna",
predicate_family="preferences",
)
At the March check-in, goals comes back with the January
commitment still active:
{ "facts": [ { "subject": "Anna", "predicate": "committed_to", "predicate_raw": "committed_to", "object": "improving public speaking", "predicate_family": "other", "valid_from": "2026-01-12T09:30:00Z", "invalid_at": null, "invalidated_by": null, "source_memory_id": "mem_3a91c0" } ], "total": 1}
Fact reads are deterministic SQL lookups. No generative model composes
output on the read path, so the call typically returns in under 50 ms
and session open feels instant. user_id is a free-form
string identifying your end user; end users are unlimited on
every tier.
Python and Node.js SDKs are available now; see SDK reference →
Goal evolution is bi-temporal gold
Goals don't get edited in coaching. They get achieved, abandoned, or
replaced, and the replacement is the story. Korely models this with
bi-temporal validity:
every fact carries valid_from and invalid_at,
and a two-stage contradiction check closes the old fact when a new one
supersedes it. The default read returns only what is true now. Pass
include_invalidated=True and you get the whole journey:
journey = korely.get_facts( user_id="coachee-anna", include_invalidated=True, # the arc, not just the present)The chain that comes back is Anna's progress report:
| Fact | valid_from | invalid_at | Status |
|---|---|---|---|
| Anna wants to improve public speaking | 2026-01-12 | 2026-05-04 | Superseded — achieved |
| Anna delivered the Q1 all-hands talk | 2026-05-04 | — | Active |
| Anna wants to lead the EMEA expansion pitch | 2026-05-04 | — | Active |
No "generate a progress narrative" prompt hallucinating dates. The data structure is the report; the LLM only adds prose around it.
Sessions vs. the arc
Two scopes, two jobs. run_id is one conversation; it keeps
a single session coherent. user_id is the person; it
accumulates the cross-session arc that survives when the chat closes.
korely.add( session_transcript, user_id="coachee-anna", # the arc (cross-session) agent_id="exec-coach", # your app's namespace run_id="session-2026-03-14", # this conversation)The session layer is disposable; the user layer is the asset. Salient session facts get promoted into the user-scoped graph. That is how March's "I bailed on the demo" becomes a dated fact in May. The write path is where the intelligence runs: embeddings, entity extraction, and typed-fact extraction with contradiction checking, about a tenth of a cent per memory, all included. Write fire-and-forget during the chat; extraction runs server-side and is asynchronous, so the freshly extracted facts may not appear on the immediate response, but they are in place for the next session open.
The weekly recap
Every Sunday the app sends Anna a "since we started" recap. The trap is generating it from vibes: summarized chat history produces warm, vague, dateless prose. Ground it instead:
# The moat: the dated, typed facts are the backbone of the recapfacts = korely.get_facts( user_id="coachee-anna", include_invalidated=True,)# Secondary: a few raw memory snippets for colour and contexthits = korely.search( "commitments", user_id="coachee-anna", limit=10,)for h in hits: print(h.id, h.score, h.snippet)# Both go into the recap prompt: the LLM writes connective tissue,# every claim has a fact with a date behind it."On January 12 you committed to improving your public speaking. On May 4 you delivered the all-hands talk." Every line is checkable against the table above. That is a coaching product, not a chatbot with a nice system prompt.
Why supersede beats overwrite
Progress is a sequence of superseded facts. That sentence is the whole design argument. The two failure modes it avoids:
- Overwrite-style memory keeps only the present. The arc, the thing the coachee is paying for, is destroyed on every update.
- Append-everything memory keeps the past but can't tell it from the present: the coach retrieves the January goal in June and congratulates Anna on something she finished in May.
Bi-temporal validity is exactly the data structure of a coaching journey: current state on the default read path, full history one parameter away, an explicit supersede edge connecting them.
The coachee can see all of this. Korely's Memory Panel lets the end user view, correct, and delete the facts held about them, built-in forget flows your users control. In coaching, that's a selling point, not a checkbox: "here is everything your coach remembers, and you hold the eraser."
Scaling to multiple coaches
Add programs, say an executive coach and a fitness coach serving the same
person. agent_id is the per-program namespace;
user_id stays the person. Filters are additive (AND):
# Just the exec-coaching program's view of Annakorely.get_facts(user_id="coachee-anna", agent_id="exec-coach")
# Just the fitness programkorely.get_facts(user_id="coachee-anna", agent_id="fitness-coach")
# The whole person, across programskorely.get_facts(user_id="coachee-anna")
The canonical sizing rule applies: one program serving 10,000 coachees is
one agent_id and 10,000 user_id
values, not 10,000 agents. End users are unlimited on every tier; what is
metered is memories written and queries made. See
pricing for quotas. Coaching is the use
case that made us build the temporal layer.
Where to go next
- Temporal facts:
supersede mechanics and point-in-time queries with
as_of. - Memory model: the full scoping model and the three layers behind it.
- API reference: the REST contract the snippets above map to.
- Chatbot that remembers: the support-bot variant of the same four calls.