Artificial Intelligence

Organizing Agents’ memory at scale: Namespace design patterns in AgentCore Memory

September 2026: This post was reviewed and updated for accuracy.

AI agents that remember context across sessions need memory that is organized, retrievable, and secure. In AgentCore memory, a capability of Amazon Bedrock AgentCore, namespaces determine how long-term memories are organized, how they’re retrieved, and who can access them. Get the namespace design wrong and irrelevant context surfaces in retrieval, or worse, one user’s memories leak into another’s conversation.

In this post, you’ll learn how to design namespace hierarchies for each memory strategy, choose between exact and hierarchical retrieval, extend hierarchies with custom namespace variables for multi-tenant applications, and enforce namespace boundaries with AWS Identity and Access Management (IAM). If you’re new to AgentCore memory, start with our introductory post: Amazon Bedrock AgentCore memory: Building context-aware agents.

What are namespaces?

Namespaces are hierarchical paths that organize long-term memory records within an AgentCore memory resource. They work like directory paths in a file system. When AgentCore memory extracts long-term memories from your conversations, each record is stored under a namespace such as /actor/customer-123/preferences/ or /actor/customer-123/session/session-789/summary/.

If you’ve worked with partition keys in Amazon DynamoDB or prefix layouts in Amazon Simple Storage Service (Amazon S3), the mental model transfers well. You think through access patterns before choosing a partition key, and you should think through retrieval patterns before designing a namespace. Decide who needs to access these memories, at what level of detail, and which isolation boundaries must never be crossed. The main difference from a partition key is that namespaces support hierarchical retrieval. You can query at any level of the hierarchy, so the same memory resource can serve memories scoped to a single session, a single user across sessions, or a broader grouping.

Namespace templates and resolution

When creating a memory resource, you define a namespaceTemplate within each strategy configuration. Templates support three built-in variables: {actorId}, {sessionId}, and {memoryStrategyId}. Memory resources are managed through the bedrock-agentcore-control client. Events and retrieval, which appear later in this post, go through the bedrock-agentcore data plane client.

control_client = boto3.client("bedrock-agentcore-control")

response = control_client.create_memory(
    name="CustomerSupportMemory",
    description="Memory for customer support agents",
    eventExpiryDuration=30,
    memoryStrategies=[
        {
            "semanticMemoryStrategy": {
                "name": "customer-facts",
                "namespaceTemplates": ["/actor/{actorId}/facts/"]
            }
        },
        {
            "summaryMemoryStrategy": {
                "name": "session-summaries",
                "namespaceTemplates": ["/actor/{actorId}/session/{sessionId}/summary/"]
            }
        }
    ]
)

When events arrive for actorId=customer-456 in sessionId=session-789, the resolved namespaces become /actor/customer-456/facts/ and /actor/customer-456/session/session-789/summary/.

All three built-in variables describe the conversation itself: who is talking, in which session, processed by which strategy. If you also need to isolate on dimensions such as tenant or team, you can extend templates with custom variables, covered later in this post.

Namespace design per memory strategy

Each memory strategy has different scoping needs, and the namespace should reflect how that data will be accessed.

Semantic and User Preferences: Actor-scoped

Semantic memory captures facts from conversations (for example, “The customer’s company has 500 employees”). UserPreference memory captures choices and styles (for example, “User prefers Python”). Both accumulate over time and are relevant across sessions. A fact learned in January should still be retrievable in March. Scope these to the actor:

/actor/{actorId}/facts/
/actor/{actorId}/preferences/

A user’s facts and preferences are then consolidated under one namespace regardless of which session produced them. The consolidation engine merges related memories within the same namespace, so the namespace boundary is also the consolidation boundary, as Figure 1 shows.

Diagram showing that a namespace boundary is also the consolidation boundary for a user’s memory records

Figure 1: A namespace boundary is also the consolidation boundary for a user’s memory records

If your application needs to retrieve across actors, for instance a support agent looking up issues reported by other customers, place the actor ID beneath the memory type instead:

/facts/{actorId}/
/preferences/{actorId}/

Querying the /facts/ subtree then spans all actors, while the exact namespace /facts/customer-123/ still returns one actor’s facts. The trade-off is that no single subtree means “everything we know about customer-123,” and per-user IAM policies need one statement per memory type. Use this layout only when cross-actor retrieval is a primary access pattern.

