Deferred execution
Queue long-running executions, get an immediate tracking ID, and finish with polling or webhooks
Fire an execution without waiting for the result. Come back later -- or let a webhook tell you when it finishes.
Request/response is the wrong abstraction for many AI workloads. Model latency varies, multi-agent chains compound it, and long-running work should not tie up an HTTP request.
This page covers deferred execution: POST /api/v1/execute/async/{target} queues work and returns immediately. Synchronous execution still exists at POST /api/v1/execute/{target} when you want the result inline.
AgentField's deferred execution model pushes work to a durable queue, returns a tracking ID immediately, and lets you finish with polling, batch status, or a webhook.
import asyncio
from agentfield import Agent
app = Agent(node_id="orchestrator", version="1.0.0")
@app.reasoner()
async def generate_quarterly_report(quarter: str) -> dict:
# Fire three analyses in parallel — async execution is automatic
financial, market, customer = await asyncio.gather(
app.call("finance.deep_analysis", quarter=quarter),
app.call("research.market_trends", quarter=quarter),
app.call("analytics.churn_report", quarter=quarter),
)
return {"sections": [financial, market, customer]}What just happened
- Three long-running sub-tasks were queued immediately instead of blocking the request
- The caller got stable tracking IDs for later inspection
- Completion could be handled by polling, batch waiting, or webhook delivery
Example lifecycle:
{ "execution_id": "exec_a1b2c3", "status": "queued" }
{ "execution_id": "exec_a1b2c3", "status": "running" }
{ "execution_id": "exec_a1b2c3", "status": "succeeded", "duration_ms": 14200 }
Session tool calls
When a session invokes a tool — either via session.call(...) in the handler or via a tools=[...] allowlist call from the live realtime model — the control plane forwards the work to execute/async with X-Session-ID and X-Parent-Execution-ID attached. The resulting execution belongs to the session and appears in its workflow DAG alongside any other reasoner work. See Workflow tracing.
What you get
- Fire-and-forget dispatch --
POST /api/v1/execute/async/{target}returns202 Acceptedimmediately with a tracking ID - Durable PostgreSQL queue -- executions survive control plane restarts; nothing is lost
- Polling and batch status -- check one execution or hundreds at once
- Fair scheduling -- a configurable worker pool prevents any single agent from starving others
- No timeout ceiling -- async executions run until they complete, fail, or are cancelled
- Webhook delivery -- optional completion webhook fires when the execution reaches a terminal state
Full SDK examples
import asyncio
from agentfield import Agent
app = Agent(node_id="orchestrator", version="1.0.0")
@app.reasoner()
async def orchestrate(task: str) -> dict:
# app.call() uses async execution automatically when connected to the control plane.
# It submits the execution, polls for results, and returns the final output.
result = await app.call("researcher.deep_dive", topic=task)
return result
app.run()Execution lifecycle
Every execution moves through a defined set of states:
| Status | Description |
|---|---|
pending | Created but not yet enqueued |
queued | Sitting in the durable PostgreSQL queue |
running | Dispatched to the target agent and executing |
waiting | Paused for human approval (see Human-in-the-Loop) |
paused | Temporarily paused via API; can be resumed |
succeeded | Completed successfully with a result |
failed | Completed with an error |
timeout | Exceeded the configured timeout |
cancelled | Explicitly cancelled via API |
Worker pool architecture
The control plane runs a configurable worker pool for async executions. When POST /execute/async/{target} arrives:
- An execution record is persisted to PostgreSQL (durable -- survives restarts)
- The execution is enqueued to the in-memory worker pool
- A worker picks up the job and forwards the request to the target agent node
- The agent processes the request and posts a status update back to the control plane
- If a webhook was registered, the webhook dispatcher fires the completion notification
The pool size, queue depth, and agent call timeout are all configurable:
# agentfield.yaml
agentfield:
execution_queue:
agent_call_timeout: 90s # default: 1800s (30 minutes)Patterns
Fan-Out / Fan-In
Fire multiple executions in parallel, then collect results:
import asyncio
@app.reasoner()
async def analyze_portfolio(stocks: list[str]) -> dict:
# Fan out — app.call() handles async execution and polling internally
tasks = [
app.call("stock-analyzer.analyze", symbol=symbol)
for symbol in stocks
]
# Fan in — collect all results
results = await asyncio.gather(*tasks)
return {symbol: result for symbol, result in zip(stocks, results)}Webhook Instead of Polling
Register a webhook at submission time to avoid polling entirely:
{
"input": { "document": "..." },
"webhook": {
"url": "https://your-api.com/hooks/execution-done",
"secret": "whsec_your_hmac_secret",
"headers": { "Authorization": "Bearer tok_..." }
}
}The control plane will POST to that URL with HMAC-SHA256 signature verification when the execution reaches a terminal state. See Webhooks for details.
AsyncConfig
Configure async execution behavior in agentfield.yaml under execution_queue.
Configuration Fields
| Field | Type | Default | Description |
|---|---|---|---|
agent_call_timeout | duration | 1800s | Timeout for the control plane's call to the target agent |
webhook_timeout | duration | 10s | Timeout for webhook delivery requests |
webhook_max_attempts | int | 3 | Maximum number of webhook delivery attempts |
webhook_retry_backoff | duration | 1s | Initial backoff between webhook retry attempts |
webhook_max_retry_backoff | duration | 5s | Maximum backoff between webhook retry attempts |
# agentfield.yaml
agentfield:
execution_queue:
agent_call_timeout: 1800s
webhook_timeout: 10s
webhook_max_attempts: 3
webhook_retry_backoff: 1s
webhook_max_retry_backoff: 5sQueue Management Methods
Beyond polling and batch status, the SDK provides methods for managing the async queue.
from agentfield import Agent
app = Agent(node_id="orchestrator", version="1.0.0")
# Cancel a running or queued execution
await app.client.cancel_async_execution("exec_a1b2c3")
# List async executions with optional status filter
executions = await app.client.list_async_executions(
status_filter="running", # filter by status
limit=50,
)
for e in executions:
print(f"{e['execution_id']} -> {e['status']} ({e['target']})")
# Get queue health metrics
metrics = await app.client.get_async_execution_metrics()
print(metrics)API reference
Submit Async Execution
POST /api/v1/execute/async/{target}
The {target} is the agent's function identifier in node_id.reasoner_id or node_id.skill_id format.
Request body:
{
"input": { "topic": "quantum computing" },
"context": { "session_id": "sess_abc" },
"webhook": {
"url": "https://example.com/hooks/complete",
"secret": "whsec_...",
"headers": { "X-Custom": "value" }
}
}Response (202 Accepted):
{
"execution_id": "exec_a1b2c3",
"run_id": "run_x7y8z9",
"workflow_id": "run_x7y8z9",
"status": "queued",
"target": "researcher.deep_dive",
"type": "reasoner",
"created_at": "2026-03-23T10:00:00Z",
"enqueued_at": "2026-03-23T10:00:00Z",
"webhook_registered": true
}Poll Execution Status
GET /api/v1/executions/{execution_id}
Response:
{
"execution_id": "exec_a1b2c3",
"run_id": "run_x7y8z9",
"status": "succeeded",
"result": { "summary": "..." },
"started_at": "2026-03-23T10:00:01Z",
"completed_at": "2026-03-23T10:00:15Z",
"duration_ms": 14200,
"webhook_registered": true
}Batch Status
Fetch up to hundreds of execution statuses in a single round trip.
POST /api/v1/executions/batch-status
Request body:
{
"execution_ids": ["exec_a1b2c3", "exec_d4e5f6", "exec_g7h8i9"]
}Response: A map keyed by execution ID, each value matching the single-status shape above.
Cancel Execution
POST /api/v1/executions/{execution_id}/cancel
Transitions a running or queued execution to cancelled.