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 memoryWith Recall
Preparing the second callThe 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 callThe rep pitches features IT has already ruled out."Their last vendor failed the security review on SSO. Start there."
The account moves to another repThe 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 CRMIn RecallWhy
Your sales team, or each customer if you sell a CRMWorkspace, e.g. crm-org-5531One organisation's prospects never mix with another's.
Each contact at a prospectPeer contact-2210, your CRM contact idRecall learns about each person from what they say, across all calls and threads.
Each repPeer rep-104 with observeMe: falseRecall learns about prospects, not about your reps; rep lines are read as context and cost less.
A call, meeting or email threadSession, e.g. call-90211, email-4410A summary per conversation, and transcripts keep their real times.
An account and a dealScopes, e.g. acct-381 and deal-7781Ask across every conversation about the account or the deal. A session can be in several scopes.
Title, stage and other CRM fieldsPeer and scope metadataFor 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" } });

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]);

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 },
);

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 },
  },
);

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 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();

Settings that matter

SettingUse
Message createdAtEvery transcript turn and email: when it was said or sent. Imports without it are dated on the day you import them.
observeMe: false on repsRecall 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 searchRead only one deal's or account's conversations. Scopes need a workspace-level key.
responseFormatA 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.
reasoningLevelmedium for a full brief, low (the default) for quick questions. See Reasoning levels and models.
includeEvidenceReturns 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:

ItemPer 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
Totalabout $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.

Pitfalls

Next: Customer support · Workplace assistants