Guides / Build well
Designing workspaces, peers and sessions
Before you send the first message, decide how your product maps onto Recall. That choice decides whose memories stay apart, who learns about whom, and what you can delete in one call later. This guide covers each concept in depth, shows two agents with different views of the same person, and ends with layouts for common products.
The concepts
| Concept | What it is | Rule of thumb |
|---|---|---|
| Workspace | An isolated memory space. Nothing is read or learned across workspaces. | One per app and environment; one per business customer if you sell to businesses; one per save in a game. |
| Peer | Anyone who speaks: a person, an agent, a bot. | One per real person or agent, with a stable id from your own database. |
| Session | One coherent conversation. Messages, summaries and prompt context belong to it. | One per chat thread, ticket, call or scene. Not one per message. |
| Membership | Which peers take part in a session, and how each one observes the others there. | Set it when you create the session. |
| Scope | A named group of sessions you want to recall together. | One per account, project or case that spans several conversations. |
| Conclusions and peer cards | What Recall learned: facts about a peer as one observer sees them, and a short profile. | Recall writes them as it learns. You can read them, add facts and delete them. |
Workspaces: where memory stops
A workspace is the hard boundary. Context, search and chat only ever read one workspace, and the same peer id in two workspaces is two unrelated peers. Deleting a workspace removes everything in it.
- Per app and environment.
support-prodandsupport-staging, so test conversations never reach production memory. - Per business customer, if you sell to businesses.
cust-1042holds only that customer's people and conversations. Their backend gets a key that reaches only their workspace (see scoped keys), and when they leave you delete one workspace. - Per save or world, in games. A new game starts with empty memory, and an old save is one delete away. See Game NPCs.
The price of a boundary is that nothing crosses it. If people move between parts of your product and should be remembered across them, those parts belong in one workspace.
Peers: one stable id per person or agent
Memory belongs to the id. Use the key your own database already has for the person, such as user-8412, and never change it. Ids are 1 to 512 characters of letters, digits, _ and -.
- Not a display name or an email, not even an encoded one. People change both, two people can share a name, and an email in the id copies personal data into every URL, log line and export. Put a name in peer
metadataif you need it there at all. - One peer across devices and channels. The same person on the web, in the app and by email is one peer. Record the channel in session or message metadata.
- Agents are peers too. Give each agent or bot its own id (
assistant,agent-billing). When your prompt defines who the agent is, setobserveMe: falseon it: Recall then does not learn about the agent from its own lines, and those lines cost less (see Cost and speed). Leave an agent observed when what it says is worth remembering, such as a research agent's findings in a multi-agent system. - One id, one person, forever. If a closed account's id is later given to someone else, they inherit the old memories unless you deleted the peer. Give each anonymous visitor a fresh id, never a shared
guest.
Sessions and membership
A session is one conversation with a beginning and an end: a chat thread, a support ticket, a phone call, a meeting, a game scene. Recall keeps its messages in order, summarises it as it grows, and builds prompt context from it. What it learns outlives the session: the next conversation with the same person starts with everything learned so far.
Too small and too large both hurt. A session per message leaves your model no recent turns or summary to work with. One endless session per user can work for a single-thread companion, but then you cannot delete one conversation, apply a retention period, or keep a work chat and a private chat apart in context.
Membership is who takes part. Writing a message makes the author a member; you can also add peers when you create the session (peers) or later (addPeers). Each member has two settings for that session: observeMe, whether Recall learns from that member's messages there, and observeOthers, whether that member builds its own view of the others. When a member leaves, their messages stay in the session, and so does what they learned while they were there. A session can have at most 10 members with observeOthers on.
Who learns about whom
Everything Recall learns is a view: what one peer, the observer, knows about another, the observed. There are two kinds.
| View | Built from | Read it with |
|---|---|---|
| Recall's view of Alice | Every message Alice wrote, in any session, while she is observed (the default) | alice.chat(...), or context with peerTarget: alice |
| Bob's view of Alice | Only Alice's messages in sessions where Bob was a member with observeOthers: true, written while he was there | bob.chat(..., { target: alice }), or context with peerTarget: alice, peerPerspective: bob |
Three settings decide what goes into each view:
| Setting | Where | Effect |
|---|---|---|
observeMe (default on) | Peer configuration | Off: nothing is learned from this peer's messages, not by Recall and not by any other peer. The messages are still stored and still appear in context and search. |
observeMe | Membership, per session | Overrides the peer's own setting in that one session. |
observeOthers (default off) | Membership, per session | On: this member builds its own view of the others from what they say in this session. |
In the example a travel app has a planner agent and a billing agent. The user tells the billing agent they moved; the planner is not there. Agents are peers with observeMe: false, because your prompts define them.
import { Recall } from "@recall-memory/sdk";
const recall = new Recall({ workspaceId: "trips-prod" });
// One peer per real person or agent, with the id your own database already uses.
const user = recall.peer("user-8412", { metadata: { plan: "pro" } });
// Your code defines the agents, so Recall need not learn about them from their own lines.
const agent = { configuration: { observeMe: false } };
const planner = recall.peer("agent-planner", agent);
const billing = recall.peer("agent-billing", agent);
from recall_memory import PeerConfig, Recall, SessionPeerConfig
recall = Recall(workspace_id="trips-prod")
# One peer per real person or agent, with the id your own database already uses.
user = recall.peer("user-8412", metadata={"plan": "pro"})
# Your code defines the agents, so Recall need not learn about them from their own lines.
agent = PeerConfig(observe_me=False)
planner = recall.peer("agent-planner", configuration=agent)
billing = recall.peer("agent-billing", configuration=agent)
Each agent observes only its own conversation. The membership is set when the session is created: the agent joins with observeOthers, the user with the defaults.
// One session per conversation. Each agent joins with observeOthers, so it builds its own
// view of the people it talks to, from this conversation only.
const trip = recall.session("chat-5531", {
metadata: { channel: "web" },
peers: { "user-8412": {}, "agent-planner": { observeOthers: true } },
});
await trip.addMessages([
user.message("I live in Utrecht and I am vegetarian."),
planner.message("Noted. I'll only suggest restaurants with good vegetarian menus."),
]);
const invoice = recall.session("chat-5532", {
metadata: { channel: "email" },
peers: { "user-8412": {}, "agent-billing": { observeOthers: true } },
});
await invoice.addMessages([
user.message("I moved to Amersfoort last week, please update my billing address."),
billing.message("Done. Your next invoice goes to your new address in Amersfoort."),
]);
# One session per conversation. Each agent joins with observe_others, so it builds its own
# view of the people it talks to, from this conversation only.
trip = recall.session(
"chat-5531",
metadata={"channel": "web"},
peers=[user, (planner, SessionPeerConfig(observe_others=True))],
)
trip.add_messages(
[
user.message("I live in Utrecht and I am vegetarian."),
planner.message("Noted. I'll only suggest restaurants with good vegetarian menus."),
]
)
invoice = recall.session(
"chat-5532",
metadata={"channel": "email"},
peers=[user, (billing, SessionPeerConfig(observe_others=True))],
)
invoice.add_messages(
[
user.message("I moved to Amersfoort last week, please update my billing address."),
billing.message("Done. Your next invoice goes to your new address in Amersfoort."),
]
)
Two views of one person. Recall's view has the move, because it learns from everything the user says. The planner's view does not: it was never told. In a prompt, pass peerPerspective for the agent that is speaking, so it only knows what it witnessed.
// (waitForIdle is for this demo; real apps rarely wait for learning to finish.)
await recall.waitForIdle();
// Recall's view of the user, built from everything they said in any session: Amersfoort.
const recallView = await user.chat("Where do I live?", { reasoningLevel: "minimal" });
// The planner's view holds only what it witnessed. It never heard about the move: Utrecht.
const plannerView = await planner.chat("Where does the user live?", {
target: user,
reasoningLevel: "minimal",
});
// In a prompt, read the user through the eyes of the agent that is talking.
const context = await trip.context({ peerTarget: user, peerPerspective: planner, tokens: 2000 });
# (wait_for_idle is for this demo; real apps rarely wait for learning to finish.)
recall.wait_for_idle()
# Recall's view of the user, built from everything they said in any session: Amersfoort.
recall_view = user.chat("Where do I live?", reasoning_level="minimal")
# The planner's view holds only what it witnessed. It never heard about the move: Utrecht.
planner_view = planner.chat("Where does the user live?", target=user, reasoning_level="minimal")
# In a prompt, read the user through the eyes of the agent that is talking.
context = trip.context(peer_target=user, peer_perspective=planner, tokens=2000)
Which view to read is a product decision. A single assistant reads Recall's view. Separate agents, characters or departments that should not share what they heard read their own. When an agent should know something it did not witness, write the fact into its view yourself with conclusionsOf(user).create(...), as the Game NPCs guide does with rumours.
Scopes: sessions recalled together
Sometimes the right memory is neither one conversation nor everything a person ever said: one customer account with several contacts, one project with many meetings. A scope groups those sessions. Add sessions to it (recall.scope("acct-1042").addSessions([...])) and pass scope to chat or context: the answer then draws only on those sessions. A scope learns from its sessions like an observer of its own, and catches up on sessions that already had messages in the background. Scopes need a key for the whole workspace. Sales and CRM copilots and Workplace assistants show them in use.
Metadata and filters
Workspaces, peers, sessions and messages carry a metadata object that is yours: the plan, the channel, your ticket number. Recall stores it and lets you filter on it; it does not learn from it. Lists and searches take filters: exact matches on metadata keys, and operators such as gt, lt, in and contains. Search also takes peer_perspective, which limits results to what one peer saw (see Search filters).
// Metadata is yours to filter on.
const emailChats = await (
await recall.sessions({ filters: { metadata: { channel: "email" } } })
).toArray();
// Search only what the billing agent saw: the trip chat is not in the results.
const billingSaw = await recall.search("restaurants", {
filters: { peer_perspective: "agent-billing" },
});
# Metadata is yours to filter on.
email_chats = recall.sessions(filters={"metadata": {"channel": "email"}}).to_list()
# Search only what the billing agent saw: the trip chat is not in the results.
billing_saw = recall.search("restaurants", filters={"peer_perspective": "agent-billing"})
Metadata is returned by every read and included in exports, so keep secrets and anything you would not show in your own admin screens out of it.
Creating and reading
Writes create what they name. Adding a message creates its workspace, session and author when they do not exist yet, and the get-or-create routes (POST /v3/workspaces, PUT …/peers/{peer}, PUT …/sessions/{session}) do the same. That keeps your code short, but a typo in an id quietly starts a new, empty memory.
An SDK handle without options, such as recall.peer("user-8412"), makes no request, so creating one creates nothing. To check whether something exists, use the read-only routes. They return the same object as the get-or-create calls and answer 404 when it does not exist (see Read without creating):
GET /v3/workspaces/{workspace}
GET /v3/workspaces/{workspace}/peers/{peer}
GET /v3/workspaces/{workspace}/sessions/{session}
Better still, validate ids where they enter your system (the signed-in user, a ticket number from your helpdesk) and derive every Recall id from them in one place in your code.
Scoped keys per tenant
An API key can be limited to one workspace, one peer or one session. Create them under API keys in the dashboard, or from your backend with recall.createKey({ workspaceId: "cust-1042" }) (POST /v3/keys).
| Key | Give it to | It can |
|---|---|---|
| Account (no scope) | Your own backend and admin jobs only | Everything, in every workspace |
| Workspace | The service that works for one business customer | Everything in that workspace, including forgetting a person |
| Peer | Code on a user's device, or one agent that only reads | Read the sessions that peer is in and ask from that peer's own point of view. It cannot write messages or delete a peer. |
| Session | A widget for one conversation | Read and write that one session. It cannot delete a peer. |
If users hold peer keys, give sessions ids they cannot guess (a UUID rather than support-8412), or create each session with your workspace key first: a peer key can create a session with itself as a member, and creating a session that already exists adds members. See Browser access.
Layouts for common products
| Product | Workspace | Peers | Session | Also |
|---|---|---|---|---|
| Personal assistant or companion | One per environment, e.g. assistant-prod | Each user; the assistant with observeMe: false | Each conversation thread | Read Recall's view of the user. |
| Workplace assistant, or any product sold to businesses | One per customer, e.g. cust-1042 | Each of their users; your agents | Each chat or meeting | A workspace key per customer; scopes for their projects. |
| Customer support | One per brand and environment | Each customer, by CRM id; the bot; each human agent | Each ticket, e.g. ticket-58213 | A scope per company account when several contacts share one. |
| Game | One per save or server, e.g. save-7f3a | Each player; NPCs with observeMe: false | Each scene | observeOthers for the NPCs in a scene; read with peerPerspective. |
| Multi-agent system | One per deployment | Each agent; each person they serve | Each task or run | observeOthers for agents that keep their own view; a key per agent. |
| Discord or Slack bot | One per community, or one for the bot if members should be known across communities | Each member, by platform user id; the bot with observeMe: false | Each channel or thread | Read Recall's view of the member who is talking. |
| Coding agents | One per developer or team | The developer; the agent, not observed | Each agent session | The integrations set this up for you. |
Pitfalls
- One workspace for all tenants. One customer's people then turn up in another's searches and answers, you cannot give a customer a key that reaches only their data, and removing a customer means hunting down every peer and session.
- A session per message. Context then has no recent turns and no summary to give your model, and you can no longer delete or expire a conversation as a whole.
- A peer per device or channel.
user-8412-iosanduser-8412-webare two people to Recall; the phone never learns what was said on the web. - Reusing ids. A recycled user id, or a shared
guest, hands one person's memories to another. Delete the peer when an account closes. - Names or emails as ids. They change, they collide, and they put personal data into every URL and log line.
- Reading without
peerPerspectivein a multi-agent product. Every agent then knows everything the person told any agent.
Next: Privacy, consent and forgetting · Reasoning levels and models