Skip to content

Session Management

Strands Agents automatically persist conversation history using S3-based session management. This guide explains how sessions work and how you can leverage the architecture for your own multi-tenant applications.

Strands SDK Reference

For deeper understanding of the session internals, see the Strands SDK session module source code.

How Sessions Work

When you chat with an agent, the conversation is automatically saved to S3. Each message exchange is persisted, allowing you to continue conversations across multiple requests.

Basic Usage

Start a new conversation:

bash
curl -N "https://stream.api.universalapi.co/agent/{agentId}/chat" \
  -H "Content-Type: application/json" \
  -H "X-Uni-UserId: $USER_ID" \
  -H "X-Uni-SecretUniversalKey: $SECRET_KEY" \
  -d '{"prompt": "Hello! My name is Alice."}'

Response includes the conversation ID:

__META__{"conversationId": "625c2112-9eac-4630-bbbc-785a845a182d"}__
Hello Alice! Nice to meet you...

Continue the conversation:

bash
curl -N "https://stream.api.universalapi.co/agent/{uniAgentId}/chat" \
  -H "Content-Type: application/json" \
  -H "X-Uni-UserId: $USER_ID" \
  -H "X-Uni-SecretUniversalKey: $SECRET_KEY" \
  -d '{
    "prompt": "What is my name?",
    "conversationId": "625c2112-9eac-4630-bbbc-785a845a182d"
  }'

The agent remembers the context: "Your name is Alice!"


S3 Storage Architecture

Conversation data is stored in S3 with the following structure:

<bucket>/sessions/{userId}/{uniAgentId}/session_{conversationId}/
├── session.json                    # Session metadata
├── agents/
│   ├── agent_default/              # Main agent (Strands default)
│   │   ├── agent.json              # Agent state
│   │   └── messages/
│   │       ├── message_0.json      # User message
│   │       ├── message_1.json      # Assistant response
│   │       └── ...
│   ├── agent_researcher/           # Sub-agent (if spawned)
│   │   ├── agent.json
│   │   └── messages/
│   │       └── ...
│   └── agent_writer/               # Another sub-agent (if spawned)
│       ├── agent.json
│       └── messages/
│           └── ...
└── multi_agents/                   # Multi-agent orchestration state (if used)
    └── multi_agent_{orchestratorId}/
        └── multi_agent.json

Terminology

TermDescription
userIdThe Universal API user ID
uniAgentIdThe Universal API agent UUID (from AgentTable pk)
conversationIdThe conversation/session UUID
agent_defaultStrands default folder for the main agent
agent_*Additional folders for sub-agents (if your agent spawns them)

Why the multi-agent structure?

The S3 structure supports Strands multi-agent capabilities. Even if your agent doesn't spawn sub-agents, the agents/agent_default/ folder structure is used. If your agent does spawn sub-agents (e.g., an orchestrator delegating to specialists), each sub-agent gets its own folder with its own message history.

Multi-Tenancy Layers

The storage structure provides two levels of isolation:

LayerPath ComponentManaged ByPurpose
Platform{userId}/{uniAgentId}Universal APIIsolates your data from other users
Application{conversationId}You (the developer)Isolates conversations within your agent

Building Multi-Tenant Applications

If you're building an application where your agent serves multiple end-users or tenants, you can leverage the conversationId for tenant isolation.

Client-Defined Conversation IDs

The conversationId is not required to be a random UUID. You can provide your own structured identifier on the very first message:

bash
# First message - define your own conversationId
curl -N "https://stream.api.universalapi.co/agent/{agentId}/chat" \
  -H "Content-Type: application/json" \
  -H "X-Uni-UserId: $USER_ID" \
  -H "X-Uni-SecretUniversalKey: $SECRET_KEY" \
  -d '{
    "prompt": "Hello!",
    "conversationId": "tenant-acme-user-bob-chat-001"
  }'

If you don't provide a conversationId, the system generates a random UUID. But if you do provide one, it uses your value exactly as given.

