Skip to main content
A parent agent can fire off one or more subagents and continue working. Each background subagent returns a job_id immediately — results can be pulled on demand or delivered automatically back to the originating chat.
Every background subagent still emits HookEvent.SUBAGENT_STOP on completion (success or failure) — same as a synchronous spawn. See Observability below.
How a user experiences it in a bot chat:
  1. The user types a long task.
  2. The agent spawns the subagent, replies “Started; I’ll ping you when done”, and ends the turn.
  3. The user does other things in the chat (or walks away).
  4. When the subagent finishes, the gateway posts the result as a new message in the same chat — no new prompt needed.

Quick Start

1

Spawn a background subagent

Call spawn_subagent with background=True. The parent gets a job_id and keeps running:
2

Deliver result back to chat automatically

Ask for deliver="origin" when the agent runs behind a gateway (BotOS) — the finished result is sent back to the same chat without polling:
Auto-delivery needs a running gateway: the origin chat is filled from the turn’s session context. In a bare Agent (no gateway) the job stays pull-only — collect it with subagent_result. See When auto-delivery kicks in.
3

Fan out and collect all results

Spawn multiple subagents in parallel and wait for all of them:
4

Check status without blocking

Poll a job without waiting — useful when you want to check periodically:

Chat-Back Delivery Flow

When running inside a gateway (BotOS), a background job delivers itself back to the originating chat when it finishes — no polling required. The result goes through the same outbound messenger and DeliveryRouter path that send_message uses — no extra configuration beyond deliver="origin". The model never picks the chat. At spawn time the origin (platform, chat_id, thread_id, user_id) is filled from the turn’s session context. The captured user_id is also what scopes /tasks to the caller’s own jobs.

When auto-delivery kicks in

The result is sent back automatically only when all of these hold:
  1. The host did not pass its own on_job_complete to create_subagent_tool(...) (an explicit callback always wins).
  2. deliver="origin" — any other token (e.g. "telegram:123", "all") stays pull-only unless the host passes on_job_complete.
  3. The spawning turn has an outbound messenger registered (a running gateway / BotOS).
  4. The active send policy does not deny "origin" (the same check send_message uses).
  5. The target is the turn’s own chat — any explicit chat_id / platform / thread_id is empty or equal to the current turn’s.
Without a gateway the job is pull-only: info.delivered stays False and info.origin["chat_id"] is "".
Not restart-safe yet. Jobs are persisted, but the gateway does not call reconcile_on_start() for background-subagent jobs, so a job that finishes while the gateway is down is not re-delivered when it comes back. See Durable Background Jobs.
Cancelling a background job after it has started does not deliver a late result or fire JOB_COMPLETED — the job ends in CANCELLED and the chat stays quiet.

How It Works (Pull Mode)

Background subagents run on a shared thread pool managed by BackgroundJobManager. Job IDs are 8-character random strings. Results are kept in memory until retrieved.

How Delivery is Wired

Core layer (praisonaiagents):
  • BackgroundJobManager.start_job(..., on_complete=None, origin=None) — accepts an optional completion callback.
  • spawn_subagent captures the origin into the job. When the auto-delivery conditions hold, it installs a per-turn callback that sends the result through the turn’s outbound messenger. A host that passes on_job_complete to create_subagent_tool(...) replaces this.
  • HookEvent.JOB_COMPLETED fires when a job reaches a terminal state. It is observational — it is not the chat delivery path.
Wrapper layer (praisonai_bot):
  • Each gateway turn registers its outbound messenger and its event loop (register_gateway_loop), so the background worker can send after the turn has ended.

Delivery Tokens

Failure delivery: On failure, the chat receives Background task failed: <error> instead of the result; an empty result is sent as Background task finished with no output. If the messenger rejects the send, the job stays delivered=False — it is never marked delivered for a message that didn’t go out. Hook handlers are best-effort — a raised exception is swallowed and logged at DEBUG so the worker never crashes.

Subscribe to JOB_COMPLETED

For observability or custom side effects when a background job finishes, register on HookEvent.JOB_COMPLETED (see Hook Events):

Tool Parameters

spawn_subagent

The model cannot set origin fields. The model-visible schema has only task, agent_name, context, tools, llm, permission_mode, background and deliver; platform, chat_id, thread_id and session_id are Python-only parameters, injected from the turn’s session context at spawn time. Explicit callers (Python code) can still override them. Returns (foreground): Full result dict with output, success, error. Returns (background): {"success": True, "job_id": "...", "status": "running"}.

subagent_result

Returns:
  • Job still running (wait=False): {"success": True, "job_id": "...", "status": "running"}
  • Job done: {"success": True, "job_id": "...", "status": "completed", "result": {...}}
  • Job failed: {"success": False, "job_id": "...", "error": "..."}

Backward compatibility: With no deliver target, behaviour is byte-for-byte as before — pull-only via subagent_result. The delivery path is purely additive.

Observability

The SUBAGENT_STOP hook fires from the same completion chokepoint for background jobs as for synchronous spawns, so you do not need to poll subagent_result just to observe completion. Register a hook and let the runtime call you.
See Hook Events → Subagent Events for the payload reference.

Best Practices

Background subagents have overhead (thread scheduling, job management). For tasks that complete in under a second, use foreground subagents (background=False, the default) — they’re simpler and return results immediately.
The parent agent should record job IDs in its working memory or output immediately after spawning. If a job ID is lost, there is no way to retrieve the result.
The BackgroundJobManager runs on a thread pool. Avoid spawning hundreds of concurrent subagents without throttling — each subagent holds a connection and context window while running.
When you need a result to proceed, always use subagent_result(job_id, wait=True) rather than polling in a loop. The wait=True mode blocks the calling thread efficiently without spinning.
In a bot context, deliver='origin' is simpler than polling: the parent can close its turn immediately and the result arrives as a new message when ready — no held connections.
On failure, Background task failed: <error> is delivered to the same chat instead of the result. If the messenger rejects the send, the job stays delivered=False. The JOB_COMPLETED hook fires for both terminal states (COMPLETED and FAILED), and handler errors never crash the worker.

Named Subagents

Delegate to your own named agents by name mid-run — no Python

Hook Events

Subscribe to JOB_COMPLETED and other lifecycle events

Background Tasks

Other background task patterns for long-running work

Proactive Delivery

Push messages to users outside of a turn

Channels Gateway

Configure BotOS for multi-platform bot deployments

Gateway Reliability

Production reliability presets for the gateway