# Writing Handlers Handlers are async Python functions that process messages. This guide covers everything you need to know to write effective handlers. ## Basic Handler Structure Every handler follows this pattern: ```python from dataclasses import dataclass from third_party.xmlable import xmlify from xml_pipeline.message_bus.message_state import HandlerMetadata, HandlerResponse # 1. Define your payload (input) @xmlify @dataclass class MyPayload: """Description of what this payload represents.""" field1: str field2: int # 2. Define your response (output) @xmlify @dataclass class MyResponse: """Description of the response.""" result: str # 3. Write the handler async def my_handler(payload: MyPayload, metadata: HandlerMetadata) -> HandlerResponse: """Process the payload and return a response.""" result = f"Processed {payload.field1} with {payload.field2}" return HandlerResponse( payload=MyResponse(result=result), to="next-listener", # Where to send the response ) ``` ## The @xmlify Decorator The `@xmlify` decorator enables automatic XML serialization and XSD generation: ```python from dataclasses import dataclass from third_party.xmlable import xmlify @xmlify @dataclass class Greeting: name: str # Required field formal: bool = False # Optional with default count: int = 1 # Optional with default ``` This generates XML like: ```xml Alice true 3 ``` ### Supported Types | Python Type | XML Representation | |-------------|-------------------| | `str` | Text content | | `int` | Integer text | | `float` | Decimal text | | `bool` | `true` / `false` | | `list[T]` | Repeated elements | | `Optional[T]` | Optional element | | `@xmlify` class | Nested element | ### Nested Payloads ```python @xmlify @dataclass class Address: street: str city: str @xmlify @dataclass class Person: name: str address: Address # Nested payload ``` ## HandlerMetadata The `metadata` parameter provides context about the message: ```python @dataclass class HandlerMetadata: thread_id: str # Opaque thread UUID from_id: str # Who sent this message own_name: str | None # This listener's name (agents only) is_self_call: bool # True if message is from self usage_instructions: str # Peer schemas for LLM prompts todo_nudge: str # System reminders about pending todos ``` ### Usage Examples ```python async def my_handler(payload: MyPayload, metadata: HandlerMetadata) -> HandlerResponse: # Log who sent the message print(f"Received from: {metadata.from_id}") # Check if this is a self-call (agent iterating) if metadata.is_self_call: print("This is a self-call") # For agents: use peer schemas in LLM prompts if metadata.usage_instructions: system_prompt = metadata.usage_instructions + "\n\nYour custom instructions..." ``` ## Response Types ### Forward to Target Send the response to a specific listener: ```python return HandlerResponse( payload=MyResponse(result="done"), to="next-listener", ) ``` ### Respond to Caller Return to whoever sent the message: ```python return HandlerResponse.respond( payload=ResultPayload(value=42) ) ``` This uses the thread registry to route back up the call chain. ### Terminate Chain End processing with no response: ```python return None ``` Use this for terminal handlers (logging, display, etc.). ## Handler Patterns ### Simple Tool A stateless transformation: ```python @xmlify @dataclass class AddPayload: a: int b: int @xmlify @dataclass class AddResult: sum: int async def add_handler(payload: AddPayload, metadata: HandlerMetadata) -> HandlerResponse: result = payload.a + payload.b return HandlerResponse.respond(payload=AddResult(sum=result)) ``` ### LLM Agent An agent that uses language models: ```python async def research_handler(payload: ResearchQuery, metadata: HandlerMetadata) -> HandlerResponse: from xml_pipeline.platform.llm_api import complete # Build prompt with peer awareness system_prompt = f""" {metadata.usage_instructions} You are a research agent. Answer the query using available tools. """ response = await complete( model="grok-4.1", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": payload.query}, ], ) return HandlerResponse( payload=ResearchResult(answer=response.content), to="summarizer", ) ``` ### Terminal Handler A handler that displays output and ends the chain: ```python async def console_output(payload: TextOutput, metadata: HandlerMetadata) -> None: print(f"[{payload.source}] {payload.text}") return None # Chain ends here ``` ### Self-Iterating Agent An agent that calls itself to continue reasoning: ```python async def thinking_agent(payload: ThinkPayload, metadata: HandlerMetadata) -> HandlerResponse: # Check if we should continue thinking if payload.iteration >= 5: return HandlerResponse( payload=FinalAnswer(answer=payload.current_answer), to="output", ) # Continue thinking by calling self return HandlerResponse( payload=ThinkPayload( iteration=payload.iteration + 1, current_answer=f"Refined: {payload.current_answer}", ), to=metadata.own_name, # Self-call ) ``` ## Error Handling Handlers should handle errors gracefully: ```python async def safe_handler(payload: MyPayload, metadata: HandlerMetadata) -> HandlerResponse: try: result = await risky_operation(payload) return HandlerResponse.respond(payload=SuccessResult(data=result)) except ValidationError as e: return HandlerResponse.respond(payload=ErrorResult(error=str(e))) except Exception as e: # Log and return generic error logger.exception("Handler failed") return HandlerResponse.respond(payload=ErrorResult(error="Internal error")) ``` ## Registration Register handlers in `organism.yaml`: ```yaml listeners: - name: calculator.add payload_class: handlers.calc.AddPayload handler: handlers.calc.add_handler description: "Adds two numbers and returns the sum" ``` The `description` is important—it's used in auto-generated tool prompts for LLM agents. ## CPU-Bound Handlers For computationally expensive handlers, mark them as `cpu_bound`: ```yaml listeners: - name: analyzer payload_class: handlers.analyze.AnalyzePayload handler: handlers.analyze.analyze_handler description: "Heavy document analysis" cpu_bound: true ``` These run in a separate process pool to avoid blocking the event loop. ## Security Notes Handlers are **untrusted code**. The system enforces: 1. **Identity injection** — `` is always set by the pump, never by handlers 2. **Thread isolation** — Handlers see only opaque UUIDs 3. **Peer constraints** — Agents can only send to declared peers Even compromised handlers cannot: - Forge sender identity - Access other threads - Discover organism topology - Route to undeclared peers ## See Also - [[Handler Contract]] — Complete handler specification - [[Configuration]] — Registering handlers - [[LLM Router]] — Using language models