Advanced Multi-Step Task Orchestration with Claude Code
When building complex AI-powered applications with Claude Code, you’ll often need to orchestrate sophisticated multi-step workflows that involve dependencies, conditional logic, and dynamic adaptation. This guide covers advanced patterns for implementing robust task orchestration systems.
Table of Contents
- Task Dependency Management with DAGs
- Topological Sorting in Prompt Chains
- Dynamic Task Planning
- State Management for Long-Running Tasks
- Error Handling and Recovery
- Production Patterns and Examples
- Integration with Claude Code Features
Task Dependency Management with DAGs
Directed Acyclic Graphs (DAGs) provide a powerful model for representing task dependencies in complex workflows. In Claude Code, we can implement DAG-based orchestration to ensure tasks execute in the correct order while maximizing parallelization opportunities.
Core Concepts
A DAG-based task orchestration system consists of:
- Nodes: Individual tasks or prompts to be executed
- Edges: Dependencies between tasks
- Execution Engine: Manages task scheduling and execution
- State Manager: Tracks task status and manages intermediate results
Implementation Pattern
interface TaskNode {
id: string
name: string
prompt: string
dependencies: string[]
status: 'pending' | 'running' | 'completed' | 'failed'
result?: any
retryCount?: number
}
interface WorkflowDAG {
nodes: Map<string, TaskNode>
edges: Map<string, Set<string>>
}
class TaskOrchestrator {
private dag: WorkflowDAG
private claudeClient: ClaudeClient
constructor(dag: WorkflowDAG, claudeClient: ClaudeClient) {
this.dag = dag
this.claudeClient = claudeClient
}
async execute(): Promise<Map<string, any>> {
const results = new Map<string, any>()
const inProgress = new Set<string>()
while (this.hasUncompletedTasks()) {
const readyTasks = this.getReadyTasks(inProgress)
// Execute ready tasks in parallel
const promises = readyTasks.map(task =>
this.executeTask(task, results)
)
await Promise.allSettled(promises)
}
return results
}
private getReadyTasks(inProgress: Set<string>): TaskNode[] {
return Array.from(this.dag.nodes.values()).filter(node => {
if (node.status !== 'pending') return false
if (inProgress.has(node.id)) return false
// Check if all dependencies are completed
return node.dependencies.every(depId => {
const dep = this.dag.nodes.get(depId)
return dep?.status === 'completed'
})
})
}
private async executeTask(task: TaskNode, results: Map<string, any>): Promise<void> {
task.status = 'running'
try {
// Build context from dependencies
const context = this.buildContext(task, results)
// Execute with Claude
const result = await this.claudeClient.sendMessage({
messages: [{
role: 'user',
content: this.interpolatePrompt(task.prompt, context)
}]
})
task.status = 'completed'
task.result = result
results.set(task.id, result)
} catch (error) {
task.status = 'failed'
task.retryCount = (task.retryCount || 0) + 1
if (task.retryCount < 3) {
// Reset for retry
task.status = 'pending'
}
throw error
}
}
}Best Practices for DAG Design
- Keep Tasks Atomic: Each node should represent a single, well-defined operation
- Minimize Dependencies: Reduce coupling between tasks for better parallelization
- Use Caching: Store intermediate results to enable resumption and debugging
- Implement Timeouts: Prevent indefinite waiting on stuck tasks
Topological Sorting in Prompt Chains
Topological sorting ensures tasks execute in an order that respects all dependencies. This is crucial for prompt chains where outputs from one prompt serve as inputs to others.
Kahn’s Algorithm Implementation
class TopologicalSorter {
static sort(dag: WorkflowDAG): TaskNode[] | null {
const nodes = Array.from(dag.nodes.values())
const inDegree = new Map<string, number>()
// Calculate in-degrees
nodes.forEach(node => {
inDegree.set(node.id, 0)
})
dag.edges.forEach((destinations, source) => {
destinations.forEach(dest => {
inDegree.set(dest, (inDegree.get(dest) || 0) + 1)
})
})
// Find nodes with no dependencies
const queue: TaskNode[] = []
nodes.forEach(node => {
if (inDegree.get(node.id) === 0) {
queue.push(node)
}
})
const sorted: TaskNode[] = []
while (queue.length > 0) {
const current = queue.shift()!
sorted.push(current)
// Process neighbors
const neighbors = dag.edges.get(current.id) || new Set()
neighbors.forEach(neighborId => {
const degree = (inDegree.get(neighborId) || 0) - 1
inDegree.set(neighborId, degree)
if (degree === 0) {
const neighbor = dag.nodes.get(neighborId)
if (neighbor) queue.push(neighbor)
}
})
}
// Check for cycles
return sorted.length === nodes.length ? sorted : null
}
}Prompt Chain Patterns
Sequential Chaining
Used when each step depends on the previous one:
const sequentialChain: TaskNode[] = [
{
id: 'analyze',
name: 'Analyze Requirements',
prompt: 'Analyze the following requirements and identify key components: {requirements}',
dependencies: [],
status: 'pending'
},
{
id: 'design',
name: 'Design Architecture',
prompt: 'Based on the analysis: {analyze.result}, design a system architecture',
dependencies: ['analyze'],
status: 'pending'
},
{
id: 'implement',
name: 'Generate Implementation',
prompt: 'Implement the following architecture: {design.result}',
dependencies: ['design'],
status: 'pending'
}
]Parallel Chaining
Execute independent tasks simultaneously:
const parallelChain: TaskNode[] = [
{
id: 'requirements',
name: 'Gather Requirements',
prompt: 'Extract functional requirements from: {input}',
dependencies: [],
status: 'pending'
},
{
id: 'frontend_design',
name: 'Design Frontend',
prompt: 'Design frontend based on: {requirements.result}',
dependencies: ['requirements'],
status: 'pending'
},
{
id: 'backend_design',
name: 'Design Backend',
prompt: 'Design backend based on: {requirements.result}',
dependencies: ['requirements'],
status: 'pending'
},
{
id: 'integration',
name: 'Design Integration',
prompt: 'Integrate frontend ({frontend_design.result}) with backend ({backend_design.result})',
dependencies: ['frontend_design', 'backend_design'],
status: 'pending'
}
]Dynamic Task Planning
Dynamic task planning allows workflows to adapt based on intermediate results. This is essential for handling conditional logic and unknown task structures at design time.
Adaptive Workflow Pattern
interface DynamicTaskPlanner {
evaluateConditions(context: Map<string, any>): TaskNode[]
generateSubtasks(parentResult: any): TaskNode[]
shouldContinue(results: Map<string, any>): boolean
}
class AdaptiveOrchestrator extends TaskOrchestrator {
private planner: DynamicTaskPlanner
async executeDynamic(): Promise<Map<string, any>> {
const results = new Map<string, any>()
while (this.planner.shouldContinue(results)) {
// Generate next tasks based on current state
const newTasks = this.planner.evaluateConditions(results)
// Add to DAG
newTasks.forEach(task => {
this.dag.nodes.set(task.id, task)
this.updateDependencies(task)
})
// Execute one iteration
await this.executeIteration(results)
}
return results
}
}Claude-Powered Task Generation
Leverage Claude to dynamically generate task plans:
class ClaudeTaskPlanner implements DynamicTaskPlanner {
async generateSubtasks(parentResult: any): Promise<TaskNode[]> {
const planningPrompt = `
Given the following result from the previous task:
${JSON.stringify(parentResult, null, 2)}
Generate a task breakdown for the next steps. Return a JSON array of tasks with:
- id: unique identifier
- name: descriptive name
- prompt: the prompt to execute
- dependencies: array of task IDs this depends on
Focus on tasks that:
1. Build upon the previous result
2. Can be parallelized where possible
3. Lead toward the overall goal
`
const response = await claudeClient.sendMessage({
messages: [{ role: 'user', content: planningPrompt }]
})
return JSON.parse(response.content)
}
}State Management for Long-Running Tasks
Long-running workflows require robust state management to handle interruptions, resume from checkpoints, and maintain context across sessions.
Checkpoint System
interface WorkflowCheckpoint {
id: string
timestamp: Date
dag: WorkflowDAG
results: Map<string, any>
metadata: {
totalTasks: number
completedTasks: number
failedTasks: number
}
}
class CheckpointManager {
private storage: CheckpointStorage
async saveCheckpoint(orchestrator: TaskOrchestrator): Promise<string> {
const checkpoint: WorkflowCheckpoint = {
id: generateId(),
timestamp: new Date(),
dag: orchestrator.getDag(),
results: orchestrator.getResults(),
metadata: orchestrator.getMetadata()
}
await this.storage.save(checkpoint)
return checkpoint.id
}
async resumeFromCheckpoint(checkpointId: string): Promise<TaskOrchestrator> {
const checkpoint = await this.storage.load(checkpointId)
return TaskOrchestrator.fromCheckpoint(checkpoint)
}
}Context Window Management
For workflows that exceed Claude’s context window:
class ContextManager {
private maxTokens = 100000 // Claude's context limit
async pruneContext(
fullContext: string,
priority: 'recency' | 'relevance'
): Promise<string> {
if (this.estimateTokens(fullContext) <= this.maxTokens) {
return fullContext
}
if (priority === 'recency') {
return this.keepRecentContext(fullContext)
} else {
return this.keepRelevantContext(fullContext)
}
}
private async keepRelevantContext(fullContext: string): Promise<string> {
// Use embeddings to identify most relevant portions
const chunks = this.chunkContext(fullContext)
const embeddings = await this.generateEmbeddings(chunks)
const scores = await this.scoreRelevance(embeddings)
// Keep highest scoring chunks within token limit
return this.selectTopChunks(chunks, scores, this.maxTokens)
}
}Error Handling and Recovery
Robust error handling is crucial for production workflows. Implement multiple layers of resilience:
Retry Strategies
interface RetryStrategy {
shouldRetry(error: Error, attempt: number): boolean
getDelay(attempt: number): number
}
class ExponentialBackoffStrategy implements RetryStrategy {
constructor(
private maxAttempts: number = 3,
private baseDelay: number = 1000
) {}
shouldRetry(error: Error, attempt: number): boolean {
// Retry on rate limits and transient errors
const retryableErrors = ['rate_limit', 'timeout', 'network_error']
return attempt < this.maxAttempts &&
retryableErrors.includes(error.name)
}
getDelay(attempt: number): number {
return this.baseDelay * Math.pow(2, attempt) +
Math.random() * 1000 // Add jitter
}
}Cascading Failure Management
class FailureHandler {
async handleTaskFailure(
task: TaskNode,
error: Error,
dag: WorkflowDAG
): Promise<void> {
// Mark dependent tasks as blocked
const dependents = this.findDependentTasks(task.id, dag)
dependents.forEach(dep => {
dep.status = 'blocked'
dep.blockReason = `Dependency ${task.id} failed: ${error.message}`
})
// Generate recovery plan
const recoveryPlan = await this.generateRecoveryPlan(task, error)
if (recoveryPlan.canRecover) {
// Insert recovery tasks
recoveryPlan.tasks.forEach(recoveryTask => {
dag.nodes.set(recoveryTask.id, recoveryTask)
})
} else {
// Initiate graceful shutdown
await this.gracefulShutdown(dag)
}
}
}Production Patterns and Examples
Multi-Agent Code Review Workflow
A real-world example implementing a comprehensive code review process:
const codeReviewWorkflow: WorkflowDAG = {
nodes: new Map([
['parse_pr', {
id: 'parse_pr',
name: 'Parse Pull Request',
prompt: 'Extract changed files and context from PR: {pr_url}',
dependencies: [],
status: 'pending'
}],
['security_review', {
id: 'security_review',
name: 'Security Analysis',
prompt: 'Analyze security implications of changes: {parse_pr.result}',
dependencies: ['parse_pr'],
status: 'pending'
}],
['performance_review', {
id: 'performance_review',
name: 'Performance Analysis',
prompt: 'Analyze performance impact: {parse_pr.result}',
dependencies: ['parse_pr'],
status: 'pending'
}],
['style_review', {
id: 'style_review',
name: 'Code Style Check',
prompt: 'Check code style and conventions: {parse_pr.result}',
dependencies: ['parse_pr'],
status: 'pending'
}],
['synthesize', {
id: 'synthesize',
name: 'Synthesize Feedback',
prompt: `Combine all review feedback:
Security: {security_review.result}
Performance: {performance_review.result}
Style: {style_review.result}
Generate actionable recommendations`,
dependencies: ['security_review', 'performance_review', 'style_review'],
status: 'pending'
}]
]),
edges: new Map([
['parse_pr', new Set(['security_review', 'performance_review', 'style_review'])],
['security_review', new Set(['synthesize'])],
['performance_review', new Set(['synthesize'])],
['style_review', new Set(['synthesize'])]
])
}Document Processing Pipeline
Complex document analysis with conditional paths:
class DocumentProcessor {
async processDocument(documentPath: string): Promise<any> {
const workflow = new AdaptiveOrchestrator(
this.createInitialDAG(),
this.claudeClient
)
// Dynamic planning based on document type
workflow.setPlanner({
evaluateConditions: async (results) => {
const docType = results.get('classify_document')?.type
switch (docType) {
case 'technical':
return this.generateTechnicalTasks()
case 'legal':
return this.generateLegalTasks()
case 'financial':
return this.generateFinancialTasks()
default:
return this.generateGenericTasks()
}
}
})
return await workflow.executeDynamic()
}
}Integration with Claude Code Features
Leveraging Claude Code Hooks
Integrate task orchestration with Claude Code’s hook system:
// hooks/task-orchestration.ts
export const onBeforeTaskExecute = async (task: TaskNode) => {
console.log(`Starting task: ${task.name}`)
await telemetry.trackTaskStart(task)
}
export const onAfterTaskComplete = async (task: TaskNode, result: any) => {
console.log(`Completed task: ${task.name}`)
await telemetry.trackTaskComplete(task, result)
// Trigger dependent workflows
if (task.id === 'critical_task') {
await triggerDownstreamWorkflows(result)
}
}
export const onTaskError = async (task: TaskNode, error: Error) => {
console.error(`Task failed: ${task.name}`, error)
await alerting.notifyTaskFailure(task, error)
}Memory File Integration
Store workflow state in Claude Code memory files:
// CLAUDE.md additions for workflow context
const workflowMemory = `
## Active Workflows
### Code Review Pipeline
- Status: In Progress
- Current Task: performance_review
- Started: 2025-01-24T10:30:00Z
- Checkpoint: checkpoint_abc123
### Data Processing Pipeline
- Status: Completed
- Total Tasks: 15
- Duration: 2h 34m
- Results: stored in /results/pipeline_xyz789.json
`Subagent Coordination
Use Claude Code’s subagent pattern for specialized tasks:
class SubagentOrchestrator {
async delegateToSpecialist(task: TaskNode): Promise<any> {
const specialist = this.selectSpecialist(task)
return await this.claudeClient.sendMessage({
messages: [{
role: 'system',
content: `You are a ${specialist.role} specialist. ${specialist.expertise}`
}, {
role: 'user',
content: task.prompt
}],
model: specialist.preferredModel
})
}
private selectSpecialist(task: TaskNode): Specialist {
// Route to appropriate specialist based on task type
const specialists = {
'code_generation': {
role: 'Senior Developer',
expertise: 'Expert in TypeScript, React, and system design',
preferredModel: 'claude-3-opus'
},
'security_analysis': {
role: 'Security Analyst',
expertise: 'Expert in OWASP, secure coding, threat modeling',
preferredModel: 'claude-3-opus'
},
'performance_optimization': {
role: 'Performance Engineer',
expertise: 'Expert in profiling, optimization, scalability',
preferredModel: 'claude-3-sonnet'
}
}
return specialists[task.type] || specialists['code_generation']
}
}Best Practices Summary
- Design for Failure: Assume tasks will fail and plan recovery strategies
- Minimize Context: Keep prompts focused to stay within token limits
- Enable Observability: Log extensively for debugging complex workflows
- Test Incrementally: Validate each task in isolation before orchestrating
- Version Control Prompts: Track prompt changes for reproducibility
- Monitor Costs: Track token usage and optimize expensive operations
- Document Dependencies: Make task relationships explicit and visible
Related Resources
- Subagent Orchestration Deep Dive
- Testing Multi-Step Workflows
- Monitoring Task Execution
- Debugging Complex Workflows
- Using Hooks for Workflow Events
Next Steps
- Explore Advanced State Management Techniques
- Learn about Memory and Context Patterns
- Implement Advanced Error Recovery Strategies
This guide represents current best practices as of January 2025. As Claude Code and the broader AI landscape evolve, these patterns will be updated to reflect new capabilities and approaches.