Summary: Session-scoped

Summary memory creates a running narrative of a conversation, so you can retrieve a compact summary instead of feeding the full history into the model’s context window. Since a summary belongs to one conversation, include the session ID:

/actor/{actorId}/session/{sessionId}/summary/

Each session gets its own summary, organized under the actor so you can still retrieve summaries across sessions. Putting the two strategies together, a memory resource is organized like this:

Memory Resource: CustomerSupportMemory
│
├── /actor/customer-123/
│   ├── facts/
│   │   ├── "Company has 500 employees across Seattle, Austin, Boston"
│   │   └── "Currently migrating from on-premises to cloud"
│   ├── preferences/
│   │   └── "Prefers email communication over phone"
│   ├── session/session-001/summary/
│   │   └── "Inquired about enterprise pricing, requested follow-up demo"
│   └── session/session-002/summary/
│       └── "Confirmed Q3 timeline, discussed CRM integration"
│
└── /actor/customer-456/
    ├── facts/
    │   └── "Startup with 20 employees, serverless architecture"
    └── preferences/
        └── "Prefers concise, high-level summaries"

Episodic: Session-scoped with reflection hierarchy

Episodic memory captures complete reasoning traces: the goal, steps taken, outcomes, and reflections. An episode records what happened in one interaction, such as how a flight booking agent handled a fare class restriction and rebooked the customer, so episodes are scoped to the session. Reflections are cross-episode insights, for instance “when a fare class restriction blocks a modification, search for alternative flights rather than only explaining the policy.” The reflections namespace must be a prefix of the episodes namespace: the same path with one or more trailing levels removed.

Episodes:    /actor/{actorId}/session/{sessionId}/episodes/
Reflections: /actor/{actorId}/

Here reflections are generated across all of an actor’s episodes. Cut the reflections namespace back to / and they span every actor, which means insights derived from one customer’s interactions become retrievable in every other customer’s context. Do that only when episodes contain nothing customer-specific.

Retrieving memories

Use RetrieveMemoryRecords when you need memories that are semantically relevant to a query. This is the primary retrieval method during agent interactions.

agentcore_client = boto3.client("bedrock-agentcore")

memories = agentcore_client.retrieve_memory_records(
    memoryId="CustomerSupportMemory-8f3k2j9d1a",
    namespace="/actor/customer-123/facts/",
    searchCriteria={
        "searchQuery": "What cloud migration approach is the customer using?",
        "topK": 5
    }
)

The search query can be the user’s question passed as-is when it maps naturally to stored memories (“What’s my budget?”). For vaguer input such as “Help me plan my next trip,” have the agent’s large language model (LLM) formulate a targeted query like “travel preferences, destination history, budget constraints.” That adds a model call, and therefore latency, before retrieval.

Use ListMemoryRecords when you need to enumerate everything in a namespace, such as showing a user their stored preferences or auditing what exists. GetMemoryRecord and DeleteMemoryRecord operate on a single record ID and support memory management workflows where users view, correct, or delete specific memories.

namespace compared to namespacePath

Both RetrieveMemoryRecords and ListMemoryRecords accept two scoping fields that look interchangeable and behave very differently.

Diagram comparing the exact-match namespace field with the hierarchical namespacePath field

Figure 2: The namespace field matches one exact path, while namespacePath returns an entire subtree

The namespace field performs an exact match and returns only records stored at that precise path. The preceding example returns records from /actor/customer-123/facts/ and nothing else. Use it when you need precise scoping, such as a user’s preferences without their facts or summaries.

The namespacePath field performs a hierarchical match, returning every record whose namespace falls under the path:

# Returns records from facts/, preferences/, and session/*/summary/
records = agentcore_client.retrieve_memory_records(
    memoryId="CustomerSupportMemory-8f3k2j9d1a",
    namespacePath="/actor/customer-123/",
    searchCriteria={"searchQuery": "cloud migration", "topK": 5}
)

Use it for “show me everything we know about this customer” features. Because it returns everything beneath the path, the level you start from is the level you’re trusting the caller with. Always include leading and trailing slashes in templates and in namespacePath values. Without the trailing slash, /actor/customer-1 also matches /actor/customer-12/ and returns other users’ records.

