Skip to content

Middleware

Middleware provides hooks into the GraphRunner execution loop. Use middleware to add caching, logging, metrics, request transformation, or custom routing logic without modifying the runner or node executors.

Pass middleware instances to the GraphRunner via the middleware option. Hooks run in registration order:

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

All hooks are optional. Implement only the ones you need.

Called before a node runs. Return { shortCircuit: action } to skip execution entirely and use the provided action instead. Useful 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. Caching 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) {
// Cache key combines node id with any inputs that influence the action.
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 — the 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';
}
},
};

Every hook receives a MiddlewareContext:

FieldTypeDescription
nodeGraphNodeThe node being executed.
stateReadonly<WorkflowState>Current state snapshot (read-only).
graphReadonly<Graph>The graph definition (read-only).
iterationnumberCurrent iteration count.

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.

  • Streaming — observe execution via events instead of middleware
  • Nodes — node types and failure policies
  • Error Handling — how errors propagate through the runner