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 facts with start and end dates fit a coaching journey better than either a vector store or an overwrite-style memory. The words end user, fact and closed fact are in the Glossary.
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": "set_goal", "predicate_raw": "set_goal", "object": "improving public speaking", "predicate_family": "events", "valid_from": "2026-01-12T09:30:00Z", "invalid_at": null, "invalidated_by": null, "source_memory_id": "mem_3a91c0" } ], "total": 1}
A fact read uses no generative model, so the call is a database query, not
an inference round trip. Pass user_id every time: a read
without it covers every end user of the project. 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.
The snippets use the Python SDK (korely-memory 0.1.28 was used
to check them); the Node.js SDK has the same calls. See the
SDK reference →
Goal evolution: facts with start and end dates
Goals don't get edited in coaching. They get achieved, abandoned, or
replaced, and the replacement is the story. Korely models this with
temporal facts:
every fact carries valid_from and invalid_at,
and a two-stage contradiction check (a prefilter, then a model judge) can
close 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 (tense is past,
and invalid_at is set when it is written), so it appears only
with include_invalidated=True. The extractor picks predicate names
such as set_goal (the name a real run gave), 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 (illustrative):
| 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 |
In a real run, one memory told the whole story in the past tense ("In
January she set the goal of improving her public speaking. In March she
skipped the demo again. On 4 May she delivered the talk and now wants to
lead the EMEA pitch"). The January goal, the skipped demo and the talk came
back as closed past facts with their own start dates (2026-01-01,
2026-03-01, 2026-05-04), and the new goal came back open. Their
invalid_at was the day the memory was written, not the day the
event ended. To end a goal on the date it was reached, call
forget_fact(fact_id, at="2026-05-04"), as above.
The facts hold the dates, so the LLM does not have to invent them: it adds prose around the data.
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
models run: 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 (usually about ten
seconds), so the freshly extracted facts do not appear on the immediate
response, but they are in place for the next session open. Facts from a
note start on the day the note was written unless the text states a date
or you pass timestamp= to add().
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 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.
Facts with start and end dates fit the 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
(encrypted backups keep a copy for up to 35 days). "Here is everything
your coach remembers, and you decide what stays" is a feature your users
can see.
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.
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.