# Hooks and middleware

> Hooks observe graph execution events. Middleware wraps execution units with before-and-after logic.

Hooks and middleware are two mechanisms for attaching cross-cutting behavior to graph execution. Hooks fire at specific points during execution — use them for observation and resource injection. Middleware wraps an entire execution unit, giving you before-and-after control — use it for timing, tracing spans, rate limiting, and anything else that needs to straddle a boundary.

## Hooks

A hook is a callback that runs when a graph execution event occurs. Hooks come in two flavors: observers and providers.

**Observers** react to events without producing anything. They are the right choice for logging, metrics, and tracing side effects.

```rust
use polaris_ai::graph::hooks::{HooksAPI, GraphEvent};
use polaris_ai::graph::hooks::schedule::OnSystemStart;


let hooks = HooksAPI::new();


hooks.register_observer::<OnSystemStart, _>("logger", |event: &GraphEvent| {
    if let GraphEvent::SystemStart { node_name, .. } = event {
        tracing::info!("system starting: {}", node_name);
    }
})?;
```

**Providers** return a resource that is inserted into the `SystemContext` before the system runs. Systems can then access the resource through `Res<T>`. The `DevToolsPlugin` uses a provider hook to inject `SystemInfo` before every system execution.

```rust
use polaris_ai::graph::hooks::{HooksAPI, GraphEvent};
use polaris_ai::graph::hooks::schedule::OnSystemStart;
use polaris_ai::graph::dev::SystemInfo;


let hooks = HooksAPI::new();


hooks.register_provider::<OnSystemStart, SystemInfo, _>(
    "devtools",
    |event: &GraphEvent| {
        match event {
            GraphEvent::SystemStart { node_id, node_name } => {
                Some(SystemInfo::new(node_id.clone(), node_name))
            }
            _ => None,
        }
    },
)?;
```

The provider closure returns `Option<T>`. When it returns `Some`, the resource is inserted into the context. When it returns `None`, nothing happens. If multiple providers on the same schedule produce the same resource type, last-write-wins.

### Schedules

Each hook is registered against one or more schedules. A schedule is a marker type that identifies a point in the graph execution lifecycle. These are the available schedules:

**Graph-level:** `OnGraphStart`, `OnGraphComplete`, `OnGraphFailure` — fire once per graph execution.

**System-level:** `OnSystemStart`, `OnSystemComplete`, `OnSystemError` — fire around each system node.

**Decision and switch:** `OnDecisionStart`, `OnDecisionComplete`, `OnSwitchStart`, `OnSwitchComplete` — fire around branching nodes.

**Loop:** `OnLoopStart`, `OnLoopIteration`, `OnLoopEnd` — fire at loop boundaries and on each iteration.

**Parallel:** `OnParallelStart`, `OnParallelComplete` — fire around parallel execution blocks.

Resources provided by `OnSystemStart` and `OnGraphStart` hooks are considered during graph validation, since they are guaranteed to be available before any system runs. Resources from other schedules are not, because they arrive after execution has begun.

### Multi-schedule registration

You can register a single hook on multiple schedules using tuple syntax. This avoids duplicating the same closure for related events.

```rust
use polaris_ai::graph::hooks::{HooksAPI, GraphEvent};
use polaris_ai::graph::hooks::schedule::{OnSystemStart, OnSystemComplete, OnSystemError};


let hooks = HooksAPI::new();


hooks.register_observer::<(OnSystemStart, OnSystemComplete, OnSystemError), _>(
    "system_tracker",
    |event: &GraphEvent| match event {
        GraphEvent::SystemStart { node_name, .. } => {
            tracing::debug!("start: {}", node_name);
        }
        GraphEvent::SystemComplete { node_name, duration, .. } => {
            tracing::debug!("complete: {} ({:?})", node_name, duration);
        }
        GraphEvent::SystemError { node_name, error, .. } => {
            tracing::warn!("error in {}: {}", node_name, error);
        }
        _ => {}
    },
)?;
```

The composite type `AllGraphSchedules` registers on every schedule at once. This is useful for blanket event tracing.

```rust
use polaris_ai::graph::hooks::schedule::AllGraphSchedules;
use polaris_ai::graph::hooks::{HooksAPI, GraphEvent};


let hooks = HooksAPI::new();


hooks.register_observer::<AllGraphSchedules, _>(
    "trace_all",
    |event: &GraphEvent| {
        tracing::debug!("{event}");
    },
)?;
```

### Execution order

Hooks execute in registration order. If you register hook A before hook B on the same schedule, A runs first.

### Using HooksAPI in a plugin

Plugins access `HooksAPI` through the server’s API registry. The `DevToolsPlugin` is a concrete example of this pattern.

```rust
use polaris_ai::graph::hooks::HooksAPI;
use polaris_ai::graph::hooks::events::GraphEvent;
use polaris_ai::graph::hooks::schedule::OnSystemStart;
use polaris_ai::system::plugin::{Plugin, Version};
use polaris_ai::system::server::Server;


pub struct LoggingHooksPlugin;


impl Plugin for LoggingHooksPlugin {
    const ID: &'static str = "myapp::logging_hooks";
    const VERSION: Version = Version::new(0, 1, 0);


    fn build(&self, server: &mut Server) {
        if !server.contains_api::<HooksAPI>() {
            server.insert_api(HooksAPI::new());
        }


        let hooks = server
            .api::<HooksAPI>()
            .expect("HooksAPI should be present");


        hooks.register_observer::<OnSystemStart, _>(
            "system_logger",
            |event: &GraphEvent| {
                if let GraphEvent::SystemStart { node_name, .. } = event {
                    tracing::info!("executing system: {}", node_name);
                }
            },
        )
        .expect("hook registration should succeed");
    }
}
```

