Korely

A sales agent that connects the dots

You mention "Acme Corp" to your sales assistant. A stateless agent answers from whatever thread you happen to be in. It doesn't know that Marco from Acme is the champion you met in March, that the renewal slipped to Q3, or that you promised their CTO a security review, those live in three different meeting notes it never connected.

With Korely in the loop, one mention of Acme Corp surfaces the champion, the renewal target and the budget stated with it, pulled from all your notes, not the open thread. That works because memories become typed (subject, predicate, object) facts (who, which relation, what): Marco Rossi is_champion Acme Corp, Acme Corp targets_renewal_in Q3, Acme Corp budget_approved approved. Ask for Acme and you get the facts that name it; the deal is one step further, through Marco, who owns it. A champion is the person at the customer who pushes your deal internally. (The predicate names are written by the extractor, a model, so yours will differ.) This cookbook walks the pattern at the call level. Terms such as entity, fact and as_of are in the Glossary.

The snippets use the Python SDK (pip install korely-memory). The same calls work over the REST API and the Node SDK.

The scoping: one rep, every account

A sales rep's book is one user_id, the end user the data is about. Every account, contact, and deal lives in that namespace, so a fact written from the Acme call and a fact written from the Globex call both land where a single get_facts can connect them. Always pass user_id: a read without it covers every end user of the project.

from korely_memory import Korely
korely = Korely(api_key="kor_live_...", region="eu")
REP = "rep-jordan" # the sales rep this book of business belongs to

1. After a call: drop the notes in

When a meeting ends, hand Korely the notes. Fact extraction runs server-side and turns the prose into typed edges that connect the people, the company, and the deal, no schema to define, no triples to hand-author.

korely.add(
"Discovery call with Acme Corp. Marco Rossi (VP Eng) is the champion and "
"owns the Enterprise renewal, now targeting Q3. Their CTO asked us to run a "
"security review before they sign. Budget is approved.",
user_id=REP,
run_id="call-acme-2026-06-12",
metadata={"account": "Acme Corp", "stage": "discovery"},
)
{
"id": "mem_8d21",
"content": "Discovery call with Acme Corp. Marco Rossi (VP Eng) is the champion...",
"user_id": "rep-jordan",
"agent_id": null,
"run_id": "call-acme-2026-06-12",
"metadata": {"account": "Acme Corp", "stage": "discovery"},
"client": "korely-python/0.1.28",
"kind": "note",
"status": "processing",
"created_at": "2026-06-12T15:04:22+00:00",
"updated_at": null,
"facts": []
}

Note facts: [] in that response, extraction is asynchronous, so the typed triples land usually within about ten seconds after the write returns. The next section reads them once they're in.

2. Mention an account: pull everything tied to it

This is the move. get_facts(entity="Acme Corp") returns every fact where Acme appears on either side of the triple, as a subject (Acme Corp targets_renewal_in Q3) or as an object. The opposite endpoint of each fact is a connected entity: the champion, the renewal target, the budget. One call, one account, its direct neighborhood. The four lines in the code comments are the facts a real run returned for the note in step 1.

facts = korely.get_facts(entity="Acme Corp", user_id=REP)
for f in facts:
print(f.subject, f.predicate, f.object)
# rep-jordan had_call_with Acme Corp
# Marco Rossi is_champion Acme Corp
# Acme Corp targets_renewal_in Q3
# Acme Corp budget_approved approved

entity matches the exact name (ignoring case), or a known alias, on either side of a fact. It is not a substring or fuzzy match, so "Acme" does not find "Acme Corp".

{
"facts": [
{
"id": "fct_a1",
"subject": "Marco Rossi",
"predicate": "is_champion",
"predicate_raw": "is_champion",
"object": "Acme Corp",
"subject_type": "person",
"object_is_literal": false,
"user_id": "rep-jordan",
"agent_id": null,
"invalid_at": null,
"invalidated_by": null,
"source_memory_id": "mem_8d21"
},
{
"id": "fct_a2",
"subject": "Acme Corp",
"predicate": "targets_renewal_in",
"predicate_raw": "targets_renewal_in",
"object": "Q3",
"subject_type": "organization",
"object_is_literal": true,
"user_id": "rep-jordan",
"agent_id": null,
"invalid_at": null,
"invalidated_by": null,
"source_memory_id": "mem_8d21"
}
],
"total": 4
}

The list above is shortened to two of the four facts, and each fact to its main fields. The request from the CTO is not in it. The extractor stored it under a different name, "Acme Corp CTO" (requested_security_review), and entity matches names exactly. This is the limit of a lookup by name: when one company appears under two names, tell Korely they are the same with the declare-name call, or read the note itself with search or get_context.

