Skip to main content
Flows are named execution pipelines that define how requests are processed through a series of lifecycle stages. Every MCP operation (calling a tool, reading a resource, getting a prompt) passes through a flow that controls pre-processing, execution, post-processing, and finalization.
Flows are part of the FrontMCP execution model. They provide hook points for cross-cutting concerns like logging, caching, validation, and error handling.

Why Flows?

Flows give you fine-grained control over request processing without modifying tool, resource, or prompt code directly.
AspectFlowToolPlugin
PurposeRequest lifecycle managementExecute actionsCross-cutting extensions
ScopePer-request pipelineSingle actionAcross all requests
CustomizationStage-level hooksExecute methodRegistration hooks
Use caseLogging, caching, auth checksBusiness logicFeature extensions
Flows are ideal for:
  • Request validation — check permissions, validate inputs before execution
  • Caching — intercept responses and serve from cache
  • Logging and auditing — trace every request through the system
  • Error handling — centralized error recovery and formatting
  • Performance monitoring — measure timing across stages

Flow Lifecycle

Every request passes through these stages in order:
StagePurposeExample
preValidation, auth checks, input transformationCheck API key, parse headers
executeRun the core operation (tool, resource, prompt)Call execute() on the tool
postTransform output, apply caching, loggingCache response, format output
finalizeSend response, cleanupEmit response to client, release resources
errorHandle failures from any stageLog error, return formatted error response

Built-in Flows

FrontMCP provides built-in flows for all MCP protocol operations:
Flow NameTriggerDescription
tools:call-tooltools/call requestExecute a tool with validated arguments
tools:list-toolstools/list requestList all available tools
resources:read-resourceresources/read requestRead a resource by URI
resources:list-resourcesresources/list requestList all available resources
resources:subscriberesources/subscribe requestSubscribe to resource changes
prompts:get-promptprompts/get requestGenerate a prompt with arguments
prompts:list-promptsprompts/list requestList all available prompts

Creating Custom Flows

Define custom flows with the @Flow decorator:
import { Flow, FlowBase } from '@frontmcp/sdk';

@Flow({
  name: 'custom:my-flow',
  description: 'Custom processing pipeline',
  plan: {
    steps: ['pre', 'execute', 'post', 'finalize'],
  },
})
export default class MyCustomFlow extends FlowBase {
  async pre(): Promise<void> {
    // Validate inputs, check permissions
    const input = this.state.get('input');
    if (!input) {
      this.fail('Missing required input');
    }
  }

  async execute(): Promise<void> {
    // Core business logic
    const result = await this.processRequest();
    this.state.set('result', result);
  }

  async post(): Promise<void> {
    // Transform output, cache results
    const result = this.state.get('result');
    this.state.set('output', this.formatResponse(result));
  }

  async finalize(): Promise<void> {
    // Send response to client
    const output = this.state.get('output');
    this.respond(output);
  }

  async error(err: Error): Promise<void> {
    // Centralized error handling
    this.respond({ error: err.message });
  }
}

Flow Metadata

@Flow({
  name: string,              // Required: unique flow identifier
  description?: string,      // Optional: human-readable description
  plan: {
    steps: string[],         // Required: ordered stage names
  },
})

Hooking into Flows

You don’t need to create a full custom flow to customize behavior. Use hooks to intercept specific stages of existing flows.

Hook Types

Hook TypeWhen It RunsUse Case
WillBefore a stage executesValidate, transform input
DidAfter a stage completesLog, cache, transform output
AroundWraps the entire stageTiming, retry logic, circuit breakers

Applying Hooks

Use the @Hooks decorator on tools, resources, or prompts:
import { Tool, ToolContext, Hooks } from '@frontmcp/sdk';
import { z } from 'zod';

@Tool({
  name: 'my-tool',
  inputSchema: { query: z.string() },
})
@Hooks([
  {
    flow: 'tools:call-tool',
    stage: 'pre',
    type: 'will',
    handler: async (ctx) => {
      console.log('About to execute tool:', ctx.input);
    },
  },
  {
    flow: 'tools:call-tool',
    stage: 'execute',
    type: 'did',
    handler: async (ctx) => {
      console.log('Tool execution complete:', ctx.output);
    },
  },
])
class MyTool extends ToolContext {
  async execute({ query }: { query: string }) {
    return `Result for: ${query}`;
  }
}

Around Hooks

Around hooks wrap a stage completely, giving you control over whether the stage executes:
{
  flow: 'tools:call-tool',
  stage: 'execute',
  type: 'around',
  handler: async (ctx, next) => {
    const start = Date.now();
    try {
      await next();  // Execute the stage
    } finally {
      const elapsed = Date.now() - start;
      console.log(`Execution took ${elapsed}ms`);
    }
  },
}
For a complete guide to hooks, see the Hooks decorator reference.

Flow Control

Within flow stages, you have access to control methods:
MethodDescription
this.respond(value)End the flow and send a response to the client
this.fail(message)Abort the flow with an error
this.state.get(key)Read from flow state
this.state.set(key, value)Write to flow state

State Management

Flows use a state object to pass data between stages. Never mutate rawInput directly — use state.set() instead:
async pre(): Promise<void> {
  // Read from the request
  const input = this.state.get('rawInput');

  // Transform and store in state
  this.state.set('normalizedInput', {
    ...input,
    timestamp: Date.now(),
  });
}

async execute(): Promise<void> {
  // Read from state set in previous stage
  const input = this.state.get('normalizedInput');
  const result = await this.process(input);
  this.state.set('result', result);
}
Never mutate rawInput in flows — use state.set() for flow state. This ensures each stage works with clean, immutable inputs.

Generating Flows

Use the Nx generator to scaffold a new flow:
nx g @frontmcp/nx:flow --name audit-log --project my-app
This generates a flow class with all lifecycle methods:
src/flows/
  audit-log.flow.ts

Best Practices

Do:
  • Use hooks for cross-cutting concerns instead of duplicating logic in tools
  • Keep flow stages focused — each stage should have a single responsibility
  • Use state.set() / state.get() for passing data between stages
  • Handle errors in the error() stage for centralized error management
  • Validate hook flows match their entry type (e.g., tool hooks use tools:call-tool)
Don’t:
  • Mutate rawInput directly — use flow state instead
  • Create custom flows for simple operations that built-in flows already handle
  • Skip the error() stage — unhandled errors surface as generic MCP errors
  • Add business logic to hooks — keep hooks lightweight, delegate to providers