DRAFT Agentic Design Patterns - Prompt Chaining"

ailangchainlangraphnextjsprompt-engineeringagents
By sko X opus 4.19/20/202513 min read

Build production-ready sequential AI workflows that decompose complex tasks into manageable steps, passing outputs between specialized prompts to achieve sophisticated reasoning and processing capabilities in your Next.js applications.

Mental Model: Assembly Line for AI

Think of prompt chaining like a modern assembly line in a Tesla factory. Each station (prompt) performs a specific operation - one installs wheels, another paints, a third inspects quality. The output from each station becomes the input for the next, creating a refined final product. In AI terms, instead of building cars, you're refining information: first extracting data, then analyzing it, then formatting it, and finally validating it. Just as assembly lines revolutionized manufacturing efficiency, prompt chaining revolutionizes how we handle complex AI tasks by breaking them into specialized, manageable operations that can be optimized independently.

Basic Sequential Chain Implementation

1. Simple Text Processing Chain

// lib/chains/basic-chain.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { RunnableSequence } from '@langchain/core/runnables';

export function createBasicChain() {
  const model = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-flash',
    temperature: 0,
    maxRetries: 3,
  });

  // Step 1: Summarize the input
  const summarizePrompt = PromptTemplate.fromTemplate(
    `Summarize the following text in 2-3 sentences:
    
    {text}`
  );

  // Step 2: Extract key points
  const extractPrompt = PromptTemplate.fromTemplate(
    `Extract 3-5 key points from this summary:
    
    {summary}`
  );

  // Step 3: Generate action items
  const actionPrompt = PromptTemplate.fromTemplate(
    `Based on these key points, generate actionable recommendations:
    
    {keyPoints}`
  );

  // Build the chain using LCEL
  const chain = RunnableSequence.from([
    // First step: summarize
    summarizePrompt,
    model,
    new StringOutputParser(),
    // Pass to next step
    (summary) => ({ summary }),
    extractPrompt,
    model,
    new StringOutputParser(),
    // Pass to final step
    (keyPoints) => ({ keyPoints }),
    actionPrompt,
    model,
    new StringOutputParser(),
  ]);

  return chain;
}

Creates a three-step chain that progressively refines text from raw input to actionable recommendations.

2. API Route with Streaming

// app/api/basic-chain/route.ts
import { createBasicChain } from '@/lib/chains/basic-chain';
import { NextResponse } from 'next/server';

export const runtime = 'nodejs';
export const maxDuration = 60;

