CopilotKitDocs
  • Docs
  • Reference
  • Cookbook
Get Enterprise Intelligence free
CopilotKitDocs
DocsReferenceCookbook
DocsReferenceCookbook

Getting Started

IntroductionQuickstartCopilotKit CLIBuild with agents

Basics

Prebuilt Components
Programmatic ControlInspector

Generative UI

Tool RenderingState Rendering

App Control

Frontend Tools
Readables

AG2

Authentication

Backend

Copilot RuntimeAG-UI

Intelligence Platform

Enterprise Intelligence PlatformCloud-Hosted Enterprise IntelligenceSelf-Hosting Enterprise IntelligenceEnterprise Intelligence ArchitectureThreads & Persistence ArchitectureThreads

Troubleshooting

Common Copilot IssuesError Debugging & ObservabilityMigrate to 1.10.XMigrate to 1.8.2Migrate to V2

Other

AG2BotsPersistence

Persistence

Make bot state, interactive actions, and per-thread data survive restarts and scale across multiple instances with a durable store backend.


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.

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:

The default. No install needed. State is lost on every restart — useful for local development and ephemeral environments.

bot.ts
import { createBot, MemoryStore } from "@copilotkit/bot";

const bot = createBot({
  adapters: [/* ... */],
  agent: (threadId) => /* ... */,
  store: {
    adapter: new MemoryStore(), // default — can be omitted
  },
});

Durable, fast, production-ready. Actions survive restarts; the store works across multiple bot instances pointing at the same Redis URL.

npm install @copilotkit/bot-store-redis
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! }),
  },
});

Relational persistence with automatic schema management. Set autoMigrate: true to let the store create its tables on first boot.

npm install @copilotkit/bot-store-postgres
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,
    }),
  },
});

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 documents the contract if you want to inspect it.

Understand what you get: turn locks and dedup#

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:

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:

store: {
  adapter: createRedisStore({ url: process.env.REDIS_URL! }),
  dedupTtl: 300_000, // 5-minute dedup window (default: 300 000)
},

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.

Add typed per-thread state#

Each conversation thread can carry structured data that persists between turns. Pass a Standard Schema (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.

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.

Thread API reference

Full signatures for state(), setState(), runAgent(), and the rest of the thread surface are in the Thread reference.

Bring your own store#

If neither Redis nor PostgreSQL fits your infrastructure, implement the StateStore interface directly:

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.

StateStore reference

The full interface definition and conformance test helper are documented in the StateStore reference.

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.

6d9397c