---
title: "Context & State Management"
description: "The shared memory that connects everything together across the entire lifetime of a run"
---

The RunContext is the central storage and state management system for your runs. Beyond storing traces, 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](./runnables#parameter-handling-and-basic-execution)).
- **Lifecycle hooks**: Functions that run before or after the main handler.

The `RunContext.current_trace()` method returns the trace 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 trace object being built during execution.

Here's a simple example showing context access from a main handler:

```python highlight={7}
from datetime import datetime

import httpx
from timbal.state import get_run_context

async def api_call(endpoint: str) -> dict:
    trace = get_run_context().current_trace()
    # Store request metadata for observability
    trace.endpoint = endpoint
    trace.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
    trace.response_status = response.status_code
    trace.request_duration = datetime.now() - trace.request_start
    return response.json()

api_tool = Tool(
    name="api_call",
    handler=api_call,
)
```

<Note>
Beyond `current_trace()`, the RunContext provides methods like `.parent_trace()` and `.step_trace()` to access parent or neighbor traces. We'll explore these methods in future sections when working with multi-step workflows and nested executions.
</Note>

## 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:

```python highlight={19}
from datetime import datetime

from timbal.state import get_run_context

def pre_hook():
    trace = get_run_context().current_trace()
    # Modify input parameters that will be passed to the handler
    trace.input["name"] = trace.input["name"].capitalize()
    # Add a new parameter
    trace.input["location"] = "Barcelona"
    # Store custom data for later use
    trace.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:

```python highlight={13}
def post_hook():
    trace = get_run_context().current_trace()
    # Retrieve custom data stored in pre_hook
    greet_time = trace.greet_time
    print(f"Greeting at {greet_time}")
    # Modify the output before it's returned
    trace.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

<Tip>
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](../agents) section.
</Tip>