export async function POST(req: Request) {
  try {
    const { text } = await req.json();
    
    if (!text || text.length < 50) {
      return NextResponse.json(
        { error: 'Please provide text with at least 50 characters' },
        { status: 400 }
      );
    }

    const chain = createBasicChain();
    const result = await chain.invoke({ text });

    return NextResponse.json({
      success: true,
      result,
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    console.error('Chain execution error:', error);
    return NextResponse.json(
      { error: 'Failed to process chain' },
      { status: 500 }
    );
  }
}

Handles the chain execution with proper error handling and validation for serverless deployment.

3. Frontend Integration with TanStack Query

// components/BasicChainInterface.tsx
'use client';

import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { debounce } from 'es-toolkit';

interface ChainResponse {
  success: boolean;
  result: string;
  timestamp: string;
}

export default function BasicChainInterface() {
  const [text, setText] = useState('');
  const [charCount, setCharCount] = useState(0);

  const processChain = useMutation<ChainResponse, Error, string>({
    mutationFn: async (inputText: string) => {
      const response = await fetch('/api/basic-chain', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: inputText }),
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error || 'Chain processing failed');
      }

      return response.json();
    },
    retry: 2,
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
  });

  const handleTextChange = debounce((value: string) => {
    setCharCount(value.length);
  }, 300);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (text.length >= 50) {
      processChain.mutate(text);
    }
  };

  return (
    <div className="card w-full bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">Text Processing Chain</h2>
        
        <form onSubmit={handleSubmit} className="space-y-4">
          <div className="form-control">
            <label className="label">
              <span className="label-text">Input Text</span>
              <span className="label-text-alt">{charCount} characters</span>
            </label>
            <textarea
              className="textarea textarea-bordered h-32"
              placeholder="Enter text to process (min 50 characters)..."
              value={text}
              onChange={(e) => {
                setText(e.target.value);
                handleTextChange(e.target.value);
              }}
              disabled={processChain.isPending}
            />
          </div>

          <button
            type="submit"
            className="btn btn-primary"
            disabled={processChain.isPending || text.length < 50}
          >
            {processChain.isPending ? (
              <>
                <span className="loading loading-spinner"></span>
                Processing Chain...
              </>
            ) : (
              'Process Text'
            )}
          </button>
        </form>

        {processChain.isError && (
          <div className="alert alert-error mt-4">
            <span>{processChain.error.message}</span>
          </div>
        )}

        {processChain.isSuccess && (
          <div className="card bg-base-200 mt-4">
            <div className="card-body">
              <h3 className="font-semibold">Result:</h3>
              <p className="whitespace-pre-wrap">{processChain.data.result}</p>
              <p className="text-xs text-base-content/60 mt-2">
                Processed at: {new Date(processChain.data.timestamp).toLocaleString()}
              </p>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

React component with debounced character counting and comprehensive error handling using TanStack Query.

Advanced Multi-Step Chain with State Management

1. Document Analysis Chain with LangGraph

// lib/chains/document-analyzer.ts
import { StateGraph, START, END, Annotation } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { BaseMessage, HumanMessage, AIMessage } from '@langchain/core/messages';
import { z } from 'zod';
import { StructuredOutputParser } from '@langchain/core/output_parsers';
import { isNil, chunk } from 'es-toolkit';

// Define state schema with Zod
const DocumentMetadata = z.object({
  title: z.string(),
  category: z.string(),
  confidence: z.number().min(0).max(1),
  keywords: z.array(z.string()),
});

// Define graph state
const GraphState = Annotation.Root({
  document: Annotation<string>(),
  chunks: Annotation<string[]>(),
  metadata: Annotation<z.infer<typeof DocumentMetadata>>(),
  summary: Annotation<string>(),
  insights: Annotation<string[]>(),
  messages: Annotation<BaseMessage[]>({
    reducer: (current, update) => current.concat(update),
    default: () => [],
  }),
});

export function createDocumentAnalyzer() {
  const model = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-pro',
    temperature: 0.1,
    maxRetries: 3,
  });

  const metadataParser = StructuredOutputParser.fromZodSchema(DocumentMetadata);

  const workflow = new StateGraph(GraphState)
    // Node 1: Chunk document
    .addNode('chunk_document', async (state) => {
      const doc = state.document;
      // Chunk into 500 character segments with 50 char overlap
      const chunks = chunk(doc.split(' '), 100).map(words => words.join(' '));
      
      return {
        chunks,
        messages: [new AIMessage(`Document chunked into ${chunks.length} segments`)],
      };
    })
    // Node 2: Extract metadata
    .addNode('extract_metadata', async (state) => {
      const prompt = `Analyze this document and extract metadata.
      
Document: ${state.chunks[0]}

${metadataParser.getFormatInstructions()}`;

      const response = await model.invoke([new HumanMessage(prompt)]);
      const metadata = await metadataParser.parse(response.content as string);
      
      return {
        metadata,
        messages: [new AIMessage(`Metadata extracted: ${metadata.category}`)],
      };
    })
    // Node 3: Generate summary
    .addNode('generate_summary', async (state) => {
      const combinedText = state.chunks.slice(0, 3).join('\n\n');
      const prompt = `Create a comprehensive summary of this document.
      Category: ${state.metadata.category}
      
      Document excerpt:
      ${combinedText}`;

      const response = await model.invoke([new HumanMessage(prompt)]);
      
      return {
        summary: response.content as string,
        messages: [new AIMessage('Summary generated')],
      };
    })
    // Node 4: Extract insights
    .addNode('extract_insights', async (state) => {
      const prompt = `Based on this summary and metadata, provide 3-5 key insights:
      
      Category: ${state.metadata.category}
      Keywords: ${state.metadata.keywords.join(', ')}
      Summary: ${state.summary}`;

      const response = await model.invoke([new HumanMessage(prompt)]);
      const insights = (response.content as string)
        .split('\n')
        .filter(line => line.trim().length > 0);
      
      return {
        insights,
        messages: [new AIMessage(`${insights.length} insights extracted`)],
      };
    })
    // Define edges
    .addEdge(START, 'chunk_document')
    .addEdge('chunk_document', 'extract_metadata')
    .addEdge('extract_metadata', 'generate_summary')
    .addEdge('generate_summary', 'extract_insights')
    .addEdge('extract_insights', END);

  return workflow.compile();
}

Implements a stateful document processing pipeline with metadata extraction and insight generation.

2. Streaming API Route for Document Analysis

// app/api/analyze-document/route.ts
import { createDocumentAnalyzer } from '@/lib/chains/document-analyzer';
import { HumanMessage } from '@langchain/core/messages';

export const runtime = 'nodejs';
export const maxDuration = 120;

export async function POST(req: Request) {
  const { document } = await req.json();
  
  if (!document || document.length < 100) {
    return new Response(
      JSON.stringify({ error: 'Document must be at least 100 characters' }),
      { status: 400 }
    );
  }

  const encoder = new TextEncoder();
  const stream = new TransformStream();
  const writer = stream.writable.getWriter();

  const analyzer = createDocumentAnalyzer();

  (async () => {
    try {
      const eventStream = await analyzer.stream({
        document,
        chunks: [],
        metadata: null,
        summary: '',
        insights: [],
        messages: [],
      });

      for await (const event of eventStream) {
        const update = {
          type: 'update',
          node: Object.keys(event)[0],
          data: event,
          timestamp: new Date().toISOString(),
        };

        await writer.write(
          encoder.encode(`data: ${JSON.stringify(update)}\n\n`)
        );
      }

      await writer.write(
        encoder.encode(`data: ${JSON.stringify({ type: 'complete' })}\n\n`)
      );
    } catch (error) {
      await writer.write(
        encoder.encode(`data: ${JSON.stringify({ 
          type: 'error', 
          error: error instanceof Error ? error.message : 'Unknown error' 
        })}\n\n`)
      );
    } finally {
      await writer.close();
    }
  })();

  return new Response(stream.readable, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

Streams each node's execution results in real-time using Server-Sent Events.

3. Advanced Chain with Error Recovery

// lib/chains/resilient-chain.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { RunnableSequence, RunnableWithFallbacks } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { retry, delay } from 'es-toolkit';
import { kv } from '@vercel/kv';

interface ChainCache {
  get: (key: string) => Promise<string | null>;
  set: (key: string, value: string, ttl?: number) => Promise<void>;
}

class VercelKVCache implements ChainCache {
  async get(key: string): Promise<string | null> {
    try {
      return await kv.get(key);
    } catch {
      return null;
    }
  }

  async set(key: string, value: string, ttl = 3600): Promise<void> {
    try {
      await kv.set(key, value, { ex: ttl });
    } catch {
      // Silently fail cache writes
    }
  }
}

export function createResilientChain() {
  const cache = new VercelKVCache();
  
  // Primary model with fallback
  const primaryModel = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-pro',
    temperature: 0,
    maxRetries: 2,
  });

  const fallbackModel = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-flash',
    temperature: 0,
    maxRetries: 1,
  });

  const modelWithFallback = primaryModel.withFallbacks({
    fallbacks: [fallbackModel],
  });

  // Create cached prompt executor
  const cachedExecutor = async (prompt: string, input: any) => {
    const cacheKey = `chain:${prompt.substring(0, 20)}:${JSON.stringify(input)}`;
    
    // Check cache
    const cached = await cache.get(cacheKey);
    if (cached) return cached;

    // Execute with retry logic
    const result = await retry(
      async () => {
        const template = PromptTemplate.fromTemplate(prompt);
        const chain = template.pipe(modelWithFallback).pipe(new StringOutputParser());
        return await chain.invoke(input);
      },
      {
        times: 3,
        delay: 1000,
        onError: async (error, attemptNumber) => {
          console.error(`Attempt ${attemptNumber} failed:`, error);
          await delay(attemptNumber * 1000); // Exponential backoff
        },
      }
    );

    // Cache result
    await cache.set(cacheKey, result);
    return result;
  };

  // Build resilient chain
  const chain = RunnableSequence.from([
    async (input: { query: string }) => {
      // Step 1: Query understanding with caching
      const understanding = await cachedExecutor(
        'Rephrase this query for clarity: {query}',
        input
      );
      return { understanding };
    },
    async (state: { understanding: string }) => {
      // Step 2: Research with fallback
      const research = await cachedExecutor(
        'Provide detailed research on: {understanding}',
        state
      );
      return { ...state, research };
    },
    async (state: { understanding: string; research: string }) => {
      // Step 3: Synthesis with validation
      const synthesis = await cachedExecutor(
        `Synthesize this research into actionable insights:
        Topic: {understanding}
        Research: {research}`,
        state
      );
      
      // Validate output
      if (synthesis.length < 100) {
        throw new Error('Synthesis too short, retrying...');
      }
      
      return { ...state, synthesis };
    },
  ]);

  return chain;
}