Scenario API Field Example
1 Retrieve semantically relevant user preferences RetrieveMemoryRecords namespace /actor/customer-123/preferences/
2 Retrieve a specific session summary ListMemoryRecords namespace /actor/customer-123/session/session-001/summary/
3 List all preferences for a user ListMemoryRecords namespace /actor/customer-123/preferences/
4 Search across all of a user’s memories RetrieveMemoryRecords namespacePath /actor/customer-123/
5 List summaries across sessions for a user ListMemoryRecords namespacePath /actor/customer-123/session/

Custom namespace variables for multi-tenant hierarchies

Everything so far isolates on the actor and the session, which is what a consumer-facing AI assistant needs. Multi-tenant and enterprise applications usually have at least one more boundary.

Consider an independent software vendor, AnyCompany DevTools, that sells an engineering support agent to large enterprises. Each customer is a tenant. Inside a tenant, the agent supports many services, each with components and teams whose engineers open sessions with the agent. A memory about a connection pool deadlock in Octank’s payments ledger must not surface for a different tenant, and probably shouldn’t surface for an unrelated team inside Octank either.

None of the built-in variables expresses “tenant” or “team.” The two workarounds don’t hold up. Hard-coding the tenant into the template (/tenant/octank/actor/{actorId}/facts/) means a new strategy or memory resource for every customer. Packing a composite key such as octank:payments:dev-123 into actorId breaks cross-session consolidation for the engineer and misaligns the IAM policies that scope on user identity.

Custom namespace variables close this gap. You declare variable names on the memory resource with namespaceKeys, reference them in templates alongside the built-in variables, and supply values per event at runtime. One memory resource and one set of strategies serve every tenant.

Diagram of a multi-tenant namespace hierarchy with tenant, service, component, team, actor, and session levels

Figure 3: A multi-tenant namespace hierarchy nests tenant, service, component, team, actor, and session levels

Defining namespace keys

The per-strategy guidance still applies beneath the custom levels. Facts stay actor-scoped and summaries get the session appended:

response = control_client.create_memory(
    name="EngineeringSupportMemory",
    description="Memory for the engineering support agent",
    eventExpiryDuration=30,
    memoryStrategies=[
        {
            "semanticMemoryStrategy": {
                "name": "engineering-facts",
                "namespaceTemplates": ["/tenant/{tenant}/service/{service}/component/{component}/team/{team}/actor/{actorId}/facts/"]
            }
        },
        {
            "summaryMemoryStrategy": {
                "name": "session-summaries",
                "namespaceTemplates": ["/tenant/{tenant}/service/{service}/component/{component}/team/{team}/actor/{actorId}/session/{sessionId}/summary/"]
            }
        }
    ],
    namespaceKeys=[
        {"key": "tenant", "validation": {"regexPattern": "^[a-z][a-z0-9-]*$"}},
        {"key": "service", "validation": {"allowedValues": ["payments", "checkout", "search"]}},
        {"key": "component"},
        {"key": "team", "validation": {"regexPattern": "^[a-z][a-z0-9-]*$"}}
    ]
)

You can define up to 5 namespace keys per memory resource and reference up to 5 in a template. Keys are at most 32 lowercase alphanumeric characters and can’t reuse a built-in variable name. Values are lowercase, up to 64 characters. allowedValues takes up to 10 values and suits closed sets such as an environment or a fixed service list. For a tenant roster that grows with your customer base, use regexPattern (up to 64 characters) and enforce the actual tenant identity with IAM. Specifying both rules applies both. A key need not be referenced by any strategy, and one key can appear in several templates.

Set validation even when it feels redundant. These values arrive from application code at request time, which is where tenant isolation bugs come from: a stale variable in a retry path, a config value pointing at the wrong environment. A value that fails validation is rejected by CreateEvent, so you find out immediately rather than discovering a stray namespace weeks later. NOTE: If there are more than one validation setup on the namespaceKey, all the validations are enforced. If one fails, the entire request fails.

Supplying values at runtime

Pass the values in extractionConfig.namespaceVariables on each CreateEvent call:

agentcore_client.create_event(
    memoryId="EngineeringSupportMemory-abc325j329f",
    actorId="dev-123",
    sessionId="debug-1",
    payload=[{
        "conversational": {
            "role": "USER",
            "content": {"text": "Our connection pool deadlocks under high concurrency. Where do I start?"}
        }
    }],
    extractionConfig={
        "namespaceVariables": {
            "tenant": "octank",
            "service": "payments",
            "component": "ledger",
            "team": "core-ledger"
        }
    }
)

