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, closed
- Coach marks it achieved: forget_fact closes the January fact (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 is a database query, not an
inference round trip, and session open stays quick. user_id is a string you choose
(1 to 255 characters, no '/' or control characters, and not '.' or '..') that identifies 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
contradicts it. A goal reached is not a contradiction: a new goal can be
true alongside the old goal. A past event such as a delivered talk is
stored as an already-closed fact (valid_from and
invalid_at on the same day), so it appears only with
include_invalidated=True. The extractor picks predicate names
such as committed_to, so they can vary. When the coach marks
the goal achieved, the app calls
forget_fact(goal_fact_id, at="2026-05-04") (or writes a
memory that says the goal is done), and the January fact is closed on
that date; the new facts stay alongside 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 | Closed, achieved |
| Anna delivered the Q1 all-hands talk | 2026-05-04 | 2026-05-04 | Closed, delivered |
| Anna wants to lead the EMEA expansion pitch | 2026-05-04 | null | 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) run_id tags the conversation, so you can search or list one
session's memories; the facts extracted from every session land directly
under the user_id, which is how March's "I bailed on the
demo" is still there, dated, in May. The write path is where the
intelligence runs: embeddings, entity extraction, and typed-fact
extraction with contradiction checking. All of it is included in the
write: one write for every 6,000 characters of a memory, counted against
your plan's monthly writes. A memory holds up to 16,000 characters and a
longer one is refused with a 422, so split a long transcript into several
add calls with the same run_id. 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, and the recap reads the history
(include_invalidated=True), because the delivered talk is a
closed fact. That is a coaching product, not a chatbot with a nice
system prompt.
Why supersede beats overwrite
Progress is a sequence of closed 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, every closed fact keeping the date it ended.
The coachee can see all of this. Show them
get_profile in your app and wire Correct and Forget to
correct_fact and forget_fact
(human in the loop),
so the end user can view and correct the facts held about them, and
close the ones that are no longer true (forget_fact keeps
a closed fact in the history). To erase everything held about them,
wire a "Delete my data" button to delete_all(user_id=...),
which physically deletes every memory and fact of that end user.
In coaching, that's a selling point, not a checkbox: "here
is everything your coach remembers, and you decide what stays."
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 writes and queries per month. 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.