> ## Documentation Index
> Fetch the complete documentation index at: https://ixoworld-mintlify-dd8815c7.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Add a middleware

> Hook into every LLM call via beforeModel, afterModel, and wrapModelCall using createMiddleware.

A middleware wraps the agent's LLM call. Return one from `getMiddlewares(ctx)` and it runs on every model turn.

<Steps>
  <Step title="Build the middleware with createMiddleware">
    Import `createMiddleware` from `langchain`. Set a `name` (appears in error messages) and any of the six hooks.

    ```ts theme={null}
    import { type AgentMiddleware, type PluginContext } from '@ixo/oracle-runtime';
    import { createMiddleware } from 'langchain';

    export function buildWeatherMiddleware(ctx: PluginContext): AgentMiddleware {
      let startedAt = 0;
      return createMiddleware({
        name: 'WeatherLoggingMiddleware',
        beforeModel: async () => {
          startedAt = Date.now();
          ctx.logger.log('model call started');
        },
        afterModel: async () => {
          const elapsed = startedAt > 0 ? Date.now() - startedAt : -1;
          ctx.logger.log(`model call complete (${elapsed}ms)`);
        },
      });
    }
    ```

    Canonical source: [weather-middleware.ts](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/apps/qiforge-example/src/plugins/weather/weather-middleware.ts).
  </Step>

  <Step title="Pick the right hook">
    `createMiddleware` accepts exactly six hooks. Pick the one whose timing matches your work — there is no `onError`, no `beforeToolCall`, no `afterToolCall`.

    | Hook            | Fires                                       | Use for                            |
    | --------------- | ------------------------------------------- | ---------------------------------- |
    | `beforeAgent`   | Once, before the agent loop starts          | One-time setup per turn            |
    | `beforeModel`   | Before every LLM call                       | Logging, timers, observation       |
    | `wrapModelCall` | Around every LLM call (you call `handler`)  | Error handling, fallbacks, retries |
    | `wrapToolCall`  | Around every tool call (you call `handler`) | Tool-level interception            |
    | `afterModel`    | After every LLM call                        | Logging, emitting events           |
    | `afterAgent`    | Once, after the agent loop ends             | Teardown, final accounting         |

    ```ts theme={null}
    createMiddleware({
      name: 'MyMiddleware',
      beforeModel: async () => {
        // Loading context, observation
      },
      afterModel: async () => {
        // Logging, emitting events
      },
    });
    ```

    Error handling lives in `wrapModelCall`: wrap the `handler(request)` call in a try/catch and retry with a fallback model. The hook owns the call, so you decide what happens on failure.

    ```ts theme={null}
    import { ChatOpenAI } from '@langchain/openai';

    createMiddleware({
      name: 'ModelFallbackMiddleware',
      wrapModelCall: async (request, handler) => {
        try {
          return await handler(request);
        } catch (err) {
          ctx.logger.warn(`primary model failed: ${String(err)} — falling back`);
          return handler({ ...request, model: new ChatOpenAI({ model: 'gpt-4o-mini' }) });
        }
      },
    });
    ```
  </Step>

  <Step title="Return undefined or jumpTo — never a state object">
    <Warning>
      The flow-control hooks (`beforeAgent`, `beforeModel`, `afterModel`, `afterAgent`) must return **`undefined`** (pass through) or **`{ jumpTo: 'end' as const }`** (short-circuit the turn). Never return `{ messages: ... }` or any other state channel. (The `wrap*` hooks are different — they return the result of calling `handler`, as in the fallback example above.)
    </Warning>

    Returning a partial state object from a plugin middleware breaks LangGraph checkpointer thread continuity — the observed symptom is a brand-new thread spawned on every message. Message and state rewrites belong in the transport layer (the messages controller), not in a middleware hook.

    ```ts theme={null}
    beforeModel: async (state) => {
      if (shouldStop(state)) {
        return { jumpTo: 'end' as const }; // stop the loop early
      }
      return undefined; // otherwise pass through
    },
    ```
  </Step>

  <Step title="Return it from getMiddlewares(ctx)">
    The runtime appends your middleware after the framework's built-in middleware (see the list below).

    ```ts theme={null}
    import { OraclePlugin, type AgentMiddleware, type PluginContext } from '@ixo/oracle-runtime';

    export class WeatherPlugin extends OraclePlugin {
      override getMiddlewares(ctx: PluginContext): AgentMiddleware[] {
        return [buildWeatherMiddleware(ctx)];
      }
    }
    ```

    See `getMiddlewares` in [weather.plugin.ts](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/apps/qiforge-example/src/plugins/weather/weather.plugin.ts).
  </Step>

  <Step title="Attach a middleware to a sub-agent (optional)">
    A sub-agent can carry its own middleware — it only runs inside that sub-agent's loop.

    ```ts theme={null}
    import { createSummarizationMiddleware } from '@ixo/oracle-runtime';

    const subAgent: PluginSubAgent = {
      name: 'long_research_agent',
      // ...
      middlewares: [createSummarizationMiddleware()],
    };
    ```
  </Step>
</Steps>

## What to know before shipping

* Four always-on middleware run first and are not removable, in this order: `capability-gate`, `tool-validation`, `tool-repetition-guard`, `tool-retry`. Two more run only when the matching hook is set on `createOracleApp`: `page-context` (when `hooks.getRoomTitle` is provided) and `safety-guardrail` (when `hooks.safetyModel` is provided). Your plugin middleware is appended after all of these.
* Plugin middleware runs in topological dependency order — encode ordering via `dependsOn` if it matters.
* Middleware fires per LLM turn, not per individual tool invocation. Wrap the tool handler directly for per-tool behaviour.
* Closure-scoped timers interleave across concurrent model calls. For accurate timing, push start times onto a per-call ID.
* Don't reimplement auth in a middleware — auth runs at the HTTP layer (`AuthHeaderMiddleware`), not in the agent loop.

## Where to read next

<CardGroup cols={2}>
  <Card title="Add a sub-agent" icon="sitemap" href="/build-an-oracle/develop/plugin-recipes/add-a-sub-agent">
    Sub-agent-scoped middleware.
  </Card>

  <Card title="State schema" icon="table" href="/build-an-oracle/reference/state-schema">
    What's in `state` when your hooks fire.
  </Card>
</CardGroup>
