Run Claude and Codex agents in the background and cancel them mid-turn
October 12, 20262 min read
When a ClaudeAgent or CodexAgent run arrives with background=True, Agno's AgentOS writes a PENDING run, answers 202 at once and hands the SDK turn to a worker. From there the run moves to RUNNING and ends as COMPLETED, ERROR or CANCELLED on the same background task, worker slot and event-stream code that native agents use.
Picture a coding agent asked to refactor a large module. The request should return right away, the UI should show progress, and a user who changes their mind should be able to stop it halfway through. On the server, the agent needs a database, and the durable queue makes each accepted run a committed job:
from agno.agents.claude import ClaudeAgent
from agno.db.postgres import PostgresDb
from agno.os import AgentOS, QueueConfig
db = PostgresDb(db_url="postgresql+psycopg://ai:ai@localhost:5532/ai")
agent = ClaudeAgent(id="claude", name="Claude", db=db)
agent_os = AgentOS(agents=[agent], db=db, queue=QueueConfig(durable=True))
app = agent_os.get_app()A client then sends background=true and stream=false with the run, receives its run_id and session_id, polls GET /agents/claude/runs/{run_id}?session_id={session_id}, and stops it with POST /agents/claude/runs/{run_id}/cancel. In Python, await agent.arun(..., background=True) returns the PENDING run and await agent.acancel_run(run_id) stops it.

Cancelling a turn that is already running
Cancellation reaches the SDK itself. Each Claude run gets its own ClaudeSDKClient, and CodexAgent keeps the handle of the Codex turn, so the process that owns the run can call interrupt() on it. A cancel request stores the intent first, and the owning process acts on it, whether the call came through that process or through the shared cancellation backend.
We served a ClaudeAgent from AgentOS with the durable queue on an embedded Postgres. No Claude login was available, so a stand-in claude_agent_sdk client streamed one text block and then waited until it was interrupted. Polling, cancelling and polling again printed this (real output):
POST /agents/claude/runs background=true -> 202 {"run_id":"16eef695-8c75-48a0-9687-be689c355c1d","session_id":"s1","status":"PENDING"}
GET run -> PENDING
GET run -> 200 RUNNING
POST cancel -> 200 {}
GET run -> 200 CANCELLED | 'Run cancelled'
SDK calls: ['connect', 'query', 'interrupt', 'disconnect']The last line is the order of calls the adapter made on the SDK client: it connected, sent the prompt, received the interrupt and disconnected. Codex works the same way through its turn handle, and an interrupted Codex turn is stored as CANCELLED.
Streaming and reconnecting
Send stream=true together with background=true and the response is an SSE stream in which every event carries an event_index. If the client drops, POST /agents/{agent_id}/runs/{run_id}/resume with the last event_index it saw sends the missed events and then continues live. Closing the stream does not cancel the run, which keeps going until it finishes or someone calls /cancel.
A background run needs a database on the agent, and arun raises a ValueError without one. Running more than one replica needs a shared event stream and cancellation backend, which QueueConfig(redis=...) sets up.
See the cookbook, and learn more about background execution in the documentation.
Frequently asked questions
Give the agent a database and pass background=True to arun, or send background=true to POST /agents/{agent_id}/runs on AgentOS. The call returns a PENDING run with a run_id right away, and you poll that run until it reaches COMPLETED, ERROR or CANCELLED.
Call POST /agents/{agent_id}/runs/{run_id}/cancel, or agent.acancel_run(run_id) in Python. Agno records the cancel intent, the process that owns the run calls interrupt() on the live Claude client or Codex turn handle, and the run is stored as CANCELLED.
Yes. Submit with stream=true and background=true, keep the event_index of the last event you received, and call POST /agents/{agent_id}/runs/{run_id}/resume with last_event_index and session_id to pick up the events you missed.
Yes. With QueueConfig(durable=True) and a Postgres or Redis store, the submission becomes a committed job row before the 202, and a queue worker runs the turn with the same background, slot and event-stream code that native agents use.


