CopilotKit

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 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.

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.

chat.tsx
  const { agent } = useAgent({ agentId });  const { copilotkit } = useCopilotKit();  const {    attachments,    fileInputRef,    containerRef,    handleFileUpload,    handleDragOver,    handleDragLeave,    handleDrop,    dragOver,    removeAttachment,    consumeAttachments,  } = useAttachmentsConfig();  const [input, setInput] = useState("");  const messages = agent.messages;  const { listRef, bottomRef, stickRef } = useAutoScroll(    messages,    agent.isRunning,  );  // Send pipeline: consume any ready attachments at submit time, build  // the multimodal `content` array if needed, then dispatch the run.  const sendText = useCallback(    (text: string) => {      const trimmed = text.trim();      // Consume queued uploads first so they get sent even if the user      // didn't type any text alongside them.      const ready = consumeAttachments();      if (!trimmed && ready.length === 0) return;      if (agent.isRunning) return;      stickRef.current = true;      const content = buildContent(trimmed, ready);      agent.addMessage({        id: crypto.randomUUID(),        role: "user",        content,      });      void copilotkit        .runAgent({ agent })        .catch((err) =>          console.error("[headless-complete] runAgent failed", err),        );    },    [agent, copilotkit, consumeAttachments],  );  const handleSend = useCallback(() => {    sendText(input);    setInput("");  }, [input, sendText]);  const handleSuggestion = useCallback(    (text: string) => {      sendText(text);    },    [sendText],  );  const handleReset = useCallback(() => {    if (agent.isRunning) {      try {        agent.abortRun();      } catch {        // no-op: some transports don't support abort      }    }    agent.setMessages([]);    setInput("");    stickRef.current = true;  }, [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. (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.)

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 through useHumanInTheLoop instead — it's the standard hook for tool-call-based pause/resume flows and works on every framework that supports tool calls. The agent.addMessage, copilotkit.runAgent, and agent.subscribe primitives above still apply — only the interrupt-resolution path is framework-specific.

See also#

  • Headless UI — the full useRenderedMessages composition that mirrors <CopilotChatMessageView> line-for-line.
  • Human-in-the-Loop — the useHumanInTheLoop and useInterrupt hooks with their render-prop contracts, for the "paused mid-chat" pattern this page's headless variant replaces.