Skip to main content
By default a memory is scoped to the user_id that ingested it. Groups let you share memory across users: tag a memory to a group at ingest, and anyone who searches that group can see it. The motivating case is collaborative agents — e.g. a travel-planning assistant where each trip is a group. Every traveler’s AI tags trip-relevant facts to the trip’s group, so the whole party shares one evolving picture (hotels, restaurants, dates) while each person’s unrelated personal memories stay private.

The model

  • A group is a registry entry with a name and a prompt. The prompt describes what belongs in the group — it’s what the ingest classifier reads to decide which extracted memories to tag.
  • Group ids are server-generated, unguessable handles (grp_…). Knowing the id is the access boundary — your client decides which users belong to which groups and which ids to send; the memory service doesn’t track membership.
  • Tagging is additive: tagging a memory to a group never removes it from the author’s own scope.

1. Register a group

const trip = await client.groups.create({
  name: 'Tokyo trip 2026',
  prompt:
    'Facts about the Tokyo trip in May 2026: flights, hotels, restaurants, ' +
    'reservations, and dietary needs for this trip.',
});

console.log(trip.id); // "grp_4f14…"
Write the prompt the way you’d brief the classifier: concrete about what to include. A sharp prompt (“facts about the Tokyo trip”) tags precisely; a vague one over- or under-tags.

2. Tag memories at ingest

Pass the group ids in group_ids. At extraction time the classifier reads each group’s prompt and tags the extracted memories that belong to it.
await client.memories.ingest({
  messages: [
    { role: 'user', content: "When I'm in Tokyo I always stay near Shibuya station." },
    { role: 'assistant', content: 'Noted.' },
  ],
  user_id: 'alice',
  conv_id: 'conv_2026_05_16',
  group_ids: [trip.id],
});
  • Each extracted memory is tagged with the subset of group_ids it belongs to — several, one, or none. (“Stays near Shibuya” → tagged to the Tokyo trip; “I’m vegetarian” → likely left untagged, as a general preference.)
  • Unknown or archived ids are soft-skipped (never fail the ingest) and returned in result.ignored_group_ids.
  • Up to 20 group ids per ingest; more returns 422.

3. Read shared memories back

The whole group, across every member — omit user_id, pass group_ids:
const shared = await client.memories.search({
  query: 'where is everyone staying?',
  group_ids: [trip.id],
});
A user’s own memories plus the group’s shared memories, in one ready-to-inject prompt — use recall:
const { prompt } = await client.memories.recall({
  query: 'what should we plan for dinner?',
  pools: [
    { user_id: 'alice' },     // her dietary prefs
    { group_ids: [trip.id] }, // the trip's shared restaurant picks
  ],
});
recall sections the prompt by Personal + the group’s name and attributes each shared line to its author (you: for the caller, <user_id>: for fellow members). See Searching memories for the full picture, including how AND-scoping makes { user_id, group_ids } on a plain search an intersection rather than a union.
Group membership matching is any-of: pass group_ids: [tripA, tripB] to search across both trips at once (a user can belong to many groups).

Managing groups

await client.groups.list();                          // all groups (active + archived)
await client.groups.get(trip.id);
await client.groups.update(trip.id, { prompt: '…' }); // re-prompt the classifier
await client.groups.archive(trip.id);                // soft-archive
Archiving is soft (status becomes "archived"): the group stays readable, but it’s dropped from future ingest tagging — a stale id passed on ingest just lands in ignored_group_ids. Re-activate by update(id, { status: 'active' }).

Best practices

Model a group around a real shared boundary. A group should map to something multiple users genuinely share — a trip, a project, a workspace channel — not a single user (that’s what user_id is for) and not a throwaway topic. If only one person will ever read it, it doesn’t need to be a group. Write the prompt like a brief to the classifier. It’s the only signal used to decide what gets tagged. Name the subject so unrelated facts don’t leak in:
Prompt
✅ Sharp”Facts about the Tokyo trip in May 2026: flights, hotels, restaurants, reservations, and dietary needs for this trip.”
❌ Vague”Travel stuff.” — over- or under-tags
Send only the groups the user is actually in. Your app owns membership; pass the group_ids relevant to this conversation, not every group in the org. A tighter list means sharper tagging and less cross-trip noise. (Max 20 per ingest.) Keep personal personal. Untagged memories stay private to the user — only tagged ones cross to the group. Scope the group prompt to genuinely shared concerns so a user’s general preferences don’t get pulled into a group they don’t belong in. Pick the right read for the job:
  • An agent serving one user inside a grouprecall({ pools: [{ user_id }, { group_ids }] }) — their own context plus the group’s, deduped and attributed.
  • A whole-group overview (“what does the trip know?”) → search({ group_ids }), omit user_id.
  • Don’t expect search({ user_id, group_ids }) to union — it’s an intersection (see Searching memories).
Treat tagging as best-effort. Whether a fact lands in a group is an LLM relevance call, not a guarantee — don’t build logic that requires a specific fact to be tagged. Always read result.ignored_group_ids to catch unknown or archived ids. Group ids are the access boundary. They’re unguessable (grp_…) and the service doesn’t track membership — your app decides who can read a group. Don’t expose a group id to anyone who shouldn’t see the group, and don’t tag anything you wouldn’t want every member to read. Archive groups when they’re done. A finished trip shouldn’t keep collecting tags; archive it to stop new tagging while keeping its history readable.

See also