onal:* Step D executes only if Step C output meets specific criteria.
4. Error Handling & Retries: Implement circuit breakers and retry logic per step. Failures in step A should not silently corrupt step B.
5. Caching Strategy: Cache intermediate results for idempotent steps to reduce latency and cost on repeated inputs.
TypeScript Implementation
The following implementation demonstrates a robust, typed chain executor using Zod for validation and an asynchronous execution model.
import { z } from 'zod';
import { createLLMClient, LLMConfig } from './llm-client';
// --- Type Definitions ---
export interface ChainStep<TInput, TOutput> {
id: string;
name: string;
inputSchema: z.ZodType<TInput>;
outputSchema: z.ZodType<TOutput>;
promptTemplate: string;
llmConfig: LLMConfig;
execute: (input: TInput, context: ChainContext) => Promise<TOutput>;
maxRetries?: number;
timeoutMs?: number;
}
export interface ChainContext {
traceId: string;
metadata: Record<string, unknown>;
stepResults: Record<string, unknown>;
}
export interface ChainResult<TFinal> {
success: boolean;
data?: TFinal;
errors: ChainError[];
metrics: ChainMetrics;
}
interface ChainError {
stepId: string;
message: string;
stack?: string;
}
interface ChainMetrics {
totalDurationMs: number;
stepDurations: Record<string, number>;
tokenUsage: number;
}
// --- Chain Executor ---
export class ChainExecutor {
private steps: ChainStep<any, any>[];
private llmClient: ReturnType<typeof createLLMClient>;
constructor(steps: ChainStep<any, any>[], llmClient: ReturnType<typeof createLLMClient>) {
this.steps = steps;
this.llmClient = llmClient;
}
async execute<TInput, TFinal>(
input: TInput,
context: ChainContext
): Promise<ChainResult<TFinal>> {
const startTime = Date.now();
const errors: ChainError[] = [];
const stepDurations: Record<string, number> = {};
let currentInput: any = input;
let totalTokens = 0;
for (const step of this.steps) {
const stepStart = Date.now();
try {
// Validate Input
const validatedInput = step.inputSchema.parse(currentInput);
// Execute Step with Retry Logic
const result = await this.executeStepWithRetry(step, validatedInput, context);
// Validate Output
const validatedOutput = step.outputSchema.parse(result.output);
// Update Context and Flow
context.stepResults[step.id] = validatedOutput;
currentInput = validatedOutput;
totalTokens += result.tokenUsage;
stepDurations[step.id] = Date.now() - stepStart;
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
errors.push({
stepId: step.id,
message: error.message,
stack: error.stack,
});
// Fail-fast strategy; could be modified to continue with partial results
break;
}
}
return {
success: errors.length === 0,
data: errors.length === 0 ? (currentInput as TFinal) : undefined,
errors,
metrics: {
totalDurationMs: Date.now() - startTime,
stepDurations,
tokenUsage: totalTokens,
},
};
}
private async executeStepWithRetry<TInput, TOutput>(
step: ChainStep<TInput, TOutput>,
input: TInput,
context: ChainContext,
retries = 0
): Promise<{ output: TOutput; tokenUsage: number }> {
const maxRetries = step.maxRetries || 2;
const timeout = step.timeoutMs || 10000;
try {
const response = await Promise.race([
this.llmClient.generate(step.promptTemplate, input, step.llmConfig, context),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Step ${step.id} timed out`)), timeout)
),
]);
return response;
} catch (err) {
if (retries < maxRetries) {
await new Promise(res => setTimeout(res, Math.pow(2, retries) * 100));
return this.executeStepWithRetry(step, input, context, retries + 1);
}
throw err;
}
}
}
Architecture Decisions
- Typed Contracts: Using Zod schemas enforces structural integrity. If Step 1 outputs a string where Step 2 expects an object, the chain fails immediately with a clear validation error, preventing silent corruption.
- Context Isolation: The
ChainContext object carries metadata and trace IDs but isolates step results. Steps access previous results via context.stepResults, preventing accidental dependency on raw prompt text from prior steps.
- Retry Granularity: Retries are applied at the step level. Transient API failures in one step do not require re-executing successful prior steps, optimizing cost and latency.
- Model Routing: The
llmConfig per step allows routing. Extraction steps can use a fast, low-cost model, while reasoning steps use a high-capability model.
Pitfall Guide
1. Context Bleed
Mistake: Passing the entire output of Step A as the input to Step B, including irrelevant details.
Impact: Increases token cost and introduces noise that distracts the LLM in subsequent steps.
Fix: Use intermediate extraction steps to filter data. Define output schemas that only include fields required by downstream steps.
2. Schema Drift
Mistake: Relying on free-text outputs between steps without validation.
Impact: Downstream steps receive malformed data, causing runtime errors or hallucinations.
Fix: Always validate step outputs against a Zod schema. If validation fails, trigger a retry with a correction prompt or fail the chain.
3. Latency Accumulation
Mistake: Building long sequential chains without considering cumulative latency.
Impact: User-facing chains with 5+ steps can exceed acceptable response times (>2s).
Fix: Identify independent steps and implement parallel execution. Use streaming for intermediate steps if the UX allows.
4. Cascade Failures
Mistake: A single step failure halts the entire workflow without recovery.
Impact: Poor user experience and lost opportunities for partial success.
Fix: Implement fallback strategies. For non-critical steps, use default values or cached results. For critical steps, return structured error responses to the client.
5. Over-Chaining
Mistake: Decomposing tasks too granularly, creating unnecessary steps.
Impact: Increased latency and cost without accuracy gains.
Fix: Benchmark chain depth. If adding a step does not improve accuracy by >5%, remove it. Merge steps that share the same cognitive load and model requirements.
6. Prompt Leakage
Mistake: Including sensitive data in intermediate prompts that are logged or stored.
Impact: Security vulnerabilities and compliance violations.
Fix: Implement data masking in the orchestration layer. Ensure logs strip PII before recording step inputs/outputs.
7. Evaluation Gap
Mistake: Testing the chain only with happy-path inputs.
Impact: Chains often fail on edge cases or adversarial inputs.
Fix: Build an evaluation harness that runs the chain against a dataset of edge cases. Monitor schema validation failure rates in production.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Simple Data Extraction | Monolithic Prompt | Low complexity; single step is sufficient and fastest. | Lowest |
| Complex Reasoning + Formatting | Prompt Chaining | Decomposition improves accuracy; schema enforcement ensures format. | Medium |
| Dynamic Exploration | Agentic Loop | Requires iterative decision-making based on intermediate results. | High |
| High-Volume, Low-Latency | Fine-Tuning + Chain | Fine-tuned models reduce prompt size and steps; chain handles orchestration. | High Setup, Low Opex |
| Strict Compliance Required | Chaining with Validation | Step-level validation provides audit trails and error isolation. | Medium |
Configuration Template
Use this TypeScript configuration to define and bootstrap a production chain.
// chain-config.ts
import { z } from 'zod';
import { ChainStep, ChainContext } from './chain-executor';
// Step 1: Extraction
const extractStep: ChainStep<string, { entities: string[]; sentiment: string }> = {
id: 'extract',
name: 'Entity Extraction',
inputSchema: z.string(),
outputSchema: z.object({
entities: z.array(z.string()),
sentiment: z.enum(['positive', 'negative', 'neutral']),
}),
promptTemplate: 'Extract entities and sentiment from: {{input}}',
llmConfig: { model: 'gpt-4o-mini', temperature: 0 },
maxRetries: 2,
timeoutMs: 5000,
execute: async (input, context) => {
// Implementation calls LLM client
return { entities: [], sentiment: 'neutral' }; // Placeholder
},
};
// Step 2: Analysis
const analysisStep: ChainStep<
{ entities: string[]; sentiment: string },
{ report: string }
> = {
id: 'analyze',
name: 'Generate Report',
inputSchema: z.object({
entities: z.array(z.string()),
sentiment: z.string(),
}),
outputSchema: z.object({ report: z.string() }),
promptTemplate: 'Generate a report for entities: {{entities}} with sentiment: {{sentiment}}',
llmConfig: { model: 'gpt-4o', temperature: 0.2 },
maxRetries: 1,
timeoutMs: 10000,
execute: async (input, context) => {
return { report: '' }; // Placeholder
},
};
export const analysisChainConfig = [extractStep, analysisStep];
Quick Start Guide
- Install Dependencies:
npm install zod @anthropic-ai/sdk openai
- Define Your Schema:
Create Zod schemas for your task inputs and expected outputs. This defines the contract for your chain.
- Write Chain Steps:
Implement individual
ChainStep objects with prompts, validation, and execution logic. Use the provided ChainExecutor class to run them.
- Bootstrap and Execute:
Instantiate the executor with your steps and LLM client. Call
execute(input, context) and handle the ChainResult.
const executor = new ChainExecutor(chainConfig, llmClient);
const result = await executor.execute(rawInput, { traceId: '123', metadata: {}, stepResults: {} });
if (result.success) console.log(result.data);
else console.error(result.errors);
- Monitor and Iterate:
Deploy with observability. Track schema validation failures and latency. Refine prompts and schemas based on production metrics.