What is Prompt Chaining?
Prompt chaining breaks complex tasks into a sequence of simpler prompts, where the output of one prompt becomes the input for the next. This approach improves accuracy, enables complex workflows, and makes debugging easier.
Think of it like breaking a large function into smaller, focused functions - each prompt does one thing well.
🔗 Why Use Prompt Chaining?
- Better Accuracy: Smaller tasks are easier for models to handle correctly
- Easier Debugging: Identify exactly where things go wrong
- Flexibility: Swap out individual steps without changing others
- Token Efficiency: Each step only includes relevant context
Single Prompt vs Chain
// ❌ Single Complex Prompt (error-prone)
"Take this code, identify bugs, fix them, add error handling,
write tests, generate documentation, and suggest performance
improvements. Return everything formatted as markdown."
// ✅ Prompt Chain (reliable)
// Step 1: Analyze code
const analysis = await prompt(`
Analyze this code for potential issues.
Return a JSON array of issues found:
[{ "type": "bug|style|perf", "line": number, "issue": string }]
Code: ${code}
`);
// Step 2: Fix issues based on analysis
const fixedCode = await prompt(`
Fix these issues in the code:
Issues: ${analysis}
Original Code: ${code}
Return only the fixed code.
`);
// Step 3: Add error handling
const robustCode = await prompt(`
Add comprehensive error handling to this code.
Include try-catch blocks and input validation.
Code: ${fixedCode}
`);
// Step 4: Generate tests
const tests = await prompt(`
Write unit tests for this code using Jest.
Cover: happy path, edge cases, error cases.
Code: ${robustCode}
`);
Common Chaining Patterns
// Pattern 1: Sequential Processing
// Each step builds on the previous
input → [Extract] → data → [Transform] → result → [Format] → output
// Example: Document Processing
const text = await extractTextFromPDF(file);
const entities = await extractEntities(text);
const summary = await summarizeWithEntities(text, entities);
const report = await formatAsReport(summary, entities);
// Pattern 2: Map-Reduce
// Process items in parallel, then combine
items.map(item => process(item)) → [Combine] → result
// Example: Code Review
const files = ['a.ts', 'b.ts', 'c.ts'];
const reviews = await Promise.all(
files.map(file => reviewFile(file))
);
const summary = await summarizeReviews(reviews);
// Pattern 3: Branching
// Different paths based on classification
input → [Classify] → type →
type A → [Process A]
type B → [Process B]
type C → [Process C]
// Example: Customer Support
const category = await classifyTicket(ticket);
switch (category) {
case 'technical': return await handleTechnical(ticket);
case 'billing': return await handleBilling(ticket);
case 'feature': return await handleFeatureRequest(ticket);
}
Decomposition Strategy
// Break down complex task into steps
// Original Complex Task:
"Create a full REST API for a todo app"
// Decomposed:
const steps = [
// 1. Design
"Design the data model for a todo app with users, " +
"lists, and items. Return as TypeScript interfaces.",
// 2. API Specification
"Based on these types, design REST endpoints. " +
"Return OpenAPI spec for CRUD operations.",
// 3. Implementation - Routes
"Implement Express routes for these endpoints. " +
"Include input validation.",
// 4. Implementation - Controllers
"Implement controller functions for each route. " +
"Include error handling.",
// 5. Database Layer
"Implement database functions using Prisma " +
"for these operations.",
// 6. Tests
"Write integration tests for these endpoints " +
"using supertest."
];
// Execute sequentially, passing context forward
let context = {};
for (const step of steps) {
const result = await prompt(step, context);
context = { ...context, ...result };
}
Verification Chain
// Chain includes verification steps
async function generateAndVerify(task) {
// Step 1: Generate
const code = await prompt(`
Generate code for: ${task}
Return only the code.
`);
// Step 2: Review
const review = await prompt(`
Review this code for bugs and issues.
Return JSON: { "issues": [], "isValid": boolean }
Code: ${code}
`);
// Step 3: Fix if needed
if (!review.isValid) {
const fixedCode = await prompt(`
Fix these issues in the code:
Issues: ${JSON.stringify(review.issues)}
Code: ${code}
`);
// Step 4: Verify fix
return await generateAndVerify(task); // Retry
}
return code;
}
// Self-critique pattern
async function generateWithCritique(task) {
const draft = await prompt(`Generate: ${task}`);
const critique = await prompt(`
Critique this output. What could be improved?
Output: ${draft}
`);
const final = await prompt(`
Improve this output based on the critique:
Original: ${draft}
Critique: ${critique}
`);
return final;
}
Real-World Example: Code Migration
// Migrating code from JavaScript to TypeScript
async function migrateToTypeScript(jsCode) {
// Step 1: Analyze structure
const analysis = await prompt(`
Analyze this JavaScript code structure.
Identify: functions, classes, exported items, dependencies.
Return as JSON.
Code: ${jsCode}
`);
// Step 2: Infer types
const types = await prompt(`
Based on this code analysis, infer TypeScript types.
Create interfaces for objects, type parameters, return types.
Analysis: ${analysis}
Original Code: ${jsCode}
`);
// Step 3: Convert code
const tsCode = await prompt(`
Convert this JavaScript to TypeScript.
Use these inferred types: ${types}
JavaScript: ${jsCode}
`);
// Step 4: Add strict checks
const strictCode = await prompt(`
Make this TypeScript code strict-mode compatible.
- Add null checks
- Handle undefined cases
- Use proper type narrowing
Code: ${tsCode}
`);
// Step 5: Verify
const verification = await prompt(`
Verify this TypeScript code:
1. Are all types properly defined?
2. Are there any 'any' types that should be specific?
3. Does it handle all edge cases?
Return issues as JSON array.
Code: ${strictCode}
`);
return { code: strictCode, issues: verification };
}
Context Management
// Managing context across chain steps
// Strategy 1: Pass relevant context only
async function chainWithContext(input) {
const step1Result = await prompt(`
Step 1 task...
Input: ${input}
`);
// Only pass what's needed for step 2
const step2Result = await prompt(`
Step 2 task...
Previous: ${step1Result.summary} // Not full output
`);
return step2Result;
}
// Strategy 2: Accumulated context object
class ChainContext {
private data: Record<string, any> = {};
set(key: string, value: any) {
this.data[key] = value;
}
get(key: string) {
return this.data[key];
}
toPromptContext() {
return Object.entries(this.data)
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
.join('\n');
}
}
// Strategy 3: Summarize between steps
async function chainWithSummary(steps) {
let context = "";
for (const step of steps) {
const result = await prompt(step + `\nContext: ${context}`);
// Summarize for next step
context = await prompt(`
Summarize the key information from this for the next step:
${result}
Keep it under 100 words.
`);
}
}
✅ Chaining Best Practices
- • Each step should have a single, clear purpose
- • Use structured output (JSON) between steps for reliability
- • Include validation/verification steps for critical tasks
- • Pass only relevant context to keep prompts focused
- • Add error handling and retry logic
- • Log intermediate results for debugging