Skip to content

Middleware

Middleware provides hooks into the runner execution loop. Use it to add caching, logging, metrics, request transformation, or custom routing without modifying the runner or the node executors. A middleware is a plain object with one or more optional hook methods, so there is nothing to instantiate. You implement the hooks you need and pass the object to the runner.

import { GraphRunner } from '@cycgraph/orchestrator';
const runner = new GraphRunner(graph, state, {
middleware: [loggingMiddleware, cachingMiddleware],
});

The runner invokes hooks at four points in each node’s lifecycle.

  • beforeNodeExecute: runs first, before the node executes.
  • afterNodeExecute: runs once the node produces an action, before the reducer applies it.
  • afterReduce: runs after the action has merged into state.
  • beforeAdvance: runs last, before the runner picks the next node.

Hooks run synchronously in registration order. If you pass [a, b], then a’s hook completes before b’s hook for the same point in the lifecycle. Each hook is async, so the runner awaits it before moving on.

Refs:

Pass an array of middleware to the runner through the middleware option on GraphRunnerOptions. The array order is the run order.

const runner = new GraphRunner(graph, state, {
middleware: [loggingMiddleware, cachingMiddleware],
});

All hooks are optional. Implement only the ones you need. Every hook receives a MiddlewareContext as its first argument.

Called before a node runs. Return { shortCircuit: action } to skip execution entirely and use the provided action instead. This is the hook for caching or circuit-breaking.

The example below uses a process-local Map so you can copy and run it. In production, swap in Redis or your existing cache backend. Cache keys should include both node.id and a hash of the relevant input. Caching by node ID alone is unsafe whenever the inputs change between runs.

import type { GraphRunnerMiddleware } from '@cycgraph/orchestrator';
import type { Action } from '@cycgraph/orchestrator';
const cache = new Map<string, Action>();
const cachingMiddleware: GraphRunnerMiddleware = {
async beforeNodeExecute(ctx) {
const key = `${ctx.node.id}:${JSON.stringify(ctx.state.memory.goal ?? '')}`;
const cached = cache.get(key);
if (cached) {
return { shortCircuit: cached };
}
},
async afterReduce(ctx, action) {
const key = `${ctx.node.id}:${JSON.stringify(ctx.state.memory.goal ?? '')}`;
cache.set(key, action);
},
};

Called after a node executes, before the action is applied by the reducer. Return a modified action to transform it, or void to keep the original.

const enrichMiddleware: GraphRunnerMiddleware = {
async afterNodeExecute(ctx, action) {
return {
...action,
metadata: {
...action.metadata,
custom_field: 'enriched',
},
};
},
};

Called after the action has been reduced into state. This hook is observational only, so its return value is ignored. Use it for logging, metrics, or external notifications.

const metricsMiddleware: GraphRunnerMiddleware = {
async afterReduce(ctx, action, newState) {
metrics.recordNodeExecution(ctx.node.id, action.metadata.duration_ms);
},
};

Called before the runner advances to the next node. Return a node ID to override the routing decision, or void to keep the default.

const routingMiddleware: GraphRunnerMiddleware = {
async beforeAdvance(ctx, nextNodeId) {
if (ctx.state.memory.urgent) {
return 'fast-track-node';
}
},
};

Refs:

Errors thrown by middleware propagate to the runner’s error handling. The same retry and failure policy that applies to node execution applies to middleware errors. Design middleware to be resilient, and avoid throwing on non-critical failures.

Refs:

The middleware object you pass to the runner. All hooks are optional. Instances are called in registration order, and errors thrown by any hook propagate to the runner’s error handling.

Hook Signature Description
beforeNodeExecute (ctx: MiddlewareContext) => Promise<BeforeNodeResult | void> Runs before a node executes. Return a shortCircuit action to skip execution.
afterNodeExecute (ctx: MiddlewareContext, action: Action) => Promise<Action | void> Runs after a node executes, before the action is reduced. Return a transformed action or void to keep the original.
afterReduce (ctx: MiddlewareContext, action: Action, newState: Readonly<WorkflowState>) => Promise<void> Runs after the action reduces into state. Observational only: the return value is ignored.
beforeAdvance (ctx: MiddlewareContext, nextNodeId: string) => Promise<string | void> Runs before advancing to the next node. Return a node ID to override routing, or void to keep the default.

The read-only context passed as the first argument to every hook.

Field Type Description
node GraphNode The node being executed.
state Readonly<WorkflowState> Current workflow state snapshot (read-only).
graph Readonly<Graph> The graph definition (read-only).
iteration number Current iteration count.

The result a beforeNodeExecute hook may return.

Field Type Description
shortCircuit Action If set, skip node execution and reduce this action instead.
  • Graph Runner: the execution loop middleware hooks into
  • Streaming: observe execution via events instead of middleware
  • Nodes: node types and failure policies
  • Error Handling: how errors propagate through the runner