Programmatic Control
Drive agent runs directly from code — no chat UI required.
What is this?#
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 withcopilotkit.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 withagent.runAgent({ forwardedProps: { command: { resume, interruptEvent } } })to drive interrupt resolution from arbitrary UI.
Every example on this page is pulled from two live cells:
headless-complete (full chat surface, shown here for the message-send
path) and interrupt-headless (button-driven interrupt resolver, shown
here for the subscribe + resume path).
When should I use this?#
Use programmatic control when you want to:
- Trigger agent runs from buttons, forms, or other UI elements
- Execute specific tools directly from UI interactions (without an LLM turn)
- Build agent features without a chat window
- Access agent state and results programmatically
- Create fully custom agent-driven workflows
Sending a message from code#
The message-send path in headless-complete is the canonical pattern:
append a user message with agent.addMessage, then call
copilotkit.runAgent({ agent }). The same handleStop calls
copilotkit.stopAgent({ agent }) to cancel mid-run. Note the
connectAgent effect at the top, which opens the backend session on
mount so the very first runAgent doesn't race the handshake.
import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
CopilotKitProvider,
CopilotChatConfigurationProvider,
useAgent,
useCopilotKit,
} from "@copilotkit/react-core/v2";
import type { Message } from "@ag-ui/core";
import { MessageList } from "./message-list";
import { InputBar } from "./input-bar";
import { useHeadlessCompleteToolRenderers } from "./tool-renderers";
const AGENT_ID = "default";
// Outer wrapper — provides the CopilotKit runtime + page layout. Built-in-agent
// uses a single in-process /api/copilotkit endpoint with the `default` agent.
export default function HeadlessCompleteDemo() {
return (
<CopilotKitProvider runtimeUrl="/api/copilotkit" useSingleEndpoint>
<div className="flex justify-center items-center h-screen w-full bg-gray-50">
<div className="h-full w-full max-w-3xl flex flex-col bg-white shadow-sm">
<header className="px-4 py-3 border-b border-gray-200">
<h1 className="text-base font-semibold">
Headless Chat (Complete)
</h1>
<p className="text-xs text-gray-500">
Built from scratch on useAgent — no CopilotChat.
</p>
</header>
<Chat />
</div>
</div>
</CopilotKitProvider>
);
}
// Inner view — the actual chat. Reads messages + isRunning straight off the
// agent, wires up the connect/run/stop lifecycle, and hands the pure
// presentational pieces their props.
function Chat() {
const threadId = useMemo(() => crypto.randomUUID(), []);
const { agent } = useAgent({ agentId: AGENT_ID, threadId });
const { copilotkit } = useCopilotKit();
// Connect the agent on mount so the backend session is live before the first
// send. Mirrors the internal connect effect used by CopilotChat (abort on
// unmount to play nice with React StrictMode).
useEffect(() => {
const ac = new AbortController();
if ("abortController" in agent) {
(
agent as unknown as { abortController: AbortController }
).abortController = ac;
}
copilotkit.connectAgent({ agent }).catch(() => {
// connectAgent emits via the subscriber system; swallow here to avoid
// unhandled-rejection noise on unmount.
});
return () => {
ac.abort();
void agent.detachActiveRun().catch(() => {});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [agent, threadId]);
const [input, setInput] = useState("");
const messages = agent.messages as Message[];
const isRunning = agent.isRunning;
const handleSubmit = useCallback(
async (override?: string) => {
const text = (override ?? input).trim();
if (!text || isRunning) return;
setInput("");
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content: text,
});
try {
await copilotkit.runAgent({ agent });
} catch (err) {
console.error("headless-complete: runAgent failed", err);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[agent, input, isRunning],
);
const handleStop = useCallback(() => {
try {
copilotkit.stopAgent({ agent });
} catch (err) {
console.error("headless-complete: stopAgent failed", err);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [agent]);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; the canonical example is resuming from an interrupt withforwardedProps.command.
Subscribing to agent events#
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 LangGraph interrupt(...) events and resume with a payload (the pattern below).
Resolving a pause from a button#
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 (Microsoft Agent Framework). 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.
See also#
- Headless UI — the full
useRenderedMessagescomposition that mirrors<CopilotChatMessageView>line-for-line. - Human-in-the-Loop — the
useHumanInTheLoopanduseInterrupthooks with their render-prop contracts, for the "paused mid-chat" pattern this page's headless variant replaces.
