@on_task handler over SSE, calls @on_boot once at startup, runs @on_tool_* hooks around every tool invocation, and routes auth events through @on_auth_event.
For the tool-registration decorator, see
@register_tool: it lives next to the tools API, since it constructs Tool objects rather than wiring runtime hooks.
Minimal worker
python worker.py: and the worker subscribes to the platform’s SSE stream, picks up tasks dispatched to its agent, runs them, and saves results. Tasks are auto-marked Completed (or Error on raise) and metrics are reported automatically when task.tokens is set.
Required environment
@on_task validates required env vars eagerly at import. Missing any of these raises ModuleException:
For local development without an org-managed agent, set these to your dev values.
@on_task
@on_task registers a function as the agent’s task executor. The decorated function runs every time the platform dispatches a task to this worker: over SSE in production, and via the embedded HTTP server (POST /invoke on port 59321) for local invocations and Cloud Run-style integrations.
The runtime auto-detects whether your function is a regular handler (returns Task) or a streaming handler (async generator that yields TaskUpdateEvent).
Decorator forms
Required signature
The handler must accept exactly one parameter namedtask. Either positional or keyword form works.
Handler types
The decorator detects which kind you wrote by inspecting whether your function is an async generator.Regular handler
Sync or async, returns the (possibly mutated)Task.
task.asave() yourself unless you need to checkpoint mid-handler.
Streaming handler
Async generator that yieldsTaskUpdateEvents. Used for token-by-token streaming responses.
A streaming handler emits two kinds of events. Each token (or text chunk) from the LLM is yielded as a TaskUpdateEventType.Chunk event, which the platform forwards to subscribers in real time. After the loop ends, the handler yields one final TaskUpdateEventType.TaskFinished event carrying the completed Task object. The platform uses that final event to mark the task complete and persist its result.
POST /invoke only: the SSE listener wraps them in an adapter that consumes the generator and tracks the final Task from the TaskFinished event.
What the runtime does
When you decorate a function with@on_task:
- Validates the signature (must accept
task). - Detects handler type (regular vs streaming).
- Starts an embedded HTTP server on port
59321forPOST /invoke(always, in a daemon thread). Override the port withXPANDER_STREAMING_PORT. - Registers an SSE listener with the
Eventsmodule. The listener:- Reads
XPANDER_API_KEY,XPANDER_ORGANIZATION_ID,XPANDER_AGENT_IDfrom env (raisesModuleExceptionif any are missing). - Subscribes to the platform’s task-dispatch stream.
- Acquires a semaphore (
max_sync_workers=6by default) before dispatching. - Sets task status to
Executingbefore calling your handler. - Calls your handler.
- Persists the returned task; if your handler raised, marks the task
Errorwith the exception string as the result. - Reports metrics if
task.tokensis set. - Re-runs the handler with a continuation prompt if deep planning is enabled (multi-step task plans tracked in
task.deep_planning) and items remain incomplete, up toMAX_PLAN_RETRIES = 5. Pre-retry the runtime triggers session compaction.
- Reads
Test mode
Passtest_task (or use the CLI override) to invoke the handler once locally instead of subscribing to the platform.
With a LocalTaskTest
From the CLI
The decorator also responds to--invoke / --prompt arguments on sys.argv, so you can run any @on_task-decorated module with:
Runtime caveats
- One handler per process. The decorator subscribes to the SSE stream on import. Decorating multiple functions in the same module isn’t useful: the most recently registered handler wins. Use multiple Python processes (or container instances) if you need to host different agents.
- HTTP server starts immediately. The
POST /invokeendpoint is up before the SSE listener finishes connecting. If port59321is in use the runtime logs a warning and continues. Override the port with theXPANDER_STREAMING_PORTenv var. - Synchronous handlers are dispatched on a thread pool. A thread pool of
max_sync_workers=6handles sync handlers concurrently. Async handlers run on the event loop.
Errors raised by the decorator
TypeError: handler doesn’t accepttask, or returns/yields the wrong type.ModuleException: required env vars are missing when the SSE listener starts.
@on_boot / @on_shutdown
@on_boot registers a function to run before the SSE listener subscribes to the platform: useful for warming caches, opening database pools, validating environment, or pre-fetching data. @on_shutdown registers a function to run during graceful shutdown.
Decorator forms
Lifecycle ordering
@on_boot handler raises, the worker fails to start: the exception propagates and the process exits. If a @on_shutdown handler raises, the error is logged but the runtime continues with the next shutdown handler.
Examples
Open a DB pool at boot, close on shutdown
Validate env at boot
@on_boot aborts startup before the worker takes any tasks: useful for fail-fast environment validation.
Multiple handlers
1 then 2. The runtime awaits each one before moving on.
Sync and async mix
Notes
- Boot/shutdown handlers are class-level on
Events. They register globally per process: there’s no scoping to a specificEventsinstance. This is fine for a one-process-one-agent model. - Shutdown handlers run after the SSE listener is stopped but before final cleanup. By then the process is no longer accepting new tasks; in-flight tasks are cancelled.
@on_tool_before / @on_tool_after / @on_tool_error
@on_tool_before, @on_tool_after, and @on_tool_error register hooks that fire around tool invocations: useful for logging, validation, caching, alerting, or analytics. The hooks run during every Tool.ainvoke (which means around every framework-driven tool call too).
Required signatures
Decorator forms
@on_tool_after and @on_tool_error. The optional configuration parameter is reserved for future use; hooks don’t currently read it.
Examples
Log every tool call
Block invocations matching a payload pattern
Hooks can’t cancel invocations directly (the runtime catches their exceptions and continues), but you can mutate state or short-circuit by raising a guard before the call:@register_tool function bodies.
Cache successful results
Alert on failures
Notes
- Hooks fire once per tool invocation, regardless of whether the tool is local or remote.
- Exceptions in hooks are caught and logged via
loguru: they never crash the invocation pipeline. - Hooks are class-level on
ToolHooksRegistry(process-wide singleton). There’s no scoping to a specific repository. - The auto-emitted
ToolCallRequest/ToolCallResultevents ontask.aevents()are independent of these hooks. The hooks fire even whenreport_activity=FalseinTool.ainvoke.
@on_auth_event
@on_auth_event registers a handler that fires whenever the agent runtime emits an authentication event: for example, when an MCP server requires the end-user to log in via OAuth. Use it to surface the OAuth URL to your UI, kick off external auth flows, or log auth attempts.
Backend.aget_args(...) call routes auth events through registered handlers automatically.
Required signature
The function must accept exactly three parameters:agent, task, event. Names are flexible; arity matters. Sync or async are both accepted.
event.type is always "auth_event" for handlers registered with this decorator.
Event payload shapes
event.data is one of the MCP OAuth response variants. Switch on event.data.type:
Examples
Show the OAuth URL to the user
Multiple handlers stack
Combine with a per-call callback
You can pass an additional one-off callback toBackend.aget_args(auth_events_callback=...): it runs in addition to every globally-registered handler.
global_handler and per_call fire when an auth event happens during the resulting agent’s run.
Helper functions
The module also exposes two helpers for testing and management:xpander_sdk package: import from the decorator module directly.
