Skip to main content
Say you’re building a support bot. Every conversation you save carries a bit of metadata — who it was about, which order:
(session_id is special: it groups pushes into one source. It’s not a filterable key — the ones you declare are.) Now Sara messages again. You want her history — not Tom’s. Two steps.

Step 1: declare the key (once)

This tells xysq “the email field matters — index it.” Everything already in the vault is indexed in the background (seconds for typical vaults), and every future push indexes on arrival. Where keys can live: the registry is an agent/team-vault feature. Agent vaults declare via the SDK (above) or the vault’s Settings; team vaults via Settings → Filterable metadata in the app. The personal vault has no registry — metadata still rides on personal pushes (like session_id), it just isn’t filterable there. Declare exact-value keys you’ll filter by: email, customer_id, ticket, order_id, repo. Don’t declare topical labels (topic: infra) — grouping by theme is what tags are for. Up to 16 keys per vault; values must be text/numbers/booleans (or arrays of them); reserved keys like session_id are refused.
Call this rule match-or-absent: a source without the key is never excluded. The policy note isn’t about Sara, but it isn’t about anyone else either — so it stays available when answering her. Only data that contradicts the filter is dropped. (Case doesn’t matter; lists work on both sides; multiple keys combine.)

Adding metadata later

Metadata usually rides in at push time — but you can attach or fix it on an existing source too:
Only declared keys are writable this way (unknown keys are echoed back, not written), and the filter index updates immediately. This is also the path for file uploads, which carry no metadata at upload time.

Filtering examples

One person’s context (their data + everything unlabeled, never anyone else’s):
Two keys at once (both must not be contradicted):
Either of two values for one key:

Tags or metadata keys?

One question decides it: should a source that says nothing about this still show up? Yes → metadata key (forgiving, match-or-absent). No, only my curated set → tag (hard scope).

Scoped pull_context

MCP agents get the same filter on pull_context — for team scopes, since that’s where MCP-reachable declared keys live (teams declare them in the team’s Settings; the personal vault has no registry):
Worth knowing on either surface:
  • Single vault. Filters apply to one vault per call (a named team on MCP, the addressed vault on the SDK); shared context isn’t searched while a metadata filter is active — the coverage_note says so.
  • Typos fail loud. Filtering on an undeclared key is a 400 on the SDK (listing the declared keys); on MCP it’s items: [] with the reason in coverage_note — read it before concluding “no memory”.
  • Combines with tags: filters={"tags": ["launch"], "meta": {"email": ...}} = your curated set, minus anyone else’s data.

Best practices

  • Canonicalize values before pushing. Matching is exact (trimmed, case-insensitive) — ORD-123 never matches 123.
  • Stamp the key consistently if exclusion matters: keyless sources always stay in scope, which is a feature for shared notes and a foot-gun for data you forgot to label. update_source_meta fixes stragglers.
  • Put metadata on the first push of a session. Later pushes in the same session_id extend the content, not the metadata.
  • Use arrays for multi-party sources"email": ["a@…", "b@…"] matches either.
  • Deleting a key is a light switch, not a shredder. Removing a declared key only drops the index; the metadata stays on every source, and re-declaring rebuilds it identically.
  • Need only-matching results, keyless excluded too? That’s not a metadata filter — that’s a curated set. Use a tag.
  • Storing names or emails in the content itself, not just metadata? See PII scrubbing to strip that at ingest.