Korely

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.

korely sdk python
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):

Factvalid_frominvalid_atStatus
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 recap
facts = korely.get_facts(
user_id="coachee-anna",
include_invalidated=True,
)
# Secondary: a few raw memory snippets for colour and context
hits = 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 Anna
korely.get_facts(user_id="coachee-anna", agent_id="exec-coach")
# Just the fitness program
korely.get_facts(user_id="coachee-anna", agent_id="fitness-coach")
# The whole person, across programs
korely.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