Guides / Use cases
Sales and CRM copilots
For sales copilots, CRM assistants and call-note tools. Recall remembers what every contact said on calls and in email, keeps each account's and each deal's history together, and turns it into a meeting brief as JSON your UI can lay out.
What memory adds
Without memory, what a prospect said lives in transcripts nobody rereads and in a few CRM fields. With it, the copilot knows the deal:
| Without memory | With Recall | |
|---|---|---|
| Preparing the second call | The rep skims an hour of transcript and their own notes. | A brief: the CFO wants the month-end close under five days by next quarter, IT requires single sign-on, the budget is approved up to 40k; open question: who signs above that. |
| The Head of IT joins the next call | The rep pitches features IT has already ruled out. | "Their last vendor failed the security review on SSO. Start there." |
| The account moves to another rep | The history leaves with the old rep. | The new rep asks "What did they object to?" and gets an answer drawn from every call and email thread. |
Recall does not record or transcribe calls, and it does not write emails. Your recorder and your model do that; Recall stores what was said, learns about each contact, and answers questions about them.
How to model it
| In your CRM | In Recall | Why |
|---|---|---|
| Your sales team, or each customer if you sell a CRM | Workspace, e.g. crm-org-5531 | One organisation's prospects never mix with another's. |
| Each contact at a prospect | Peer contact-2210, your CRM contact id | Recall learns about each person from what they say, across all calls and threads. |
| Each rep | Peer rep-104 with observeMe: false | Recall learns about prospects, not about your reps; rep lines are read as context and cost less. |
| A call, meeting or email thread | Session, e.g. call-90211, email-4410 | A summary per conversation, and transcripts keep their real times. |
| An account and a deal | Scopes, e.g. acct-381 and deal-7781 | Ask across every conversation about the account or the deal. A session can be in several scopes. |
| Title, stage and other CRM fields | Peer and scope metadata | For your own code and UI. Recall learns from what people say, not from metadata. |
Build it
Install the SDK (npm install @recall-memory/sdk or pip install recall-mem) and set RECALL_API_KEY. Create the contacts, the rep and the scopes.
import { Recall } from "@recall-memory/sdk";
const recall = new Recall({ workspaceId: "crm-org-5531" });
// Contacts at the prospect are peers, keyed by your CRM contact ids.
const cfo = recall.peer("contact-2210", { metadata: { title: "CFO" } });
const itLead = recall.peer("contact-2211", { metadata: { title: "Head of IT" } });
// Recall learns about prospects, not about your reps.
const rep = recall.peer("rep-104", { configuration: { observeMe: false } });
// One scope per account and one per deal: every call and thread about them.
const account = recall.scope("acct-381");
const deal = recall.scope("deal-7781", { metadata: { stage: "evaluation" } });
from datetime import datetime, timedelta, timezone
from pydantic import BaseModel
from recall_memory import PeerConfig, Recall
recall = Recall(workspace_id="crm-org-5531")
# Contacts at the prospect are peers, keyed by your CRM contact ids.
cfo = recall.peer("contact-2210", metadata={"title": "CFO"})
it_lead = recall.peer("contact-2211", metadata={"title": "Head of IT"})
# Recall learns about prospects, not about your reps.
rep = recall.peer("rep-104", configuration=PeerConfig(observe_me=False))
# One scope per account and one per deal: every call and thread about them.
account = recall.scope("acct-381")
deal = recall.scope("deal-7781", metadata={"stage": "evaluation"})
Calls: one message per speaker turn, with its time. Most recorders give you a transcript with speakers and timestamps. Add one message per turn, from the person who spoke, with createdAt set to when it was said, so "by next quarter" is reckoned from the call. Put the call in the account's scope when you create it, or add it to a scope later with addSessions.
// A discovery call from your recorder's transcript: one message per turn, each dated with when
// it was said, so "next quarter" is reckoned from the call, not from the upload.
const call = recall.session("call-90211", { scopes: [account] });
const start = Date.parse("2026-10-02T14:00:00Z");
const at = (seconds: number) => new Date(start + seconds * 1000);
await call.addMessages([
rep.message("Thanks for making time. What made you start looking?", { createdAt: at(0) }),
cfo.message("Month-end close takes eight days. I want it under five by next quarter.", {
createdAt: at(9),
}),
itLead.message("Whatever we pick must use our SSO. The last vendor failed our review.", {
createdAt: at(31),
}),
cfo.message("Budget is approved up to 40k a year. Above that it goes to the board.", {
createdAt: at(88),
}),
]);
await deal.addSessions([call]);
# A discovery call from your recorder's transcript: one message per turn, each dated with when
# it was said, so "next quarter" is reckoned from the call, not from the upload.
call = recall.session("call-90211", scopes=[account])
start = datetime(2026, 10, 2, 14, 0, tzinfo=timezone.utc)
def at(seconds: int) -> datetime:
return start + timedelta(seconds=seconds)
call.add_messages(
[
rep.message("Thanks for making time. What made you start looking?", created_at=at(0)),
cfo.message(
"Month-end close takes eight days. I want it under five by next quarter.",
created_at=at(9),
),
it_lead.message(
"Whatever we pick must use our SSO. The last vendor failed our review.",
created_at=at(31),
),
cfo.message(
"Budget is approved up to 40k a year. Above that it goes to the board.",
created_at=at(88),
),
]
)
deal.add_sessions([call])
Email threads and files. An email thread is a session like a call. Files a contact sends you, such as an RFP or a requirements list, go in with uploadFile, from the person who sent them: PDF, text and JSON are understood, and a long file becomes several messages. If your recorder gives you one transcript file for a whole call, split it by speaker first; a file uploaded as one peer makes every line in it, the rep's pitch included, that person's words.
// An email thread is a session too. The requirements the Head of IT attached go in with
// uploadFile, as their words (PDF, text or JSON; long files become several messages).
const thread = recall.session("email-4410", { scopes: [account, deal] });
const sent = "2026-10-05T08:30:00Z";
await thread.addMessages([
itLead.message("Attached are our requirements. SSO and EU hosting are must-haves.", {
createdAt: sent,
}),
]);
const requirements =
"Must: SAML single sign-on.\nMust: data stored in the EU.\nNice: ERP export.";
await thread.uploadFile(
{ filename: "requirements.txt", content: new TextEncoder().encode(requirements) },
itLead,
{ createdAt: sent },
);
# An email thread is a session too. The requirements the Head of IT attached go in with
# upload_file, as their words (PDF, text or JSON; long files become several messages).
thread = recall.session("email-4410", scopes=[account, deal])
sent = "2026-10-05T08:30:00Z"
thread.add_messages(
[
it_lead.message(
"Attached are our requirements. SSO and EU hosting are must-haves.",
created_at=sent,
)
]
)
requirements = b"Must: SAML single sign-on.\nMust: data stored in the EU.\nNice: ERP export."
thread.upload_file(("requirements.txt", requirements, "text/plain"), it_lead, created_at=sent)
Meeting prep: one structured question. Before a meeting, ask across the deal's scope for the brief you want, as JSON. In TypeScript pass a JSON Schema, or { jsonSchema, parse } for a typed result; in Python pass a Pydantic model and get an instance back (a JSON Schema dict returns the JSON as a string). medium is still one retrieval and one model call, at $0.025; we have no measured latency for it, so make briefs ahead of the meeting rather than during it.
// Before the next meeting: one question across the whole deal, answered as JSON your UI lays
// out. (With zod: { jsonSchema: z.toJSONSchema(Brief), parse: (d) => Brief.parse(d) }.)
type MeetingBrief = {
stakeholders: { contact: string; role: string; cares_about: string }[];
objections: string[];
next_steps: string[];
open_questions: string[];
};
const text = { type: "string" };
const list = { type: "array", items: text };
const schema = {
type: "object",
properties: {
stakeholders: {
type: "array",
items: {
type: "object",
properties: { contact: text, role: text, cares_about: text },
required: ["contact", "role", "cares_about"],
},
},
objections: list,
next_steps: list,
open_questions: list,
},
required: ["stakeholders", "objections", "next_steps", "open_questions"],
};
await recall.waitForIdle(); // for this demo; briefs are made later, when learning is done
const brief = await recall.chat(
"Prepare me for the next meeting on this deal: who is involved and what each cares about, " +
"their objections, the next steps we agreed and the questions still open.",
{
scope: deal,
reasoningLevel: "medium",
responseFormat: { jsonSchema: schema, parse: (data) => data as MeetingBrief },
},
);
# Before the next meeting: one question across the whole deal, answered as JSON your UI lays
# out. Pass a Pydantic model (or a JSON Schema dict, which returns the JSON as a string).
class Stakeholder(BaseModel):
contact: str
role: str
cares_about: str
class MeetingBrief(BaseModel):
stakeholders: list[Stakeholder]
objections: list[str]
next_steps: list[str]
open_questions: list[str]
recall.wait_for_idle() # for this demo; briefs are made later, when learning is done
brief = recall.chat(
"Prepare me for the next meeting on this deal: who is involved and what each cares about, "
"their objections, the next steps we agreed and the questions still open.",
scope=deal,
reasoning_level="medium",
response_format=MeetingBrief,
)
Quick questions about one person. Ask the contact's peer. The answer draws on everything they said, in any call or thread, and takes a few seconds at low.
// A quick question about one person, from everything they said in any call or thread.
const cfoNeeds = await cfo.chat("What does this contact need to see before signing?", {
reasoningLevel: "low",
});
# A quick question about one person, from everything they said in any call or thread.
cfo_needs = cfo.chat(
"What does this contact need to see before signing?", reasoning_level="low"
)
A contact who asks to be deleted. delete() erases their words and everything learned about them, in every call, thread and scope. The rep's lines in those calls stay; delete the calls too if they must go.
// A contact asks you to delete their data: erase their words and everything learned about
// them, in every call, thread and scope. Keep the result for your deletion log.
const removed = await itLead.delete();
# A contact asks you to delete their data: erase their words and everything learned about
# them, in every call, thread and scope. Keep the result for your deletion log.
removed = it_lead.delete()
Settings that matter
| Setting | Use |
|---|---|
Message createdAt | Every transcript turn and email: when it was said or sent. Imports without it are dated on the day you import them. |
observeMe: false on reps | Recall reads rep lines as context but learns nothing from them. Give it when you create the handle; the SDKs send it before the rep's first line. |
scope on chat and search | Read only one deal's or account's conversations. Scopes need a workspace-level key. |
responseFormat | A JSON Schema with an object at the root, or a Pydantic model. Keep fields few and plainly named. allOf, not and if/then/else are not supported. |
reasoningLevel | medium for a full brief, low (the default) for quick questions. See Reasoning levels and models. |
includeEvidence | Returns the facts and messages behind an answer, so a rep can check a claim before repeating it to the customer. |
Cost and speed
Measured from Europe against the hosted API (Frankfurt), October 2026: a turn or context call took 0.3 to 0.7 seconds, a search about 0.9 seconds, a question at minimal or low 2 to 4.5 seconds. Learning from a transcript happens in the background, usually within about 20 seconds, and never holds up a call.
Context, search, storage, summaries and dreams are included. You pay for learning, $1.75 per million message tokens with every message counting as at least 50; a rep's line, which Recall reads without learning from it, counts only what it reads, at most 200 tokens as context. One active deal in a month: three 45-minute calls with about 100 turns per side of about 40 tokens each, an email thread with six emails each way of about 250 tokens, three briefs at medium and ten quick questions at low:
| Item | Per month |
|---|---|
| Contacts' words: (300 × 50 + 6 × 250) tokens × $1.75 / 1M | $0.0289 |
| Rep's words, read but not learned from: (300 × 40 + 6 × 200) tokens × $1.75 / 1M | $0.0231 |
Briefs at medium: 3 × $0.025 | $0.0750 |
Questions at low: 10 × $0.005 | $0.0500 |
| Total | about $0.18 per deal |
A rep working ten such deals costs about $1.27 a month. Answers are cached until the memory they depend on changes, but a cached answer is still billed as one query, so keep a brief in your own database once it is made. More in Cost and speed.
Privacy and forgetting
What prospects say on calls is personal data about them, also in business-to-business sales.
- Tell people you record. Say at the start of a call that it is recorded and transcribed and that your team keeps notes from it. Rules on consent to recording differ by country and state; check the ones that apply to you (this is not legal advice). If someone declines, do not add their words.
- Keep it to business. Calls drift into family, health and politics. Recall learns from what is said, so trim small talk from transcripts when you can.
- Objections and deletion. A contact who objects to being profiled but stays in touch: set
observeMe: falseon their peer so Recall stops learning about them, and delete what was learned. A contact who asks to be deleted:contact.delete(), orDELETE /v3/workspaces/{workspace}/peers/{peer}. See Forget a person. - Retention. Delete the calls and threads of lost deals after the period your policy sets; deleting a session removes its messages and what was learned from it.
- Where data lives: the database and API are in Frankfurt (EU). Text is processed by AI model providers in the US through OpenRouter, restricted to providers that do not collect the data, and customer data is not used to train models. See the privacy policy, the DPA and Privacy, consent and forgetting.
Pitfalls
- One transcript file as one peer. Every line in it, the rep's included, becomes that person's words. Split transcripts by speaker.
- Call notes in the rep's name. Recall does not learn from peers it does not observe, so a rep's notes are stored and searchable, but nothing in them is learned. Write facts you have confirmed as conclusions about the contact.
- No
createdAton imported calls. "Next quarter" is then reckoned from the day you imported them. - One session for the whole deal. Summaries blur and calls run together. Use a session per call or thread and a scope for the deal.
- Contact ids that change when your CRM merges duplicates. Memory belongs to the id. Decide which id survives before you write, or what you know about the person stays split.
- Treating the brief as a record. It is a model's reading of the conversations. Check figures with
includeEvidencebefore you quote them to the customer.
Next: Customer support · Workplace assistants