showcase/shell-docs/src/content/docs/programmatic-control.mdx
Programmatic control is what you reach for when you want to drive an agent run from code rather than from a chat composer: a button, a form, a cron job, a keyboard shortcut, a graph callback. CopilotKit exposes three primitives that cover every triggering pattern:
agent.addMessage(...) — append a message to the conversation without running the agent. Pair with copilotkit.runAgent({ agent }) when you want the appended message to kick off a turn.copilotkit.runAgent({ agent }) — the same entry point <CopilotChat /> calls under the hood. Orchestrates frontend tools, follow-up runs, and the subscriber lifecycle.agent.subscribe(subscriber) — low-level AG-UI event subscription (onCustomEvent, onRunStartedEvent, onRunFinalized, onRunFailed, …). Pairs with copilotkit.runAgent({ agent, forwardedProps: { command: { resume, interruptEvent } } }) to drive interrupt resolution from arbitrary UI.The send-and-stop example below is intentionally self-contained. The
later subscription and interrupt examples are pulled from the live
interrupt-headless cell.
Use programmatic control when you want to:
The canonical pattern is to append a user message with
agent.addMessage, then call copilotkit.runAgent({ agent }). Use
copilotkit.stopAgent({ agent }) to cancel an in-flight run.
import { useAgent, useCopilotKit } from "@copilotkit/react-core/v2";
export function AgentTrigger({ agentId }: { agentId: string }) {
const { agent } = useAgent({ agentId });
const { copilotkit } = useCopilotKit();
const run = async () => {
if (agent.isRunning) return;
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content: "Summarize the latest sales data",
});
try {
await copilotkit.runAgent({ agent });
} catch (error) {
console.error("CopilotKit runAgent failed:", error);
}
};
return (
<>
<button onClick={run} disabled={agent.isRunning}>
Run agent
</button>
<button
onClick={() => copilotkit.stopAgent({ agent })}
disabled={!agent.isRunning}
>
Stop
</button>
</>
);
}
copilotkit.runAgent() vs agent.runAgent()Both methods trigger the agent, but they operate at different levels:
copilotkit.runAgent({ agent }) — the recommended default. Orchestrates the full lifecycle: executes frontend tools, handles follow-up runs, and routes errors through the subscriber system.agent.runAgent(options) — low-level method on the agent instance. Sends the request to the runtime but does not execute frontend tools or chain follow-ups. Reach for this only when you need direct control. (For the interrupt-resume case, use copilotkit.runAgent({ agent, forwardedProps: { command: { resume, interruptEvent } } }) — see the snippet below — so the subscriber lifecycle still wraps the resumed run.)agent.subscribe(subscriber) returns { unsubscribe }. The subscriber
object accepts every AG-UI lifecycle callback: onCustomEvent,
onRunStartedEvent, onRunFinalized, onRunFailed, and the streaming
deltas. Use it to drive custom progress UI, forward events to
analytics, or catch framework pause/resume events and resolve them with
a payload (the pattern below).
The interrupt-headless cell demonstrates the full pattern without
useInterrupt or a chat surface. A plain hook subscribes to
on_interrupt custom events, buffers the payload until the run
finalizes (so the UI doesn't flash mid-stream), and exposes a
resolve(response) callback that calls copilotkit.runAgent({ agent, forwardedProps: { command: { resume, interruptEvent } } }) to unblock
the graph:
The resulting { pending, resolve } tuple is pure data; any UI can
drive it. The cell itself renders a simple button grid, but the same
hook would power a modal, a toast, a sidebar form, or a voice UI.
For promise-based integrations there is no native interrupt primitive —
the demo uses useFrontendTool with a Promise-based handler instead.
The handler stages its resolve callback and pending payload via React
state, the app surface renders the picker outside the chat, and the
user's pick resolves the Promise that the agent's tool call is awaiting.
Same UX, different mechanism — the agent never knows it's talking to a
button grid instead of a chat picker:
The resulting { pending, resolveActive } pair is pure data; any UI
can drive it. The cell itself renders a simple button grid, but the
same pattern would power a modal, a toast, a sidebar form, or a voice
UI.
</WhenFrameworkHas>Interrupt-style pause/resume isn't available on this framework. The headless interrupt pattern shown above requires the underlying runtime to expose either a native
interrupt(...)primitive (LangGraph) or a Promise-resolving frontend-tool path. For all other integrations, drive pauses throughuseHumanInTheLoopinstead — it's the standard hook for tool-call-based pause/resume flows and works on every framework that supports tool calls. Theagent.addMessage,copilotkit.runAgent, andagent.subscribeprimitives above still apply — only the interrupt-resolution path is framework-specific.
useRenderedMessages composition
that mirrors <CopilotChatMessageView> line-for-line.useHumanInTheLoop and
useInterrupt hooks with their render-prop contracts, for the
"paused mid-chat" pattern this page's headless variant replaces.