Because entity matches the subject or object side, the champion surfaces from the same query as the renewal target and the budget. Nothing in the agent's open thread had to mention Marco for him to come back when you asked about his company. The renewal Marco owns is one hop further, in step 4.

In the chat

You mention "Acme Corp"

  • No special prompt
  • Resolves to user_id "rep-jordan"

Korely

get_facts(entity="Acme Corp")

  • Every triple where Acme is subject OR object
  • Across every note, not one thread

Surfaced

The whole neighborhood

  • Marco (champion), the approved budget
  • The Q3 renewal target

3. Write an explicit edge yourself

Sometimes you already know the connection and want it recorded exactly, not inferred from prose. add_fact_triple writes the edge directly, it skips extraction but still runs the server-side contradiction check (a model call, so it can fail for a moment with 503 model_unavailable) and counts as one write against your plan. A new edge that conflicts with an old one can supersede it.

fact = korely.add_fact_triple(
"Marco Rossi", "works_at", "Acme Corp",
user_id=REP,
subject_type="person",
confidence=0.97,
)
# fact.invalidated -> ids of any earlier conflicting edge this one replaced
{
"id": "fct_b1",
"subject": "Marco Rossi",
"predicate": "works_at",
"object": "Acme Corp",
"predicate_family": "work",
"valid_from": "2026-06-12T15:09:40+00:00",
"invalidated": [],
"tense": "current",
"invalid_at": null,
"observation_count": 1,
"user_id": "rep-jordan",
"agent_id": null
}

The note in step 1 never states where Marco works, so this write creates a new fact: a fresh id, a valid_from equal to the write time, and observation_count 1.

If Marco later moves to Globex, write Marco Rossi works_at Globex and the contradiction detector can return the superseded edge's id in invalidated, and the old employment fact gets an invalid_at timestamp and stops being served. The history stays intact. The check is a model's judgement, so read with include_invalidated=True when it matters.

4. Pivot to a person

The same one-hop lookup walks the other way. Ask for a person and you get every fact they touch, what they own, who they champion. To follow the graph one more step, call get_facts again on a neighbor you found.

marco = korely.get_facts(entity="Marco Rossi", user_id=REP)
# Marco Rossi owns Enterprise renewal
# Marco Rossi is_champion Acme Corp
# walk one hop further: from the deal Marco owns
deal = korely.get_facts(entity="Enterprise renewal", user_id=REP)
# only the edges that name "Enterprise renewal": in this run, the owns edge above.
# "Q3" hangs on "Acme Corp", so the hop through the deal name does not find it.

Each hop is a lookup in the facts table with no generative model on the read path. You're traversing typed edges one at a time, choosing where to go next from what came back.

5. Point in time: who was on the account back then?

Because facts have start and end dates, you can ask the graph as it stood on any date. Useful for a deal post-mortem, who the champion was before a contact moved on, what the renewal target was last quarter.

# the account's connections as they stood on 30 June, before Marco moved to Globex (step 3)
# Note: a fact starts on the day its note was written. If you wrote the notes today,
# a date in June finds nothing: use a date after your own writes, or pass timestamp=
# to add() with the day the call really happened.
before = korely.get_facts(entity="Acme Corp", user_id=REP, as_of="2026-06-30")
# include superseded edges to see the full history, each with its invalid_at
history = korely.get_facts(entity="Acme Corp", user_id=REP, include_invalidated=True)

as_of="2026-06-30" returns the edges that were valid on that date, so Marco's Acme edge is still there even if he has since moved; include_invalidated=True adds the edges that have since been superseded, each with its invalid_at, so you can see how the account map changed.

Why this beats a flat note store

  • One query, the whole account. A vector store returns the notes that mention "Acme" by similarity. get_facts(entity="Acme Corp") returns every typed edge Acme touches, across every note it was stated in, not just the notes that rank closest to the word "Acme".
  • Connections are first-class. Memories become (subject, predicate, object) facts, so "who champions Acme" and "when Acme plans to renew" are the same lookup from different sides of the triple, not a re-read of every transcript and a hope the model joins them.
  • On every plan. The entity view is part of every Korely plan, including the free one.

One honest caveat: get_facts(entity=...) matches the subject or object side of a triple, so connections surface through shared typed facts, it is a one-hop lookup over the facts table, not a server-side multi-hop graph-traversal API (there is no get_related on the agents SDK; you walk further by calling get_facts again on a neighbor). And korely.add extracts those facts asynchronously, so the typed edges from a fresh note land a few seconds after the write returns, ready at the next get_facts, not inside the same call. When you need an edge recorded instantly and exactly, write it with add_fact_triple.

Where to go next

Temporal facts explains supersession, as_of, and the contradiction check under the hood; get context assembles a prompt-ready block when you'd rather hand the model the account state than raw facts; and the multi-session research cookbook applies the same typed-fact pattern to a long-running research thread.