Files
ai/gateway/src/harness/workflows

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:

  1. Code Review (using CodeReviewerSubagent)
  2. If issues → Fix Code → loop back
  3. Backtest (via MCP)
  4. If failed → Fix Code → loop back
  5. Risk Assessment
  6. Human Approval
  7. 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:

  1. Analyze market conditions
  2. Calculate risk and position size
  3. Request human approval (PAUSE)
  4. If approved → Execute trade
  5. 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 | null with default: () => 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');

References