Workflows
LangGraph-based workflows for multi-step agent orchestration.
What are Workflows?
Workflows are state machines that orchestrate complex multi-step tasks with:
- State Management: Typed state with annotations
- Conditional Routing: Different paths based on state
- Validation Loops: Retry with fixes
- Human-in-the-Loop: Approval gates and interrupts
- Error Recovery: Graceful handling of failures
Built on LangGraph.js.
Directory Structure
workflows/
├── base-workflow.ts # Base class and utilities
├── {workflow-name}/
│ ├── config.yaml # Workflow configuration
│ ├── state.ts # State schema (Annotations)
│ ├── nodes.ts # Node implementations
│ └── graph.ts # StateGraph definition
└── README.md # This file
Workflow Components
State (state.ts)
Defines what data flows through the workflow:
import { Annotation } from '@langchain/langgraph';
import { BaseWorkflowState } from '../base-workflow.js';
export const MyWorkflowState = Annotation.Root({
...BaseWorkflowState.spec, // Inherit base fields
// Your custom fields
input: Annotation<string>(),
result: Annotation<string | null>({ default: () => null }),
errorCount: Annotation<number>({ default: () => 0 }),
});
export type MyWorkflowStateType = typeof MyWorkflowState.State;
Nodes (nodes.ts)
Functions that transform state:
export function createMyNode(deps: Dependencies) {
return async (state: MyWorkflowStateType): Promise<Partial<MyWorkflowStateType>> => {
// Do work
const result = await doSomething(state.input);
// Return partial state update
return { result };
};
}
Graph (graph.ts)
Connects nodes with edges:
import { StateGraph } from '@langchain/langgraph';
import { BaseWorkflow } from '../base-workflow.js';
export class MyWorkflow extends BaseWorkflow<MyWorkflowStateType> {
buildGraph(): StateGraph<MyWorkflowStateType> {
const graph = new StateGraph(MyWorkflowState);
// Add nodes
graph
.addNode('step1', createStep1Node())
.addNode('step2', createStep2Node());
// Add edges
graph
.addEdge('__start__', 'step1')
.addEdge('step1', 'step2')
.addEdge('step2', '__end__');
return graph;
}
}
Config (config.yaml)
Workflow settings:
name: my-workflow
description: What it does
timeout: 300000 # 5 minutes
maxRetries: 3
requiresApproval: true
approvalNodes:
- human_approval
# Custom settings
myCustomSetting: value
Common Patterns
1. Validation Loop (Retry with Fixes)
graph
.addNode('validate', validateNode)
.addNode('fix', fixNode)
.addConditionalEdges('validate', (state) => {
if (state.isValid) return 'next_step';
if (state.retryCount >= 3) return '__end__'; // Give up
return 'fix'; // Try to fix
})
.addEdge('fix', 'validate'); // Loop back
2. Human-in-the-Loop (Approval)
const approvalNode = async (state) => {
// Send approval request to user's channel
await sendToChannel(state.userContext.activeChannel, {
type: 'approval_request',
data: {
action: 'execute_trade',
details: state.tradeDetails,
}
});
// Mark as waiting for approval
return { approvalRequested: true, userApproved: false };
};
graph.addConditionalEdges('approval', (state) => {
return state.userApproved ? 'execute' : '__end__';
});
// To resume after user input:
// const updated = await workflow.execute({ ...state, userApproved: true });
3. Parallel Execution
import { Branch } from '@langchain/langgraph';
graph
.addNode('parallel_start', startNode)
.addNode('task_a', taskANode)
.addNode('task_b', taskBNode)
.addNode('merge', mergeNode);
// Branch to parallel tasks
graph.addEdge('parallel_start', Branch.parallel(['task_a', 'task_b']));
// Merge results
graph
.addEdge('task_a', 'merge')
.addEdge('task_b', 'merge');
4. Error Recovery
const resilientNode = async (state) => {
try {
const result = await riskyOperation();
return { result, error: null };
} catch (error) {
logger.error({ error }, 'Operation failed');
return {
error: error.message,
fallbackUsed: true,
result: await fallbackOperation()
};
}
};
5. Conditional Routing
graph.addConditionalEdges('decision', (state) => {
if (state.score > 0.8) return 'high_confidence';
if (state.score > 0.5) return 'medium_confidence';
return 'low_confidence';
});
graph
.addNode('high_confidence', autoApproveNode)
.addNode('medium_confidence', humanReviewNode)
.addNode('low_confidence', rejectNode);
Available Workflows
strategy-validation
Validates trading strategies with multiple steps and a validation loop.
Flow:
- Code Review (using CodeReviewerSubagent)
- If issues → Fix Code → loop back
- Backtest (via MCP)
- If failed → Fix Code → loop back
- Risk Assessment
- Human Approval
- Final Recommendation
Features:
- Max 3 retry attempts
- Multi-file memory from subagent
- Risk-based auto-approval
- Comprehensive state tracking
trading-request
Human-in-the-loop workflow for trade execution.
Flow:
- Analyze market conditions
- Calculate risk and position size
- Request human approval (PAUSE)
- If approved → Execute trade
- Generate summary
Features:
- Interrupt at approval node
- Channel-aware approval UI
- Risk validation
- Execution confirmation
Creating a New Workflow
1. Create Directory
mkdir -p workflows/my-workflow
2. Define State
// state.ts
import { Annotation } from '@langchain/langgraph';
import { BaseWorkflowState } from '../base-workflow.js';
export const MyWorkflowState = Annotation.Root({
...BaseWorkflowState.spec,
// Add your fields
input: Annotation<string>(),
step1Result: Annotation<string | null>({ default: () => null }),
step2Result: Annotation<string | null>({ default: () => null }),
});
export type MyWorkflowStateType = typeof MyWorkflowState.State;
3. Create Nodes
// nodes.ts
import { MyWorkflowStateType } from './state.js';
export function createStep1Node(deps: any) {
return async (state: MyWorkflowStateType) => {
const result = await doStep1(state.input);
return { step1Result: result };
};
}
export function createStep2Node(deps: any) {
return async (state: MyWorkflowStateType) => {
const result = await doStep2(state.step1Result);
return { step2Result: result, output: result };
};
}
4. Build Graph
// graph.ts
import { StateGraph } from '@langchain/langgraph';
import { BaseWorkflow, WorkflowConfig } from '../base-workflow.js';
import { MyWorkflowState, MyWorkflowStateType } from './state.js';
import { createStep1Node, createStep2Node } from './nodes.js';
export class MyWorkflow extends BaseWorkflow<MyWorkflowStateType> {
constructor(config: WorkflowConfig, private deps: any, logger: Logger) {
super(config, logger);
}
buildGraph(): StateGraph<MyWorkflowStateType> {
const graph = new StateGraph(MyWorkflowState);
const step1 = createStep1Node(this.deps);
const step2 = createStep2Node(this.deps);
graph
.addNode('step1', step1)
.addNode('step2', step2)
.addEdge('__start__', 'step1')
.addEdge('step1', 'step2')
.addEdge('step2', '__end__');
return graph;
}
}
5. Create Config
# config.yaml
name: my-workflow
description: My workflow description
timeout: 60000
maxRetries: 3
requiresApproval: false
model: claude-3-5-sonnet-20241022
6. Add Factory Function
// graph.ts (continued)
export async function createMyWorkflow(
deps: any,
logger: Logger,
configPath: string
): Promise<MyWorkflow> {
const config = await loadYAML(configPath);
const workflow = new MyWorkflow(config, deps, logger);
workflow.compile();
return workflow;
}
Usage
Execute Workflow
import { createMyWorkflow } from './harness/workflows';
const workflow = await createMyWorkflow(deps, logger, configPath);
const result = await workflow.execute({
userContext,
input: 'my input'
});
console.log(result.output);
Stream Workflow
for await (const state of workflow.stream({ userContext, input })) {
console.log('Current state:', state);
}
With Interrupts (Human-in-the-Loop)
// Initial execution (pauses at interrupt)
const pausedState = await workflow.execute(initialState);
// User provides input
const userInput = await getUserApproval();
// Resume from paused state
const finalState = await workflow.execute({
...pausedState,
userApproved: userInput.approved
});
Best Practices
State Design
- Immutable Updates: Return partial state, don't mutate
- Type Safety: Use TypeScript annotations
- Defaults: Provide sensible defaults
- Nullable Fields: Use
| nullwithdefault: () => null
Node Implementation
- Pure Functions: Avoid side effects in state logic
- Error Handling: Catch errors, return error state
- Logging: Log entry/exit of nodes
- Partial Updates: Only return fields that changed
Graph Design
- Single Responsibility: Each node does one thing
- Clear Flow: Easy to visualize the graph
- Error Paths: Handle failures gracefully
- Idempotency: Safe to retry nodes
Configuration
- Timeouts: Set reasonable limits
- Retries: Don't retry forever
- Approvals: Mark approval nodes explicitly
- Documentation: Explain complex config values
Debugging
View Graph
// Get graph structure
const compiled = workflow.compile();
console.log(compiled.getGraph());
Log State
const debugNode = async (state) => {
logger.debug({ state }, 'Current state');
return {}; // No changes
};
graph.addNode('debug', debugNode);
Test Nodes in Isolation
const step1 = createStep1Node(deps);
const result = await step1({ input: 'test', /* ... */ });
expect(result.step1Result).toBe('expected');