Implements production-ready chain with caching, fallbacks, and retry logic using es-toolkit utilities.

4. Parallel Chain Processing

// lib/chains/parallel-processor.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { RunnableParallel, RunnableSequence } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { chunk, uniqBy } from 'es-toolkit';

export function createParallelProcessor() {
  const model = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-flash',
    temperature: 0.3,
  });

  // Define parallel analysis branches
  const sentimentAnalysis = PromptTemplate.fromTemplate(
    'Analyze the sentiment of this text (positive/negative/neutral): {text}'
  ).pipe(model).pipe(new StringOutputParser());

  const entityExtraction = PromptTemplate.fromTemplate(
    'Extract all named entities (people, places, organizations) from: {text}'
  ).pipe(model).pipe(new StringOutputParser());

  const topicClassification = PromptTemplate.fromTemplate(
    'Classify the main topic of this text: {text}'
  ).pipe(model).pipe(new StringOutputParser());

  const keywordExtraction = PromptTemplate.fromTemplate(
    'Extract 5-10 keywords from this text: {text}'
  ).pipe(model).pipe(new StringOutputParser());

  // Create parallel execution
  const parallelAnalysis = RunnableParallel({
    sentiment: sentimentAnalysis,
    entities: entityExtraction,
    topic: topicClassification,
    keywords: keywordExtraction,
  });

  // Synthesis step
  const synthesisPrompt = PromptTemplate.fromTemplate(`
    Create a comprehensive analysis report based on:
    
    Sentiment: {sentiment}
    Entities: {entities}
    Topic: {topic}
    Keywords: {keywords}
    
    Format as a structured report with sections.
  `);

  // Complete chain: parallel analysis then synthesis
  const chain = RunnableSequence.from([
    parallelAnalysis,
    synthesisPrompt,
    model,
    new StringOutputParser(),
  ]);

  return chain;
}

