# Bring your own session storage

> Keep agent conversation history in a DynamoDB table you own, reached through a connected AWS credential, or manage history yourself.

By default the platform stores agent sessions for you. When history has to live in your own account, for retention policy, for audit, or to read it from other systems, point the agent runtime at a DynamoDB table you own and it writes every session there instead. When you need a different store entirely, drive the loop yourself and keep history wherever you like.

:::note{title="Version"}
Needs `@trayai/helix-sdk` and `@trayai/helix-cli` newer than 1.6.0.
:::

## Option 1: your own DynamoDB table

Add `agentSessions` to `helix.config.ts`. `auth` is an authentication alias holding an AWS credential; the platform signs every request to the table with it, so no AWS key appears in your code or your deployment:

```typescript
// helix.config.ts
import { defineConfig } from '@trayai/helix-sdk';

export default defineConfig({
  name: 'support',
  workspaceId: '<your-workspace-uuid>',
  authentications: {
    anthropicKey: '<your-anthropic-auth-uuid>',
    sessionsTable: '<your-aws-auth-uuid>',
  },
  aiProviders: {
    anthropicKey: 'anthropic',
  },
  agentSessions: {
    store: 'dynamodb',
    auth: 'sessionsTable',
    table: 'helix-agent-sessions',
    region: 'us-east-1',
  },
});
```

The setting applies to every agent in the project, in `helix dev` and in production alike. Nothing in the agent definitions or calling code changes.

### Create the table

| Attribute | Type | Role |
|---|---|---|
| `pk` | String | Partition key |
| `sk` | String | Sort key |
| `expiresAt` | Number | Written only when the agent sets `memory.ttl`. Point the table's time-to-live setting at it if you want expired sessions removed |

The credential behind `auth` needs these actions on the table: `dynamodb:GetItem`, `dynamodb:Query`, `dynamodb:UpdateItem`, and `dynamodb:BatchWriteItem`. Writes and deletes of message items go through batch writes, so no separate put or delete permission is needed.

### What is stored

One partition per session. The partition key is `agent:<name>:session:<id>` for a project-scoped session and `agent:<name>:user:<userId>:session:<id>` for a user-scoped one, so a user's sessions are separated by key, not by a filter. Within a partition, one item per message under `msg#<sequence>`, plus a `meta` item carrying the creation and last-activity times, the message count, the pending approvals, and, when the agent sets a `ttl`, `expiresAt` as epoch seconds. Every message is kept: nothing is deleted as a conversation grows.

With a `ttl`, only the `meta` item carries `expiresAt`, so the table's time-to-live setting removes the meta item and leaves the message items in place. The runtime treats a session whose meta item is gone but whose messages remain as expired, and deletes the leftover messages the next time that session is opened.

Rows are plain DynamoDB JSON, so you can read them from your own tooling. Treat the layout as owned by Helix: do not write to the table yourself.

### Limits of the table option

- **One message is one item**, and an item is capped at 400 KB. A message larger than that, such as a tool returning a very large payload, is rejected with `SdkError.agentSessionsFailed` before anything is written. The model call has already happened by then, so keep tool results small; see [defining tools](/documentation/guides/agent-tools/).
- **`agent.sessions()` is not supported.** The table has no index for a user's most recent sessions. Calling it throws an error that says to use the platform store.
- **Retention is opt-in, and yours.** With no `ttl`, nothing expires and no `expiresAt` is written. With one, the runtime writes `expiresAt` and, on open, treats a session past it as expired: with approvals pending it refuses the session, otherwise it clears the history and the id starts fresh. Deleting rows without the session being opened again is the table's time-to-live setting, which you enable.
- **The table grows with the conversation.** Nothing is removed as a session grows. Each turn reads only the newest items for the model's window, so a long session costs no more per turn than a short one.

### Switching back

Remove `agentSessions`, or set `agentSessions: { store: 'managed' }`, and the next deploy uses the platform store. Sessions already in your table are not migrated.

## Option 2: manage history yourself

If the store you want is not DynamoDB, skip sessions and drive the loop with the agent's model, tools, and instructions. You pass the history in and save what comes back. The [key-value store](/documentation/guides/key-value-store/) works for short conversations; its 200 KB value cap bounds how long a history you can keep in one key.

```typescript
// functions/api/v1/notes-chat.post.ts
import { z } from 'zod';
import { defineFunction, stepCountIs } from '@trayai/helix-sdk';
import type { ModelMessage } from '@trayai/helix-sdk';

export const input = z.object({
  conversationId: z.string().min(1),
  message: z.string().min(1),
});

const KEEP = 40; // messages to retain per conversation

export default defineFunction<typeof input>(async (ctx) => {
  const agent = ctx.agent('notes-agent');
  const kv = ctx.kv();
  const { userId } = ctx;
  const key = `chat:${userId}:${ctx.input.conversationId}`;

  const history = (await kv.getItem<ModelMessage[]>(key)) ?? [];
  const turn: ModelMessage = { role: 'user', content: ctx.input.message };

  const result = await ctx.ai.generateText({
    model: agent.model,
    system: agent.instructions,
    tools: agent.tools,
    messages: [...history, turn],
    stopWhen: stepCountIs(10),
  });

  const next = [...history, turn, ...result.response.messages].slice(-KEEP);
  await kv.setItem(key, next, { ttl: 60 * 60 * 24 * 7 });

  return { text: result.text, cost: result.helix.cost.amount };
});
```

What this gives up: the approval pause and resume cycle, the `agent.generate` span, and the platform's scope enforcement. In exchange you choose the store, the key, the retention, and the trim rule. Keying by the execution's `userId` as above gives you user scoping by construction.

When limiting what you send from your own history, start at a user message, as the runtime's own window does. A history that starts with a tool result whose tool call was dropped is rejected by the model provider.

For the rest of the custom-loop pattern, including per-step control, see [agent architecture and patterns](/documentation/guides/agent-patterns/). Config field reference: [agent definition reference](/documentation/reference/agents/).

---

Canonical: https://helix.tray.ai/documentation/guides/agent-session-storage/
Any link on this page is available as markdown by appending .md to its URL.
Full corpus: https://helix.tray.ai/documentation/llms-full.txt