---
title: Programmatic Control
icon: "lucide/Terminal"
description: Drive agent runs directly from code — no chat UI required.
hideTOC: true
snippet_cell: headless-complete
---

## 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 `agent.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.

<Snippet region="page-send-message" title="frontend/src/app/page.tsx — connect, send, stop" />

### `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 with `forwardedProps.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).

<WhenFrameworkHas flag="interrupt_pattern" equals="native">

## Resolving a LangGraph interrupt from a button

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:

<Snippet region="headless-useinterrupt-primitives" cell="interrupt-headless" title="frontend/src/app/page.tsx — subscribe + resume" />

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.

</WhenFrameworkHas>

<WhenFrameworkHas flag="interrupt_pattern" equals="promise-based">

## Resolving an MS Agent tool call from a button

On Microsoft Agent Framework there's 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:

<Snippet region="headless-promise-primitives" cell="interrupt-headless" title="frontend/src/app/page.tsx — useFrontendTool + Promise resolver" />

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>

<WhenFrameworkHas flag="interrupt_pattern" absent>

## 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`](./human-in-the-loop) 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.

</WhenFrameworkHas>

## See also

- [Headless UI](./headless) — the full `useRenderedMessages` composition
  that mirrors `<CopilotChatMessageView>` line-for-line.
- [Human-in-the-Loop](./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.

<IntegrationGrid path="programmatic-control" />
