Skip to content
Helix Docs

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.

For developers Updated Oct 20, 2026
View as Markdown

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.

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:

// 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

AttributeTypeRole
pkStringPartition key
skStringSort key
expiresAtNumberWritten 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.
  • 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 works for short conversations; its 200 KB value cap bounds how long a history you can keep in one key.

// 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. Config field reference: agent definition reference.

Loading search…

Jump to a section

tab to move · esc to close