During extraction, the service substitutes those values into every template that references them, producing memories under:

/tenant/octank/service/payments/component/ledger/team/core-ledger/actor/dev-123/facts/
/tenant/octank/service/payments/component/ledger/team/core-ledger/actor/dev-123/session/debug-1/summary/

Across tenants, a single memory resource looks like this:

Memory Resource: EngineeringSupportMemory-abc325j329f
│
└── /tenant/
  ├── octank/
  │   └── service/payments/
  │       ├── component/ledger/team/core-ledger/
  │       │   ├── actor/dev-123/
  │       │   │   ├── facts/
  │       │   │   │   └── "Connection pool mutex deadlocks under high concurrency"
  │       │   │   └── session/debug-1/summary/
  │       │   │       └── "Investigated ledger deadlock, traced to lock ordering"
  │       │   └── actor/dev-456/facts/
  │       │       └── "Batch writes throttle at peak load"
  │       └── component/settlement/team/reconciliation/
  │           └── actor/dev-789/facts/
  │               └── "Cross-region replication lag spikes during failover"
  │
  └── examplecorp/
      └── service/search/component/relevance/team/ranking/
          └── actor/ml-eng-42/facts/
              └── "Embedding model v3 degrades recall on long-tail queries"

Custom variables only affect the write path. By the time you read memories back, the namespace is fully resolved, so namespace and namespacePath work exactly as before, with no new parameters.

Ordering the hierarchy

The order of the segments is the design decision that matters most. Every namespacePath query walks down from the level you name, so the broadest isolation boundary belongs at the top. With tenant first, a search across everything the payments service has learned stays inside one tenant by construction:

records = agentcore_client.retrieve_memory_records(
    memoryId="EngineeringSupportMemory-abc325j329f",
    namespacePath="/tenant/octank/service/payments/",
    searchCriteria={"searchQuery": "deadlocks under load", "topK": 10}
)

Reverse those two levels and the same query shape reaches across your entire customer base. Prefix-based IAM policies inherit the same property, which is why tenant-first hierarchies are easier to secure.

Each level you add is a value you must supply on every event, and it fragments retrieval: memories under two different components live in two subtrees that only a service-level namespacePath query brings back together. Add a level only if you’ll actually query or isolate at it.

Missing values and key updates

A missing custom variable behaves differently from an invalid one. If a template references a variable the CreateEvent request doesn’t supply, the namespace can’t be resolved and long-term extraction is skipped for that strategy. The CreateEvent call still succeeds and the event is persisted in short-term memory, so the conversation keeps working while no long-term memories accumulate behind it. Monitor the NamespaceResolutionFailure metric, emitted with the dimensions Operation, Resource, StrategyType, and StrategyId, and alarm on it. It is a straightforward way to notice a client that quietly stopped sending one variable after a refactor.

The namespaceKeys value in UpdateMemory replaces the existing set rather than merging into it, so treat it as a read-modify-write: call GetMemory, append the new key to the returned list, and pass the full list back. Omitting a key that a template still references raises a ValidationException. To retire a key, remove it from the template first, then from namespaceKeys.

Custom namespace variables compared to metadata

AgentCore memory also supports structured metadata that propagates from events to extracted records. The two features answer different questions. Namespace variables define where a memory lives: they set the boundary that retrieval and IAM operate on, and a record sits in exactly one namespace. Metadata describes what a memory is about within a boundary you’ve already established, letting you narrow retrieval by category, priority, or risk level. A filter you forget to apply returns everything, so metadata is the wrong tool for tenant isolation. If getting it wrong would be a security finding, it belongs in the namespace. If getting it wrong would be an irrelevant search result, it belongs in metadata.

Writing IAM policies for namespace access control

Namespaces integrate with IAM through condition keys that restrict which namespaces a principal can include in Memory API requests.

Exact match and hierarchical read policies