Naming Conventions

Choose a naming convention that fits your application:

PatternExampleUse Case
Tenant prefixacme-corp-conv-abc123SaaS with organization tenants
User scopeduser-12345-chat-1Per-user conversation isolation
Hierarchicalorg:acme:team:sales:conv:1Complex organizational structures
Compositetenant-acme-user-bob-session-morningMultiple isolation dimensions

Example: Multi-Tenant SaaS

Here's how a SaaS application might structure conversations:

javascript
// Your backend generates tenant-scoped conversation IDs
function startConversation(tenantId, userId) {
  const conversationId = `${tenantId}-${userId}-${Date.now()}`;

  return fetch(`https://stream.api.universalapi.co/agent/${agentId}/chat`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Uni-UserId': UNIVERSAL_API_USER_ID,
      'X-Uni-SecretUniversalKey': UNIVERSAL_API_SECRET_KEY
    },
    body: JSON.stringify({
      prompt: userMessage,
      conversationId: conversationId  // Your tenant-scoped ID
    })
  });
}

This creates isolated conversation histories:

sessions/{yourUserId}/{agentId}/session_acme-corp-user-alice-1706400000000/
sessions/{yourUserId}/{agentId}/session_acme-corp-user-bob-1706400001000/
sessions/{yourUserId}/{agentId}/session_other-tenant-user-charlie-1706400002000/

Listing Conversations by Tenant

When listing conversations, you can filter by your naming convention:

bash
# Get all conversations
curl "https://api.universalapi.co/agent/{agentId}/conversations" \
  -H "X-Uni-UserId: $USER_ID" \
  -H "X-Uni-SecretUniversalKey: $SECRET_KEY"

Then filter client-side by conversationId prefix:

javascript
const conversations = response.conversations;
const acmeConversations = conversations.filter(c =>
  c.conversationId.startsWith('acme-corp-')
);

Session Lifecycle

Creation

Sessions are created automatically on the first message. No explicit "create session" call is needed.

Persistence

All messages are persisted to S3 immediately after each exchange. The agent automatically loads the full conversation history when you provide a conversationId.

Deletion

Delete a conversation and all its data:

bash
curl -X DELETE "https://api.universalapi.co/agent/{agentId}/conversation/{conversationId}" \
  -H "X-Uni-UserId: $USER_ID" \
  -H "X-Uni-SecretUniversalKey: $SECRET_KEY"

This removes both the DynamoDB metadata and the S3 conversation data.


Best Practices

1. Use Meaningful Conversation IDs

Instead of relying on random UUIDs, use structured IDs that encode context:

✅ tenant-acme-user-123-support-ticket-456
✅ org-sales-team-west-deal-review-789
❌ 625c2112-9eac-4630-bbbc-785a845a182d (harder to debug/filter)

2. Include Timestamps for Uniqueness

When users might have multiple conversations, include a timestamp:

javascript
const conversationId = `${tenantId}-${userId}-${Date.now()}`;

3. Validate Conversation ID Format

On your backend, validate that conversation IDs match your expected format before passing to the API:

javascript
function isValidConversationId(id) {
  // Example: must start with tenant prefix
  return /^[a-z0-9-]+-[a-z0-9-]+-\d+$/.test(id);
}

4. Don't Expose Internal IDs

If your conversation IDs contain sensitive information, consider hashing:

javascript
const conversationId = `tenant-${hash(tenantId)}-${hash(internalUserId)}-${timestamp}`;

Summary

FeatureDescription
Auto-persistenceAll conversations saved to S3 automatically
Platform isolationYour data isolated by userId + agentId
Custom conversation IDsProvide your own IDs for tenant isolation
Flexible namingUse any string format that fits your needs
Full CRUDList, get, and delete conversations via API

The session architecture gives you both automatic multi-tenancy at the platform level and the flexibility to implement your own tenant isolation within your agent applications.

Universal API - The agentic entry point to the universe of APIs