Async Jobs
A /chat request runs the agent inside the HTTP request and returns when it's
done. That's fine for quick calls, but an agent that researches, calls tools,
and chains for several minutes will outlast client read timeouts, load-balancer
idle limits, and flaky connections.
The job system solves this. You submit an agent run, it executes in the background as a durable record, and you stream or poll its updates later — surviving timeouts, disconnects, and reconnects.
Enabling jobs
Jobs are opt-in. Turn them on in code:
or in agents.toml:
[jobs]
enabled = true
store = ".pyagentic/jobs.db" # SQLite path; ":memory:" for ephemeral
admission_cap = 16 # max jobs in-flight at once
max_concurrency = 8 # max concurrent agent runs
ttl = "24h" # how long terminal job records are kept
cleanup_interval_seconds = 300
This mounts a /jobs API per agent (under each agent's prefix in a multi-agent
app) and persists job records and their update logs to SQLite, so they survive a
server restart.
The flow
import httpx
# 1. Submit — returns immediately with a job id (202)
r = httpx.post("http://localhost:8000/jobs",
json={"input": {"user_input": "Research AI safety"}})
job_id = r.json()["job_id"]
# 2. Stream updates as the agent works (resumable — see below)
with httpx.stream("GET", f"http://localhost:8000/jobs/{job_id}/stream") as s:
for line in s.iter_lines():
print(line)
# 3. Or just fetch the final result when you're ready
result = httpx.get(f"http://localhost:8000/jobs/{job_id}").json()
Endpoints
| Route | Purpose |
|---|---|
POST /jobs |
Submit a run. Body {"input": {<chat fields>}, "session_id": <optional>, "construct": <optional>}. Returns 202 {job_id, status}. |
GET /jobs |
List jobs, newest first. Filter with ?status= and ?session_id=. |
GET /jobs/{id} |
Job status and, when terminal, its result. |
GET /jobs/{id}/updates?since=<seq> |
The update log past a sequence cursor. |
GET /jobs/{id}/stream |
SSE stream of updates: replay from cursor, then live tail. |
POST /jobs/{id}/cancel |
Cancel a queued or running job. |
The input object is your agent's normal chat request body. Pass a
session_id (from POST /sessions) to run the job against an existing session;
omit it for a one-off run.
For a one-off run, add a construct object — the same construction data as
POST /sessions (state and any nested linked agents) — to build a fresh agent
for the job. Its dependencies are injected
server-side, just like for sessions. Session-bound jobs reuse the session's
existing agent and ignore construct.
# One-off job that builds its own agent from a construct payload
httpx.post("http://localhost:8000/jobs", json={
"input": {"user_input": "Summarize the latest results"},
"construct": {"topic": {"name": "kv-cache"}},
})
Surviving timeouts: replay-from-cursor
Every update carries a monotonic, gapless sequence number (seq), emitted on the
SSE stream as the frame id. If the connection drops, reconnect with the
Last-Event-ID header (or ?since=<seq>) and the stream replays only what
you missed, then resumes the live tail:
# Reconnect after seq 1 — replays seq 2 onward, then tails live
curl -N http://localhost:8000/jobs/{id}/stream \
-H "Last-Event-ID: 1"
The stream closes after a terminal event (agent_response, job_failed, or
job_cancelled). Because the store is the single source of truth, a client can
disconnect for any reason and pick up exactly where it left off — the agent run
keeps going server-side regardless.
How it works
- Durable store — a
JobStore(SQLite by default) holds each job record and its append-only update log. Restarting the server keeps finished jobs and their logs; an in-flightrunningjob is marked failed on restart (execution is not resumed), andqueuedjobs are re-dispatched. - In-process backend — runs
agent.step()in the server's event loop and emits each update. Session-bound jobs run on the session's live agent; the orchestrator serializes a session's jobs FIFO (one in-flight per session). - Admission cap — a single process-wide cap bounds concurrent runs; jobs
beyond it stay
queueduntil a slot frees.
Next steps
- Deploy your app — set a persistent
[jobs].storepath in the container - Revisit the synchronous chat endpoints for quick calls