When semantic search isn’t enough
Semantic retrieval answers “what’s related to this?” You give it a query, it returns records close in meaning. That’s the right tool for “what has this customer told us about their architecture” — you want proximity, not exactness.
It’s the wrong tool for “the high-priority billing records for this customer” or “events from the last seven days.” Those aren’t proximity questions; they’re predicates. Within a single namespace, a broad semantic search returns everything close in meaning and gives you no way to say only these. That’s what metadata filtering is for. AWS’s framing is apt: namespaces isolate by primary entity (user, tenant), and metadata filtering narrows within a namespace by attribute — priority, category, department, time range. It’s the WHERE clause for agent memory.
I set out to prove the WHERE clause works. It does — with a prerequisite that isn’t optional and isn’t loud about itself.
The setup: records that differ only by metadata
I wanted records that semantic search couldn’t cleanly separate, so the metadata predicate would be doing the real work. Five support memories under one actor namespace, differing mostly in their tags:
| Text | category | priority | amount |
|---|---|---|---|
| Disputed duplicate charge; refund issued | billing | high | 4200 |
| How to read the monthly billing statement | billing | low | — |
| Production API 500s during deploy; rolled back | reliability | high | — |
| Feature request: export usage as CSV | product | low | — |
| Billing overage of $1,300 flagged for review | billing | high | 1300 |
Rather than wait for extraction — the slow path where an LLM reads events and pulls records — I wrote the records directly with batch_create_memory_records. That API takes custom content and a metadata map with typed values (stringValue, numberValue, stringListValue, dateTimeValue). It’s the deterministic path: no extraction lag, records I fully control.
The write returned clean: 5 records written, 0 failed.
The gate: success that isn’t
Then I listed the records back and inspected what metadata actually survived:
stored metadata keys observed:
['x-amz-agentcore-memory-createdAt',
'x-amz-agentcore-memory-recordType',
'x-amz-agentcore-memory-updatedAt']
custom keys (category/priority/amount) present on records? False
My category, priority, and amount were gone. The batch API accepted them, reported success, and dropped them on the floor. Only system-generated keys remained.
And filtering on a custom key doesn’t fail soft — it fails hard:
[custom filter] category = billing:
ERROR: ValidationException — Filter key 'category' is not a valid filter key
Not “zero results.” A 400. The service refuses to filter on a key it doesn’t know about.
This is the prerequisite the tutorials gloss over. Metadata filtering is gated on the memory having those keys declared as indexed metadata. For an extraction strategy, that’s the strategy’s memoryRecordSchema.metadataSchema — it tells the LLM which keys to populate. The AWS docs are explicit: “only keys defined in the strategy’s metadataSchema are populated on extracted records — event metadata keys not in the schema are ignored.” The same gate applies to direct batch writes: an undeclared key is not stored and not filterable. The memory I was writing to had no metadata schema and no indexed custom keys, so the custom tags had nowhere to live and nothing to filter against.
The working path: system-indexed keys
To confirm the mechanism is sound and it was only the index declaration missing, I filtered on the keys that are indexed by default — the system-generated ones:
[system filter] recordType = BASE: 5 record(s)
[system filter] createdAt AFTER 2026-07-01: 5 record(s)
Both work. An exact string match (EQUALS_TO on recordType) and a date-range predicate (AFTER on createdAt) both return correctly, no errors. So metadataFilters is real: the operators fire, the date type works, and the compound call is accepted. The feature isn’t broken — it’s conditional. Give it an indexed key and it does exactly what the WHERE-clause framing promises. Give it an undeclared key and it rejects the request.
I also ran the same query through retrieve_memory_records (semantic) with and without a metadata filter — "customer billing problem" alone, then the same query plus recordType = BASE. Both returned the set without error, confirming the metadata filter composes with semantic search in one call. (On five records the counts don’t diverge; the point being verified here is that semantic search accepts an indexed-key predicate as a pre-filter, which it does.)
One retrieval-shape footnote that cost real time
A smaller trap worth recording: list_memory_records with the namespace parameter (prefix match) returned zero for my batch-written records, while the same call with namespacePath (hierarchical match) returned all five. The records were there the whole time; the prefix-vs-path distinction decided whether I saw them. This is the same class of mistake — query the wrong shape and a working feature looks empty. When batch-written records seem to vanish, try namespacePath before concluding the write failed.
When to use each strategy
The decision table for retrieval still holds — with the gate made explicit:
| Use case | Approach | Prerequisite |
|---|---|---|
| ”What has this user said before?” | SEMANTIC retrieval | none beyond the strategy |
| ”How does this user prefer to be addressed?” | USER_PREFERENCE | none beyond the strategy |
| ”Only high-priority billing records this week” | metadata filter | custom keys declared as indexed metadata |
| ”Filter by when it happened / record type” | metadata filter on system keys | none — indexed by default |
| ”What did the agent learn on similar tickets?” | EPISODIC | episode must be closed |
So what
Metadata filtering on AgentCore Memory is a genuine capability, not a stub: string equality and date-range operators filter correctly, and the filter composes with semantic search in a single call. That’s the WHERE clause the pitch promises.
But the capability is gated, and the gate is quiet. Custom metadata keys must be declared as indexed metadata on the memory — via a strategy’s metadataSchema for extracted records, or the equivalent index configuration for direct writes — before they’ll be stored or filtered. Skip that step and batch_create_memory_records still returns success while silently dropping your tags, and the first filter call fails with ValidationException: not a valid filter key.
The honest takeaway for anyone reaching for this: design your indexed metadata keys up front, the same way you’d design a database index, because an un-indexed tag on AgentCore Memory isn’t a slow query — it’s no query at all. The mechanism works the moment the index exists; it does nothing, loudly and then silently, until it does.
Sources
- “Structured metadata for long-term memories” — AgentCore docs on metadata schema, per-strategy configuration, value types, and indexed key declaration
- “batch_create_memory_records” — boto3 reference for direct record creation with metadata maps and typed values
- “list_memory_records / retrieve_memory_records” — boto3 reference for retrieval with metadataFilters, operators, and namespace scoping