showcase/shell-docs/src/content/snippets/shared/threads/headless-threads.mdx
CopilotKit Rich Threads enable persistent, resumable multi-turn conversations. The useThreads hook lists, creates, renames, archives, and deletes Enterprise Intelligence Platform threads with realtime synchronization via WebSocket. Threads work with any agent framework — the Enterprise Intelligence Platform stores conversation history server-side, so users can close their browser and pick up where they left off. It does not list or mutate native LangGraph, ADK, or other framework stores unless your backend explicitly bridges those systems. Thread metadata updates (renames, archives, new threads) appear on connected clients without polling.
<OpsPlatformCTA variant="inline" title="Threads run on the Enterprise Intelligence Platform" body="Get persistent threads and realtime sync on the free Developer tier." surface="docs_threads" />
<Callout type="info" title="Prebuilt thread UI: Threads Drawer"> If your app came from a CopilotKit CLI starter, it already includes the prebuilt **[Threads Drawer](/prebuilt-components/copilot-threads-drawer)**. Keep it for the shortest path to a production thread switcher. Use the `useThreads` steps below when adding a custom thread UI to an existing app or replacing the Drawer with your own layout and interactions (including thread **rename**, which the prebuilt Drawer doesn't surface). </Callout>@copilotkit/react-core v1.56 or laterFor multi-user applications, configure the Runtime to scope Rich Threads to the signed-in user before exposing thread lists or lifecycle actions.
Your `CopilotRuntime` must be connected to Enterprise Intelligence before the thread UI can list and resume conversations. If your app came from a CLI starter, this Runtime configuration is generated for you. Otherwise, keep your existing Enterprise Intelligence Runtime configuration while adding the headless UI. Thread names are automatically generated by the LLM after the first message — you can disable this with `generateThreadNames: false`.
```typescript title="server.ts"
import { CopilotRuntime } from "@copilotkit/runtime";
const runtime = new CopilotRuntime({
agents: {
default: agent,
},
// Thread names are auto-generated by default.
// Set to false to disable:
// generateThreadNames: false,
// Optional: tune thread lock behavior
// lockTtlSeconds: 20, // Lock TTL (default 20s, max 3600s)
// lockHeartbeatIntervalSeconds: 15, // Heartbeat interval (default 15s, max 3000s)
// lockKeyPrefix: "my-app", // Custom Redis key prefix for the lock
});
```
CLI-created starters write the cloud-hosted platform URLs and project-scoped `INTELLIGENCE_API_KEY` to `.env`; keep that key server-side. Existing Intelligence-enabled apps should keep their current server-side Runtime configuration. Production self-hosting uses the same React APIs and is deployed with CopilotKit Engineering through [Self-Hosting Enterprise Intelligence](/premium/self-hosting).
**Thread lock options:** When an agent run starts, the runtime acquires a lock on the thread to prevent concurrent runs. You can tune this behavior:
| Option | Default | Max | Description |
|--------|---------|-----|-------------|
| `lockTtlSeconds` | `20` | `3600` (1 hour) | How long the lock is held before it expires automatically. |
| `lockHeartbeatIntervalSeconds` | `15` | `3000` (50 min) | How often the runtime renews the lock during a run. |
| `lockKeyPrefix` | — | — | Custom Redis key prefix for the thread lock. Useful when multiple apps share a Redis instance. |
</Step>
<Step>
### List and manage threads with useThreads
Use the `useThreads` hook to fetch and manage threads for a specific agent. The hook returns the thread list, loading state, and mutation methods.
```tsx title="ThreadSidebar.tsx"
import { useThreads } from "@copilotkit/react-core/v2"; // [!code highlight]
function ThreadSidebar() {
const { // [!code highlight:6]
threads,
isLoading,
renameThread,
archiveThread,
deleteThread,
} = useThreads({ agentId: "my-agent" });
if (isLoading) return <div>Loading...</div>;
return (
<div>
{threads.map((thread) => (
<div key={thread.id}>
<span>{thread.name ?? "New conversation"}</span>
<button onClick={() => renameThread(thread.id, "Renamed")}>
Rename
</button>
<button onClick={() => archiveThread(thread.id)}>
Archive
</button>
</div>
))}
</div>
);
}
```
The `threads` array is sorted by most recently updated first and stays synchronized in realtime — new threads from other tabs or devices appear automatically.
**Archive vs. delete:** `archiveThread` is a soft delete — the thread stays in the database but is hidden from the list by default. Pass `includeArchived: true` to show archived threads. `deleteThread` is permanent and irreversible. Neither has a built-in confirmation dialog — add your own if needed.
</Step>
<Step>
### Switch between threads
When a user selects a thread, pass its `threadId` to your chat component. The chat clears the current messages, fetches the selected thread's history, and replays it. If the agent is still running on that thread, the chat picks up the live stream.
```tsx title="App.tsx"
import { CopilotChat } from "@copilotkit/react-core/v2"; // [!code highlight]
import { useState } from "react";
function App() {
const [activeThreadId, setActiveThreadId] = useState<string | undefined>();
return (
<div className="flex">
<ThreadSidebar onSelectThread={setActiveThreadId} />
<CopilotChat threadId={activeThreadId} />
</div>
);
}
```
When `threadId` changes, the chat component automatically loads the selected thread's history and reconnects to the agent's stream. If the agent is still running on that thread, the chat picks up the live stream.
<WhenFrameworkHas flag="thread_persistence_pattern" equals="langgraph">
<Callout type="info" title="LangGraph persistence">
When you pass an explicit CopilotKit `threadId`, CopilotKit forwards it to your backend as the AG-UI `threadId`. A LangGraph backend can use that value directly, or map it to its own checkpoint/thread identifier, when you implement that mapping. LangGraph Platform thread IDs must be UUIDs. CopilotKit `useThreads` manages Enterprise Intelligence Platform thread records; rename, archive, and delete operations do not update LangGraph stores unless your backend adds that bridge.
</Callout>
</WhenFrameworkHas>
<WhenFrameworkHas flag="thread_persistence_pattern" equals="adk-session">
<Callout type="info" title="ADK sessions">
When you pass an explicit CopilotKit `threadId`, CopilotKit forwards it to your backend as the AG-UI `threadId`. An ADK backend can map that value to an ADK session ID, but the current showcase uses in-memory ADK services, so those sessions are not durable by default. Durable ADK session persistence requires configuring a separate ADK session service. CopilotKit `useThreads` manages Enterprise Intelligence Platform thread records, not ADK's native session store.
</Callout>
</WhenFrameworkHas>
</Step>
<Step>
### Add pagination for large thread lists
For users with many conversations, use the `limit` parameter to enable cursor-based pagination.
```tsx title="ThreadSidebar.tsx"
const {
threads,
hasMoreThreads,
isFetchingMoreThreads,
fetchMoreThreads,
} = useThreads({
agentId: "my-agent",
limit: 20, // [!code highlight]
});
// In your JSX:
{hasMoreThreads && (
<button
onClick={fetchMoreThreads}
disabled={isFetchingMoreThreads}
>
{isFetchingMoreThreads ? "Loading..." : "Load more"}
</button>
)}
```
</Step>