Skip to main content
The RunContext is the central storage and state management system for your runs. Beyond storing spans, it enables data sharing between components, input/output manipulation, and hierarchical data access across parent-child relationships.

Accessing the RunContext

The RunContext is accessible from any callable within a Runnable’s execution using get_run_context(). This includes:
  • Main handlers: The main function that does the work (i.e. the function you pass to a Tool).
  • Default parameter callables: Functions used to compute default values at runtime (as shown in Runnables).
  • Lifecycle hooks: Functions that run before or after the main handler.
The RunContext.current_span() method returns the span object for the currently executing Runnable, containing all execution data - input parameters, output, timing, metadata, and any custom data you store on it. You get direct access to the live span object being built during execution. Here’s a simple example showing context access from a main handler:
from datetime import datetime

import httpx
from timbal.state import get_run_context

async def api_call(endpoint: str) -> dict:
    span = get_run_context().current_span()
    # Store request metadata for observability
    span.endpoint = endpoint
    span.request_start = datetime.now()
    # Perform actual HTTP request
    async with httpx.AsyncClient() as client:
        response = await client.get(endpoint)
    # Store response metadata for debugging/monitoring
    span.response_status = response.status_code
    span.request_duration = datetime.now() - span.request_start
    return response.json()

api_tool = Tool(
    name="api_call",
    handler=api_call,
)
Beyond current_span(), the RunContext provides methods like .parent_span() and .step_span() to access parent or neighbor spans. We’ll explore these methods in future sections when working with multi-step workflows and nested executions.

Lifecycle Hooks

Beyond the main handler, Runnables support lifecycle hooks - functions that run at specific points during execution. These provide structured access points for context interaction and enable powerful data transformation patterns. Every Runnable supports two optional hooks:
  • pre_hook: A function that runs before the main handler
  • post_hook: A function that runs after the main handler completes
Hooks can modify inputs, store custom data, and transform outputs - all while sharing the same RunContext.

Pre-hooks: Modifying Input and Adding Context

A pre_hook runs before your handler and can both modify input parameters and store additional context data:
from datetime import datetime

from timbal.state import get_run_context

def pre_hook():
    span = get_run_context().current_span()
    # Modify input parameters that will be passed to the handler
    span.input["name"] = span.input["name"].capitalize()
    # Add a new parameter
    span.input["location"] = "Barcelona"
    # Store custom data for later use
    span.greet_time = datetime.now()

def greet(name: str, location: str) -> str:
    return f"Hello {name} from {location}!"

greet_tool = Tool(
    name="greet",
    pre_hook=pre_hook,
    handler=greet,
)

result = await greet_tool(name="alice").collect() # "Hello Alice from Barcelona!"
Pre-hooks are perfect for:
  • Data Preparation: Process raw webhook payloads, parse JSON, or normalize input formats
  • Input Enhancement: Enrich data with additional context from databases or APIs
  • Request Preprocessing: Extract headers, validate signatures, or decode authentication tokens
  • State Initialization: Set up execution context, timestamps, or tracking metadata

Post-hooks: Processing Output After Completion

A post_hook runs after your handler and can access both input and output:
def post_hook():
    span = get_run_context().current_span()
    # Retrieve custom data stored in pre_hook
    greet_time = span.greet_time
    print(f"Greeting at {greet_time}")
    # Modify the output before it's returned
    span.output = "Greeting overridden!"

greet_tool = Tool(
    name="greet",
    pre_hook=pre_hook,
    handler=greet,
    post_hook=post_hook,
)

result = await greet_tool(name="alice").collect() # "Greeting overridden!"
Post-hooks are perfect for:
  • Logging: Record execution details and results
  • Metadata Storage: Store processing metrics, timestamps, or analysis data
  • Output Modification: Transform or enrich the final result
  • Cleanup Tasks: Handle resource cleanup or state management
These simple examples show the basics. In practice, hooks excel at:
  • Input manipulation: Processing webhooks where we don’t control the shape of the incoming data.
  • Agent adaptation: Converting between modalities (audio ↔ text) for different models.
More advanced patterns in the Agents section.