Use StringEquals with bedrock-agentcore:namespace to restrict a principal to one namespace. This policy uses the userId principal tag, injected at authentication time, so each user can read only their own preferences:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "bedrock-agentcore:RetrieveMemoryRecords",
        "bedrock-agentcore:ListMemoryRecords"
      ],
      "Resource": "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/CustomerSupportMemory-8f3k2j9d1a",
      "Condition": {
        "StringEquals": {
          "bedrock-agentcore:namespace": "/actor/${aws:PrincipalTag/userId}/preferences/"
        }
      }
    }
  ]
}

For hierarchical access, use StringLike with bedrock-agentcore:namespacePath. Replace the preceding condition with the following so a user can search across all of their own namespaces (facts, preferences, summaries) while helping prevent access to other users’ data:

"Condition": {
  "StringLike": {
    "bedrock-agentcore:namespacePath": "/actor/${aws:PrincipalTag/userId}/*"
  }
}

Write-path policies for custom namespace variables

The read-path keys act on a namespace that arrives fully resolved. Custom variables add a write path worth securing: the values in a CreateEvent request decide which namespace a memory lands in, and validation rules can’t tell you which tenant a caller is allowed to be.

The bedrock-agentcore:namespaceVariable/<variableName> condition key matches the value supplied for one custom variable. Tag each tenant’s role with its tenant name, and one policy pins every caller to its own subtree:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "WriteOnlyToOwnTenant",
      "Effect": "Allow",
      "Action": "bedrock-agentcore:CreateEvent",
      "Resource": "arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/EngineeringSupportMemory-abc325j329f",
      "Condition": {
        "StringEquals": {
          "bedrock-agentcore:namespaceVariable/tenant": "${aws:PrincipalTag/tenant}"
        }
      }
    }
  ]
}

A misconfigured or compromised client for Octank can send whatever it likes in namespaceVariables. Unless tenant matches the tag on its own principal, the write is refused. One behavior to design around: if the request omits a variable that a policy conditions on, the condition key is absent from the request context. The Allow doesn’t match, and the request is denied unless another statement grants it. That makes the variable mandatory for every caller the policy covers, so make sure clients set it before you roll out the policy.

Conclusion

Namespace design decides what your agent can retrieve, what gets consolidated together, and what one caller can see of another’s data. Those decisions are far easier to make before the first event is written than after a year of memories have accumulated under the wrong hierarchy.

  • Design from your access patterns. Decide who retrieves what, at which level of detail, and which boundaries must not be crossed. Put the broadest isolation boundary at the top.
  • Scope by strategy. Facts and preferences belong to the actor so they consolidate across sessions. Summaries and episodes belong to the session.
  • Pick the retrieval field deliberately. namespace matches one exact path. namespacePath returns the whole subtree. Keep leading and trailing slashes so prefixes don’t collide.
  • Reach for custom namespace variables when your isolation boundary isn’t the actor or the session. Constrain values with allowedValues or regexPattern, and alarm on NamespaceResolutionFailure.
  • Enforce boundaries with IAM condition keys: bedrock-agentcore:namespace and bedrock-agentcore:namespacePath on reads, bedrock-agentcore:namespaceVariable/<key> on writes.

To get started, visit the following resources:


About the authors

Akarsha Sehwag

Akarsha Sehwag

Akarsha is a Sr. Generative AI Data Scientist leading AgentCore memory GTM team. With over seven years of experience in AI/ML product development, she has delivered enterprise-grade solutions for customers across a wide range of industries. Outside of work, she enjoys learning new things and exploring the outdoors.

Noor Randhawa

Noor Randhawa

Noor is the Tech Lead for AgentCore memory at Amazon Web Services (AWS), building systems that enable developers to create intelligent, context-aware agents powered by Memory. He previously worked across Amazon Retail and Amazon Elastic Kubernetes Service (Amazon EKS), designing highly scalable and distributed platforms.

Sukruth G L

Sukruth G L

Sukruth is a Software Development Engineer for AgentCore memory at Amazon Web Services (AWS). His background lies in Machine Learning, Human-Robot Interaction, and he focuses on building scalable, reliable systems that power memory infrastructure for AI agents.

Piradeep Kandasamy

Piradeep Kandasamy

Piradeep is a Software Development Manager for AgentCore memory. Over his career at Amazon, he has built and scaled systems across Amazon Alexa, Amazon Elastic Container Service (Amazon ECS), and AWS CloudFormation, bringing deep expertise in distributed systems and large-scale cloud services to his current work on memory infrastructure for AI agents.