showcase/shell-docs/src/content/snippets/shared/threads/threads-lifecycle.mdx
Every CopilotKit conversation is scoped to a thread, identified by a threadId. This page explains the full lifecycle of that thread on the client: how the id is created, how prior history is restored when a conversation loads, how to switch between threads or start a new one, and where CopilotKit's threads end and your agent framework's own persistence (e.g. a LangGraph checkpointer) begins.
threadId, the client generates one (a UUID v4).threadId. If a server-side store is configured (the Enterprise Intelligence Platform, or a persisting AgentRunner), they are persisted as they happen so the thread can be replayed later. A runtime with no persistence layer keeps nothing server-side. See Threads & Persistence Architecture for the full server-side model.threadId, the client connects and replays the persisted history into the UI.When you don't supply one, CopilotKit mints a threadId on the client, at mount, using a UUID v4. The value is resolved by this precedence (highest first):
threadId prop on <CopilotChat> / <CopilotChatConfigurationProvider> (this is authoritative: it also drives history replay and disables the welcome screen).setActiveThreadId(...) / a picked thread row / startNewThread().threadId inherited from a parent configuration provider.threadId seed.randomUUID().import { CopilotChat } from "@copilotkit/react-core/v2";
// You control the id: history replays for it, and it's stable across remounts.
<CopilotChat agentId="my-agent" threadId={myThreadId} />
CopilotKit v2 does not use an initialMessages prop to seed a conversation. History is restored one of two ways:
1. Server-side replay. When a chat mounts with an explicit threadId, the client sets agent.threadId and calls connectAgent(), which replays that thread's persisted event history back into the UI, with no manual message wiring:
<CopilotChat agentId="my-agent" threadId={someExistingThreadId} />
For a fresh (non-explicit) thread, e.g. after startNewThread(), there's nothing to replay, so connect is skipped and the message view is cleared.
2. Manual hydration. If you're managing messages yourself (for non-platform or custom cases), read them from the agent and set them directly:
import { useAgent } from "@copilotkit/react-core/v2";
function MyComponent() {
const { agent } = useAgent({ agentId: "my-agent" });
// Read the current conversation
const messages = agent.messages;
// Replace it (e.g. hydrate from your own store)
// agent.setMessages(myPersistedMessages);
return null;
}
Your application owns end-user authentication. For an Intelligence-enabled
Runtime, implement identifyUser(request) with the server-verified session or
token from your auth provider and return a stable user id and name:
const runtime = new CopilotRuntime({
agents: { default: agent },
intelligence,
identifyUser: async (request) => {
const session = await verifyAppSession(request); // Your server-side auth.
if (!session?.user) throw new Error("Unauthorized");
return {
id: session.user.id,
name: session.user.name,
};
},
});
Enterprise Intelligence combines that application user identity with the selected project when it scopes thread lists, subscriptions, history, rename, archive, unarchive, and delete operations. The project Runtime API key is a separate credential: keep it on the server and do not use the developer who created that key as the application user.
<Callout type="warn"> A static `identifyUser` value is suitable only for a single-user demo. In a multi-user application, every request must resolve the authenticated application user or those users will share the same thread scope. </Callout>See Authentication for forwarding and verifying your provider's auth
context. identifyUser is the additional Runtime contract that assigns the
verified application user to Rich Threads.
Set the active thread to restore its history, or start a fresh conversation:
import { useCopilotChatConfiguration } from "@copilotkit/react-core/v2";
function ThreadControls() {
const config = useCopilotChatConfiguration();
return (
<>
<button onClick={() => config?.setActiveThreadId(existingId, { explicit: true })}>
Open conversation
</button>
<button onClick={() => config?.startNewThread()}>New chat</button>
</>
);
}
setActiveThreadId(threadId, { explicit }): explicit: true (the default) treats it as a known thread and replays its history, while explicit: false shows the welcome screen.startNewThread(): resets to a freshly minted, non-explicit id. Also available from useThreads() for thread-sidebar UIs.A common ask: intercept the user's first message to mint a thread via your own backend (e.g. create a DB row and get its id) before the conversation starts.
On the built-in <CopilotChat> this cannot be done through onSubmitMessage: the component sets its own submit handler internally and discards any you pass. Instead, use one of these approaches.
Recommended: mint up front and drive the id. Create your thread when the user starts a new conversation (e.g. on a "New chat" action, or lazily just before you render the chat), then hand the id to CopilotKit:
async function startConversation() {
const { id } = await myApi.createThread(); // your backend
config?.setActiveThreadId(id, { explicit: true });
// or render <CopilotChat threadId={id} />
}
At submit-time (headless only). If you must intercept exactly when the user hits send, compose the UI yourself with CopilotChatInput / CopilotChatView and pass your own onSubmitMessage (the built-in <CopilotChat> ignores it). Because the handler is yours, you set the thread on the agent and drive the send:
import { CopilotChatInput, useAgent } from "@copilotkit/react-core/v2";
function MyInput() {
const { agent } = useAgent({ agentId: "my-agent" });
return (
<CopilotChatInput
onSubmitMessage={async (text) => {
const { id } = await myApi.createThread(); // your backend
agent.threadId = id; // set it on the agent directly
agent.addMessage({ role: "user", content: text });
// ...then trigger the run with your send logic
}}
/>
);
}
These are two different layers, correlated only by the shared threadId:
| Layer | What it stores | Managed by |
|---|---|---|
| CopilotKit threads | Conversation list + full AG-UI event history (messages, tool calls, state), with realtime sync | The Enterprise Intelligence Platform, via useThreads |
| Framework-native persistence | Framework-internal state/checkpoints | Your agent, e.g. a LangGraph AsyncPostgresSaver |
A LangGraph checkpointer (an AsyncPostgresSaver + compile(checkpointer=...)) creates LangGraph's own checkpoint tables. It does not create a CopilotKit "threads" table, and you won't see a threads table appear just from configuring a checkpointer. Conversely, useThreads rename/archive/delete operate on platform threads and do not reach into LangGraph (or ADK, etc.) stores unless your backend explicitly bridges them.
The bridge between the two is the threadId: when you pass an explicit CopilotKit threadId, it's forwarded to the backend as the AG-UI threadId, which your framework can use directly as (or map to) its own checkpoint/thread id.
After historical import, future conversations that run through CopilotKit are written to Enterprise Intelligence as Rich Threads. If your LangGraph agent continues using a durable checkpointer or LangGraph Platform, those same runs also continue through LangGraph's native persistence. The same applies to an ADK agent that remains connected to a durable session service with retained sessions. This lets you keep native framework storage and analytics while users get replay, generative UI, multimodal history, and realtime continuity through CopilotKit.
This is coordinated persistence during future CopilotKit-mediated runs, not general database replication. CopilotKit lifecycle actions such as rename, archive, and delete apply to the Enterprise Intelligence thread; they do not rename, archive, or delete records in the native framework store.
<Callout type="info"> See [LangGraph persistence](/integrations/langgraph/advanced/persistence/message-persistence) for the framework-native (checkpointer) side, and [Importing and synchronizing thread history](/threads-import) for bringing existing framework conversations into the platform store. (LangGraph *Platform* requires thread ids to be UUIDs, which CopilotKit's auto-minted ids already are.) </Callout>MCP Apps render through activity messages (activityType: "mcp-apps"), and that rendering is a client-side concern: the registered renderer resolves the referenced UI resource and runs any follow-up in the browser. So the visible MCP App UI is re-derived on the client from the conversation when it loads. It isn't a separately-stored artifact you persist on its own.
Practically, MCP App UI restores the same way the rest of the conversation does: if your server-side store replays the thread's history (see server-side replay above), the tool-call/activity messages come back and the renderers run again on hydration. There's no separate MCP-Apps store to configure.
@copilotkit/react-core/v2): useThreads returns the platform thread list; threadId lives per-chat (on <CopilotChat> / <CopilotChatConfigurationProvider>), not on the provider; read messages via useAgent().agent.messages and mutate via agent.setMessages(...). No initialMessages.@copilotkit/react-core): useThreads returns { threadId, setThreadId, isThreadIdExplicit } (a single-conversation id manager, not the platform list). Its setThreadId silently no-ops when a threadId prop is present, because the prop shadows internal state. <CopilotKit threadId> sets the id at the provider, and useCopilotChat() exposes setMessages(...) and an initialMessages option. (Separately, the setThreadId on the main <CopilotKit> hooks throws if you call it while threadId is prop-supplied.)useThreads API