@on_task handler over SSE, calls @on_boot once at startup, runs @on_tool_* hooks around every skill invocation, and routes auth events through @on_auth_event. In the SDK a skill is a Tool, which is where the hook names come from.
For the skill-registration decorator, see
@register_tool: it lives next to the ToolsRepository API, since it constructs Tool objects rather than wiring runtime hooks.
Minimal handler
python worker.py, and the process subscribes to the runtime’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 yourself: get
XPANDER_API_KEY from Settings > API keys in Xpander Chat, and pick your own XPANDER_ORGANIZATION_ID and XPANDER_AGENT_ID. See Environment variables for where each value comes from.
@on_task
@on_task registers a function as the agent’s task executor. The decorated function runs every time the runtime dispatches a task to this process: 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 xpander forwards to subscribers in real time. After the loop ends, the handler yields one final TaskUpdateEventType.TaskFinished event carrying the completed Task object. xpander 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 xpander’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 xpander.
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 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 xpander: 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 process 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 process 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 skill invocations: useful for logging, validation, caching, alerting, or analytics. The hooks run during every Tool.ainvoke (which means around every skill call the agent loop makes 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 skill 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 skill invocation, regardless of whether the skill 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 a skill served over MCP 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.
