Appearance
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.jsonTerminology
| Term | Description |
|---|---|
userId | The Universal API user ID |
uniAgentId | The Universal API agent UUID (from AgentTable pk) |
conversationId | The conversation/session UUID |
agent_default | Strands 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:
| Layer | Path Component | Managed By | Purpose |
|---|---|---|---|
| Platform | {userId}/{uniAgentId} | Universal API | Isolates 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:
| Pattern | Example | Use Case |
|---|---|---|
| Tenant prefix | acme-corp-conv-abc123 | SaaS with organization tenants |
| User scoped | user-12345-chat-1 | Per-user conversation isolation |
| Hierarchical | org:acme:team:sales:conv:1 | Complex organizational structures |
| Composite | tenant-acme-user-bob-session-morning | Multiple 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
| Feature | Description |
|---|---|
| Auto-persistence | All conversations saved to S3 automatically |
| Platform isolation | Your data isolated by userId + agentId |
| Custom conversation IDs | Provide your own IDs for tenant isolation |
| Flexible naming | Use any string format that fits your needs |
| Full CRUD | List, 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.