Hook names must be unique per schedule. Registering two hooks with the same name on the same schedule returns a `HookRegistrationError::DuplicateName`. The same name on different schedules is allowed.

## Middleware

Middleware wraps an execution unit with before-and-after logic. Where hooks fire at a single point, middleware spans a duration — you run code before the unit executes, call `next.run(ctx).await` to let it proceed, then run code after it completes.

```rust
use polaris_ai::graph::middleware::{MiddlewareAPI, info::SystemInfo};


let mw = MiddlewareAPI::new();


mw.register_system("timer", |info: SystemInfo, ctx, next| {
    Box::pin(async move {
        let start = std::time::Instant::now();
        let result = next.run(ctx).await;
        eprintln!("{}: {:?}", info.node_name, start.elapsed());
        result
    })
});
```

The handler receives three arguments: a typed info struct with metadata about the execution unit, a mutable reference to the `SystemContext`, and a `Next` value representing the rest of the chain. You must call `next.run(ctx).await` exactly once. Dropping `next` without calling it produces an `ExecutionError::InternalError`.

### Targets

Each middleware target wraps a different execution unit. You register middleware using the corresponding method on `MiddlewareAPI`.

| Target           | Registration method        | Info type            | Wraps                                     |
| ---------------- | -------------------------- | -------------------- | ----------------------------------------- |
| `GraphExecution` | `register_graph_execution` | `GraphInfo`          | Entire graph run                          |
| `System`         | `register_system`          | `SystemInfo`         | Single system node                        |
| `Loop`           | `register_loop`            | `LoopInfo`           | Entire loop node                          |
| `LoopIteration`  | `register_loop_iteration`  | `LoopIterationInfo`  | Single loop iteration                     |
| `Parallel`       | `register_parallel`        | `ParallelInfo`       | Entire parallel node                      |
| `ParallelBranch` | `register_parallel_branch` | `ParallelBranchInfo` | Single parallel branch                    |
| `Decision`       | `register_decision`        | `DecisionInfo`       | Decision node evaluation                  |
| `Switch`         | `register_switch`          | `SwitchInfo`         | Switch node evaluation                    |
| `Scope`          | `register_scope`           | `ScopeInfo`          | Scoped subgraph execution                 |
| `Dynamic`        | `register_dynamic`         | `DynamicInfo`        | Dynamic candidate selection and execution |

Each info type carries metadata relevant to its target. For example, `LoopInfo` includes `max_iterations` and the node name, while `ParallelBranchInfo` includes the branch index and total branch count.

### Layer ordering

When you register multiple middlewares on the same target, they form a chain. The last-registered middleware is the outermost layer. If you register A then B, execution flows: B before -> A before -> hooks -> terminal -> hooks -> A after -> B after.

```rust
use polaris_ai::graph::middleware::{MiddlewareAPI, info::SystemInfo};


let mw = MiddlewareAPI::new();


mw.register_system("auth_check", |info: SystemInfo, ctx, next| {
    Box::pin(async move {
        // Runs second (inner layer)
        next.run(ctx).await
    })
});


mw.register_system("tracing_span", |info: SystemInfo, ctx, next| {
    Box::pin(async move {
        // Runs first (outer layer, registered last)
        let span = tracing::info_span!("system", name = info.node_name);
        let _guard = span.enter();
        next.run(ctx).await
    })
});
```

Registration calls chain, so you can register multiple middlewares in sequence.

```rust
mw.register_system("first", |_info, ctx, next| {
    Box::pin(async move { next.run(ctx).await })
})
.register_system("second", |_info, ctx, next| {
    Box::pin(async move { next.run(ctx).await })
});
```

### Error handling

Middleware handlers return `Result<(), MiddlewareError>`. There are two error variants.

`MiddlewareError::Layer(message)` indicates an error from the middleware itself. The framework converts this into an `ExecutionError::MiddlewareError` with the registered middleware name attached automatically.

`MiddlewareError::Inner(error)` propagates an error from deeper in the chain unchanged. When you call `next.run(ctx).await` and it returns an error, that error is already wrapped as `Inner`. You can inspect it, log it, or propagate it.

```rust
use polaris_ai::graph::middleware::{MiddlewareAPI, MiddlewareError, info::SystemInfo};


let mw = MiddlewareAPI::new();


mw.register_system("error_logger", |info: SystemInfo, ctx, next| {
    Box::pin(async move {
        let result = next.run(ctx).await;
        if let Err(ref err) = result {
            tracing::error!("system {} failed: {}", info.node_name, err);
        }
        result
    })
});
```

## When to use which

Hooks and middleware solve different problems. The right choice depends on whether you need point-in-time observation or duration-spanning behavior.

**Use hooks** when you need to react to an event or inject a resource. Hooks are simpler — they are synchronous callbacks that receive a `&GraphEvent`. Logging a message when a system starts, recording a metric when a loop completes, or injecting a `SystemInfo` resource before execution are all hook use cases.

**Use middleware** when you need to hold something open across the execution of a unit. Opening and closing a tracing span, measuring elapsed time, enforcing rate limits, or counting tokens across an LLM call all require before-and-after behavior that hooks cannot express. Two disconnected point events (for example, `OnSystemStart` and `OnSystemComplete`) cannot hold a span guard open — middleware can.

Both hooks and middleware are registered through their respective APIs (`HooksAPI` and `MiddlewareAPI`) during the plugin `build` phase. For details on API registration, see [APIs](/docs/polaris/advanced-topics/apis.md).
