Ariso

Ivan's Inferences #2

Adopting LangChain's Middleware API

8 min read
AILangChainEngineeringMCP

AI agents that simply call tools are everywhere. You give a model access to a set of functions, it decides when to call them, and the results flow back into the conversation. It's elegant, powerful, and—for many use cases—entirely sufficient.

But then you hit the wall.

You're building something more sophisticated: a calendar assistant that doesn't just create events, but understands your preferences, schedules video conferences, and updates event details automatically. Suddenly, "just call tools" isn't enough. You need tools that talk to each other, workflows that trigger after specific actions, and context that flows between operations.

This is where LangChain's Middleware API becomes essential.


The Problem: Complex Tool Collaboration

Let's make this concrete. You're building a calendar agent that integrates with Google Calendar over MCP (Model Context Protocol). The user says:

"Schedule a meeting with Sarah next Tuesday at 2pm to discuss the Q1 roadmap."

Sounds simple. But your requirements are:

  1. Before creating the event: Inject the user's preferences—default meeting duration, preferred video conferencing tool, standard description templates
  2. After the event is created: Automatically schedule a Zoom meeting, then update the calendar event with the conference link

This isn't one tool call. It's an orchestrated workflow where tools depend on and augment each other.


The Old Way: Callbacks and Tool Chains

Before LangChain's middleware API (pre-v1), solving this required two painful patterns:

Pattern 1: Tools That Call Other Tools

You'd create a "super tool" that internally orchestrates other tools:

const createMeetingWithZoom = tool({ name: "create_meeting_with_zoom", description: "Creates a calendar event and sets up video conferencing", async execute({ title, time, attendees }) { // First, manually inject user preferences const userPrefs = await getUserPreferences(); // Call the calendar tool const event = await calendarTool.execute(...); // Then call Zoom tool const zoomLink = await zoomTool.execute(...); // Update the event with the link await calendarUpdateTool.execute({ eventId: event.id, description: `${event.description}\n\nJoin: ${zoomLink}`, }); return event; }, });

The problems compound quickly:

  • Tight coupling: Your "super tool" hardcodes the orchestration logic
  • No reusability: The calendar tool alone can't be used without Zoom
  • Invisible to the model: The LLM doesn't understand what's happening internally
  • Testing nightmare: You can't test individual steps in isolation

Pattern 2: Callback Managers

LangChain's callback system let you hook into events:

const callbackManager = new CallbackManager(); callbackManager.addHandler({ handleToolEnd: async (output, runId, parentRunId, tags) => { if (tags?.includes("calendar_create")) { // Trigger Zoom creation... somehow // But how do you access the event data? // How do you update the calendar afterward? } }, });

Callbacks gave you hooks, but not control. You could observe that something happened, but actually orchestrating follow-up actions required threading state through a labryinth of handler functions.


The Middleware API: Composable Control

LangChain's middleware API introduces two powerful primitives that solve these problems elegantly:

  • wrapModelCall: Intercept and modify requests before they reach the model
  • wrapToolCall: Intercept tool executions and run logic after specific tools complete

These aren't callbacks—they're composable wrappers that give you full control over the execution pipeline.


wrapModelCall: Injecting Context Before Inference

The first challenge was injecting user preferences before the model decides how to create an event. With wrapModelCall, you intercept the request and augment the tool descriptions:

import { createMiddleware } from "langchain"; const withUserPreferences = createMiddleware({ name: "UserPreferencesMiddleware", wrapModelCall: async (request, handler) => { augmentToolDescriptions(request.tools, await getUserPreferences()); return handler(request); }, });

Now when the model sees the calendar tool, it understands the user's preferences as part of the tool's contract. The model can make informed decisions—scheduling within working hours, using the preferred video tool—without you hardcoding business logic.


wrapToolCall: Post-Execution Workflows

The second challenge was triggering the Zoom workflow after event creation. wrapToolCall lets you intercept specific tool executions:

import { createMiddleware } from "langchain"; const withZoomIntegration = createMiddleware({ name: "ZoomIntegrationMiddleware", wrapToolCall: async (request, handler) => { const result = await handler(request); if (request.toolCall.name === "google_calendar_create_event") { const zoom = await createZoomMeeting(result); await updateCalendarWithZoomLink(result.id, zoom.joinUrl); } return result; }, });

The elegance here is separation of concerns:

  • The calendar tool stays focused on calendar operations
  • The Zoom integration is a composable layer
  • You can add, remove, or modify integrations without touching core tools
  • Each piece is independently testable

Composing Middlewares

The real power emerges when you compose multiple middlewares:

import { createMiddleware } from "langchain"; const middlewares = [ withUserPreferences, // Inject preferences before model call withZoomIntegration, // Auto-add video conferencing withNotifications, // Send Slack notifications on creation ]; const agent = createReactAgent({ llm: new ChatOpenAI({ model: "gpt-4" }), tools: await mcpClient.getTools(), middlewares, });

Each middleware is a single-purpose unit. You compose them declaratively. The execution order is explicit and predictable.


Why This Matters

The middleware pattern isn't just about cleaner code—it's about building AI systems that scale:

Separation of concerns: Core tools stay simple. Integration logic lives in composable layers.

Testability: Test your calendar tool without Zoom. Test your Zoom middleware with a mock calendar response.

Flexibility: Swap integrations per-user, per-workspace, or per-request. A/B test different workflows without touching core logic.

Observability: Each middleware can add its own logging, metrics, and tracing.

Type safety: The middleware signatures enforce that you can't accidentally break the execution chain.


The Shift in Mental Model

The old way of thinking: "I need a tool that does X, Y, and Z."

The middleware way: "I need a tool that does X. I'll compose Y and Z as layers around it."

This is the same shift that made Express middleware, Redux middleware, and fetch interceptors so powerful. It's separation of concerns applied to AI agent architecture.

If you're building agents that need to coordinate multiple services, inject user context, or trigger workflows based on tool results, the middleware API is worth adopting. It transforms brittle, monolithic tool implementations into composable, maintainable systems.


Shawn Zhu is the founding engineer and head of AIOps at Ariso. Previously worked in IBM, LifeOmic and FountainLife. He co-created Zori AI medical assistant.

Who's Ivan?

Who's Ivan you ask? He writes half our code, and he's free to use. Check it out on npm or github.