// Batch processor for multiple documents
export async function processBatch(documents: string[]) {
  const processor = createParallelProcessor();
  const BATCH_SIZE = 5;
  
  // Process in batches to avoid rate limits
  const batches = chunk(documents, BATCH_SIZE);
  const results = [];

  for (const batch of batches) {
    const batchResults = await Promise.all(
      batch.map(async (doc) => {
        try {
          const result = await processor.invoke({ text: doc });
          return { success: true, result, document: doc };
        } catch (error) {
          return { 
            success: false, 
            error: error instanceof Error ? error.message : 'Unknown error',
            document: doc 
          };
        }
      })
    );
    results.push(...batchResults);
    
    // Rate limiting delay between batches
    if (batches.indexOf(batch) < batches.length - 1) {
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }

  return results;
}

Executes multiple analysis tasks in parallel then synthesizes results, with batch processing support.

5. Context-Aware Chain with Memory

// lib/chains/contextual-chain.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { BufferMemory } from 'langchain/memory';
import { ConversationChain } from 'langchain/chains';
import { PromptTemplate } from '@langchain/core/prompts';
import { MessagesPlaceholder } from '@langchain/core/prompts';
import { RunnableSequence } from '@langchain/core/runnables';

export class ContextualChain {
  private memory: BufferMemory;
  private chain: RunnableSequence;
  private model: ChatGoogleGenerativeAI;

  constructor() {
    this.model = new ChatGoogleGenerativeAI({
      modelName: 'gemini-2.5-pro',
      temperature: 0.7,
    });

    this.memory = new BufferMemory({
      returnMessages: true,
      memoryKey: 'history',
      inputKey: 'input',
      outputKey: 'output',
    });

    this.initializeChain();
  }

  private initializeChain() {
    const contextPrompt = PromptTemplate.fromTemplate(`
      You are analyzing a document step by step.
      Previous context: {history}
      
      Current task: {task}
      Current input: {input}
      
      Provide a detailed response that builds on previous analysis.
    `);

    this.chain = RunnableSequence.from([
      async (input: { task: string; input: string }) => {
        const history = await this.memory.loadMemoryVariables({});
        return { ...input, history: history.history || '' };
      },
      contextPrompt,
      this.model,
      async (response) => {
        const content = response.content as string;
        // Save to memory
        await this.memory.saveContext(
          { input: this.lastInput },
          { output: content }
        );
        return content;
      },
    ]);
  }

  private lastInput: string = '';

  async process(task: string, input: string): Promise<string> {
    this.lastInput = `${task}: ${input}`;
    return await this.chain.invoke({ task, input });
  }

  async reset() {
    await this.memory.clear();
  }

  async getHistory(): Promise<string> {
    const history = await this.memory.loadMemoryVariables({});
    return history.history || 'No history available';
  }
}

// Usage in API route
export async function processWithContext(
  sessionId: string,
  task: string,
  input: string
) {
  // Store chains per session
  const chainStore = global as any;
  chainStore.contextChains = chainStore.contextChains || new Map();
  
  if (!chainStore.contextChains.has(sessionId)) {
    chainStore.contextChains.set(sessionId, new ContextualChain());
  }
  
  const chain = chainStore.contextChains.get(sessionId);
  return await chain.process(task, input);
}

Maintains conversation context across multiple chain executions for stateful document analysis.

6. Production-Ready Chain with Monitoring

// lib/chains/monitored-chain.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { RunnableSequence } from '@langchain/core/runnables';
import { PromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';

interface ChainMetrics {
  executionTime: number;
  tokenCount: number;
  cost: number;
  steps: Array<{
    name: string;
    duration: number;
    tokens: number;
  }>;
}

export class MonitoredChain {
  private metrics: ChainMetrics[] = [];
  
  async executeWithMonitoring(input: string): Promise<{
    result: string;
    metrics: ChainMetrics;
  }> {
    const startTime = Date.now();
    const stepMetrics: ChainMetrics['steps'] = [];
    
    const model = new ChatGoogleGenerativeAI({
      modelName: 'gemini-2.5-flash',
      temperature: 0,
      callbacks: [
        {
          handleLLMStart: async (llm, prompts) => {
            console.log('LLM Start:', prompts[0].substring(0, 100));
          },
          handleLLMEnd: async (output) => {
            const tokens = output.llmOutput?.tokenUsage?.totalTokens || 0;
            stepMetrics[stepMetrics.length - 1].tokens = tokens;
          },
        },
      ],
    });

    // Step 1: Classification
    const stepStart = Date.now();
    stepMetrics.push({ name: 'classification', duration: 0, tokens: 0 });
    
    const classificationPrompt = PromptTemplate.fromTemplate(
      'Classify this text into categories: {text}'
    );
    
    const classification = await classificationPrompt
      .pipe(model)
      .pipe(new StringOutputParser())
      .invoke({ text: input });
    
    stepMetrics[0].duration = Date.now() - stepStart;

    // Step 2: Enhancement
    const step2Start = Date.now();
    stepMetrics.push({ name: 'enhancement', duration: 0, tokens: 0 });
    
    const enhancementPrompt = PromptTemplate.fromTemplate(
      'Enhance this classification with details: {classification}'
    );
    
    const enhancement = await enhancementPrompt
      .pipe(model)
      .pipe(new StringOutputParser())
      .invoke({ classification });
    
    stepMetrics[1].duration = Date.now() - step2Start;

    // Calculate total metrics
    const totalTokens = stepMetrics.reduce((sum, step) => sum + step.tokens, 0);
    const metrics: ChainMetrics = {
      executionTime: Date.now() - startTime,
      tokenCount: totalTokens,
      cost: totalTokens * 0.000001, // Example pricing
      steps: stepMetrics,
    };

    this.metrics.push(metrics);

    return {
      result: enhancement,
      metrics,
    };
  }

  getAverageMetrics(): ChainMetrics | null {
    if (this.metrics.length === 0) return null;
    
    const avgTime = this.metrics.reduce((sum, m) => sum + m.executionTime, 0) / this.metrics.length;
    const avgTokens = this.metrics.reduce((sum, m) => sum + m.tokenCount, 0) / this.metrics.length;
    const avgCost = this.metrics.reduce((sum, m) => sum + m.cost, 0) / this.metrics.length;
    
    return {
      executionTime: Math.round(avgTime),
      tokenCount: Math.round(avgTokens),
      cost: avgCost,
      steps: [],
    };
  }
}

Tracks detailed metrics for each chain execution including timing, token usage, and costs.

7. Frontend Dashboard for Chain Monitoring

// components/ChainDashboard.tsx
'use client';

import { useState, useEffect } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { groupBy, mean, round } from 'es-toolkit';

interface ChainExecution {
  id: string;
  chainName: string;
  status: 'pending' | 'processing' | 'complete' | 'failed';
  startTime: string;
  endTime?: string;
  metrics?: {
    executionTime: number;
    tokenCount: number;
    cost: number;
  };
}

export default function ChainDashboard() {
  const [executions, setExecutions] = useState<ChainExecution[]>([]);
  
  // Fetch execution history
  const { data: history } = useQuery({
    queryKey: ['chain-history'],
    queryFn: async () => {
      const response = await fetch('/api/chain-history');
      return response.json();
    },
    refetchInterval: 5000,
  });

  // Execute new chain
  const executeChain = useMutation({
    mutationFn: async (chainConfig: { name: string; input: string }) => {
      const response = await fetch('/api/execute-chain', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(chainConfig),
      });
      return response.json();
    },
    onSuccess: (data) => {
      setExecutions(prev => [...prev, data]);
    },
  });

  // Calculate statistics
  const stats = executions.reduce(
    (acc, exec) => {
      if (exec.status === 'complete' && exec.metrics) {
        acc.totalExecutions++;
        acc.totalTokens += exec.metrics.tokenCount;
        acc.totalCost += exec.metrics.cost;
        acc.avgTime = mean([acc.avgTime, exec.metrics.executionTime]);
      } else if (exec.status === 'failed') {
        acc.failures++;
      }
      return acc;
    },
    {
      totalExecutions: 0,
      totalTokens: 0,
      totalCost: 0,
      avgTime: 0,
      failures: 0,
    }
  );

  return (
    <div className="p-6 space-y-6">
      {/* Stats Grid */}
      <div className="stats shadow w-full">
        <div className="stat">
          <div className="stat-title">Total Executions</div>
          <div className="stat-value">{stats.totalExecutions}</div>
          <div className="stat-desc">
            {stats.failures > 0 && `${stats.failures} failed`}
          </div>
        </div>
        
        <div className="stat">
          <div className="stat-title">Avg Execution Time</div>
          <div className="stat-value">{round(stats.avgTime / 1000, 2)}s</div>
          <div className="stat-desc">Per chain execution</div>
        </div>
        
        <div className="stat">
          <div className="stat-title">Total Tokens</div>
          <div className="stat-value">{stats.totalTokens.toLocaleString()}</div>
          <div className="stat-desc">
            ${round(stats.totalCost, 4)} total cost
          </div>
        </div>
      </div>

      {/* Execution Timeline */}
      <div className="card bg-base-100 shadow-xl">
        <div className="card-body">
          <h2 className="card-title">Recent Executions</h2>
          
          <div className="overflow-x-auto">
            <table className="table table-zebra">
              <thead>
                <tr>
                  <th>Chain</th>
                  <th>Status</th>
                  <th>Duration</th>
                  <th>Tokens</th>
                  <th>Cost</th>
                </tr>
              </thead>
              <tbody>
                {executions.slice(-10).reverse().map((exec) => (
                  <tr key={exec.id}>
                    <td>{exec.chainName}</td>
                    <td>
                      <div className={`badge ${
                        exec.status === 'complete' ? 'badge-success' :
                        exec.status === 'failed' ? 'badge-error' :
                        exec.status === 'processing' ? 'badge-warning' :
                        'badge-ghost'
                      }`}>
                        {exec.status}
                      </div>
                    </td>
                    <td>
                      {exec.metrics 
                        ? `${round(exec.metrics.executionTime / 1000, 2)}s`
                        : '-'}
                    </td>
                    <td>{exec.metrics?.tokenCount || '-'}</td>
                    <td>
                      {exec.metrics 
                        ? `$${round(exec.metrics.cost, 4)}`
                        : '-'}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>

      {/* Execute New Chain */}
      <div className="card bg-base-100 shadow-xl">
        <div className="card-body">
          <h2 className="card-title">Execute Chain</h2>
          
          <div className="form-control">
            <label className="label">
              <span className="label-text">Select Chain Type</span>
            </label>
            <select className="select select-bordered">
              <option>Basic Sequential</option>
              <option>Document Analyzer</option>
              <option>Parallel Processor</option>
              <option>Contextual Chain</option>
            </select>
          </div>
          
          <button 
            className="btn btn-primary"
            onClick={() => executeChain.mutate({
              name: 'Basic Sequential',
              input: 'Sample input text',
            })}
            disabled={executeChain.isPending}
          >
            {executeChain.isPending ? (
              <>
                <span className="loading loading-spinner"></span>
                Executing...
              </>
            ) : (
              'Execute Chain'
            )}
          </button>
        </div>
      </div>
    </div>
  );
}

Real-time dashboard for monitoring chain executions with statistics and cost tracking.

Conclusion

Prompt chaining transforms complex AI tasks into manageable, sequential operations that can be optimized, monitored, and scaled independently. By implementing these patterns with LangChain's LCEL and LangGraph's stateful workflows on Vercel's serverless platform, you can build production-ready AI applications that handle sophisticated reasoning tasks reliably. The key is starting with simple chains and progressively adding resilience features like caching, fallbacks, and monitoring as your application scales.