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.- The user types a long task.
- The agent spawns the subagent, replies “Started; I’ll ping you when done”, and ends the turn.
- The user does other things in the chat (or walks away).
- 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:- The host did not pass its own
on_job_completetocreate_subagent_tool(...)(an explicit callback always wins). deliver="origin"— any other token (e.g."telegram:123","all") stays pull-only unless the host passeson_job_complete.- The spawning turn has an outbound messenger registered (a running gateway /
BotOS). - The active send policy does not deny
"origin"(the same checksend_messageuses). - The target is the turn’s own chat — any explicit
chat_id/platform/thread_idis empty or equal to the current turn’s.
info.delivered stays False and info.origin["chat_id"] is "".
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 byBackgroundJobManager. 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_subagentcaptures 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 passeson_job_completetocreate_subagent_tool(...)replaces this.HookEvent.JOB_COMPLETEDfires when a job reaches a terminal state. It is observational — it is not the chat delivery path.
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
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
TheSUBAGENT_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.
Best Practices
Don't background trivially fast tasks
Don't background trivially fast tasks
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.Always capture job IDs
Always capture job IDs
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.
Set a concurrency budget
Set a concurrency budget
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.Use wait=True for final collection
Use wait=True for final collection
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.Use deliver='origin' instead of polling for chat bots
Use deliver='origin' instead of polling for chat bots
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.Failure delivery behaviour
Failure delivery behaviour
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.Related
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

