showcase/shell-docs/src/content/docs/bots/persistence.mdx
Where you're starting from: a dev bot where every restart wipes button handlers, resets thread state, and drops duplicate-detection memory. A user clicks a button five minutes after a deploy and nothing happens.
Where you're headed: the same bot, wired to a durable store, where actions survive restarts, concurrent turns are serialised with a turn lock, and each thread carries typed state that persists across runs.
<Steps> <Step> ### Choose a storage backend`createBot` accepts a `store.adapter` — the backing store for actions, locks, dedup, and state. Pick the backend that matches your deployment:
<Tabs items={["Memory", "Redis", "PostgreSQL"]}>
<Tab value="Memory">
The default. No install needed. State is lost on every restart — useful for local development and ephemeral environments.
```ts title="bot.ts"
import { createBot, MemoryStore } from "@copilotkit/bot";
const bot = createBot({
adapters: [/* ... */],
agent: (threadId) => /* ... */,
store: {
adapter: new MemoryStore(), // default — can be omitted
},
});
```
</Tab>
<Tab value="Redis">
Durable, fast, production-ready. Actions survive restarts; the store works across multiple bot instances pointing at the same Redis URL.
```bash
npm install @copilotkit/bot-store-redis
```
```ts title="bot.ts"
import { createBot } from "@copilotkit/bot";
import { createRedisStore } from "@copilotkit/bot-store-redis";
const bot = createBot({
adapters: [/* ... */],
agent: (threadId) => /* ... */,
store: {
adapter: createRedisStore({ url: process.env.REDIS_URL! }),
},
});
```
</Tab>
<Tab value="PostgreSQL">
Relational persistence with automatic schema management. Set `autoMigrate: true` to let the store create its tables on first boot.
```bash
npm install @copilotkit/bot-store-postgres
```
```ts title="bot.ts"
import { createBot } from "@copilotkit/bot";
import { createPostgresStore } from "@copilotkit/bot-store-postgres";
const bot = createBot({
adapters: [/* ... */],
agent: (threadId) => /* ... */,
store: {
adapter: createPostgresStore({
connectionString: process.env.DATABASE_URL!,
autoMigrate: true,
}),
},
});
```
</Tab>
</Tabs>
<Callout type="info" title="Durable actions are automatic">
No code changes needed for button handlers. With `MemoryStore`, a button clicked after a restart is acknowledged by Slack but ignored — the handler is gone. Swap in Redis or PostgreSQL and the same handler is fetched from the store and executed normally. The [ActionStore reference](/reference/bot/types/ActionStore) documents the contract if you want to inspect it.
</Callout>
A durable store does more than keep button handlers alive. The bot uses the store for two additional guarantees:
**Turn lock** — at most one agent run per thread at a time. If a second event arrives while the agent is still running, `onLockConflict` decides what happens:
```ts
store: {
adapter: createRedisStore({ url: process.env.REDIS_URL! }),
onLockConflict: "drop", // silently ignore the new event (default)
// onLockConflict: "force", // evict the running turn and start fresh
// onLockConflict: async ({ thread }) => { /* custom logic */ },
lockTtl: 60_000, // ms before a stale lock auto-expires (default: 60 000)
},
```
**Dedup** — identical inbound events (Slack sometimes delivers the same event twice) are deduplicated by their event id. The window is controlled by `dedupTtl`:
```ts
store: {
adapter: createRedisStore({ url: process.env.REDIS_URL! }),
dedupTtl: 300_000, // 5-minute dedup window (default: 300 000)
},
```
<Callout type="info" title="Lock and dedup TTLs">
Both values are in milliseconds. The defaults are conservative — raise `dedupTtl` if your platform retries events after more than five minutes, or lower `lockTtl` if you want stale locks to expire faster during long agent runs.
</Callout>
Each conversation thread can carry structured data that persists between turns. Pass a [Standard Schema](https://standardschema.dev) (e.g. a Zod object) as `store.state` and the `Thread` class types `thread.state()` and `thread.setState()` to match it — validated at runtime on every write.
```ts title="bot.ts"
import { createBot } from "@copilotkit/bot";
import { createRedisStore } from "@copilotkit/bot-store-redis";
import { z } from "zod";
const TicketFlow = z.object({
stage: z.enum(["triage", "assigned", "resolved"]).default("triage"),
assignee: z.string().optional(),
priority: z.enum(["low", "medium", "high"]).optional(),
});
const bot = createBot({
adapters: [/* ... */],
agent: (threadId) => /* ... */,
store: {
adapter: createRedisStore({ url: process.env.REDIS_URL! }),
state: TicketFlow,
},
});
bot.onMention(async ({ thread }) => {
// thread.state() returns TicketFlow — typed and validated
const current = await thread.state();
if (current.stage === "triage") {
await thread.setState({ stage: "assigned", assignee: "on-call" });
await thread.runAgent({
prompt: "The ticket has been assigned to on-call. Summarise next steps.",
});
return;
}
await thread.runAgent();
});
```
`thread.setState()` merges the partial patch into the stored state and validates the result against your schema — it throws if the merged object doesn't conform, so invalid state never reaches the store.
<Callout type="info" title="Thread API reference">
Full signatures for `state()`, `setState()`, `runAgent()`, and the rest of the thread surface are in the [Thread reference](/reference/bot/classes/Thread).
</Callout>
If neither Redis nor PostgreSQL fits your infrastructure, implement the `StateStore` interface directly:
```ts title="my-store.ts"
import type { StateStore } from "@copilotkit/bot";
export class MyCustomStore implements StateStore {
// Required methods: get, set, delete, lock, unlock, dedup
// See the StateStore reference for the full contract.
}
```
The package ships `runStateStoreConformance(store)` — a test suite that verifies your implementation against the full contract (locking, dedup, state round-trips). Run it in your test suite before wiring the store into production.
<Callout type="info" title="StateStore reference">
The full interface definition and conformance test helper are documented in the [StateStore reference](/reference/bot/types/StateStore).
</Callout>
You've reached point B. The bot now has a durable store: button handlers survive restarts, concurrent turns are serialised, duplicates are dropped, and every thread carries typed state that outlives the process.