The problem
A contract review has steps that depend on one another, but not every step needs to wait. Running everything in sequence wastes time; restarting the whole review after one temporary failure wastes even more.
Extract contract clauses once, review and summarize them in parallel, then produce a final Markdown notification.
Useful forLegal-operations and workflow teams evaluating repeatable review pipelines
A contract review has steps that depend on one another, but not every step needs to wait. Running everything in sequence wastes time; restarting the whole review after one temporary failure wastes even more.
The bundled example supplies the contract content and a four-step review plan.
Each specialist sees the evidence it needs. Independent work runs together; dependent work waits for the right inputs.
One upstream task prepares the shared material needed by the reviewers.
Identifies and structures the relevant contract clauses.
Both tasks start after extraction and do not block each other.
Checks the extracted clauses and retries this step if it fails.
Creates a concise contract summary.
The last task waits until both review branches are complete.
Combines the completed work into the final Markdown notification.
The final artifact is produced only after the compliance check and summary have both succeeded.
The business view above is editorial. The commands, prerequisites, APIs, and source below stay synchronized with the real repository.
OpenMultiAgent From a clone of the repo — this exact file:
npx tsx packages/core/examples/cookbook/contract-review-dag.tsFORCE_FAIL=task2 npx tsx packages/core/examples/cookbook/contract-review-dag.ts
OMA is provider-agnostic — this example is written for the key above, but you can run it on OpenAI, Gemini, Groq and others. All providers →
The complete example, synchronized from the pinned Framework commit.
/*** Contract Review DAG with Step-Level Retry** Demonstrates DAG task orchestration with step-level retry using runTasks().* Scenario: contract review pipeline with 4 tasks forming a DAG:** [Task 1: extract-clauses]* ├──→ [Task 2: compliance-check] ────┐* │ ├──→ [Task 4: notify] → Markdown* └──→ [Task 3: summary] ──────────────┘** Key features:* - DAG dependencies with parallel execution (Task 2 and 3 run concurrently)* - Step-level retry on Task 2/3/4 with exponential backoff* - FORCE_FAIL=task2 env var triggers Task 2 failure on first attempt** Run:* npx tsx packages/core/examples/cookbook/contract-review-dag.ts* FORCE_FAIL=task2 npx tsx packages/core/examples/cookbook/contract-review-dag.ts** Prerequisites:* ANTHROPIC_API_KEY env var must be set.*/import { OpenMultiAgent } from '../../src/index.js'import type { AgentConfig, OrchestratorEvent } from '../../src/types.js'import { readFileSync } from 'fs'import { fileURLToPath } from 'url'import { dirname, join } from 'path'const __filename = fileURLToPath(import.meta.url)const __dirname = dirname(__filename)// ---------------------------------------------------------------------------// Attempt counter for FORCE_FAIL mechanism (closure-based per agent)// ---------------------------------------------------------------------------const attemptCounter = new Map<string, number>()function getAndIncrementAttempt(agentName: string): number {const count = (attemptCounter.get(agentName) ?? 0) + 1attemptCounter.set(agentName, count)return count}// ---------------------------------------------------------------------------// Step 1: Agent configurations (4 agents)// ---------------------------------------------------------------------------const extractorConfig: AgentConfig = {name: 'extractor',model: 'claude-sonnet-4-6',provider: 'anthropic',systemPrompt: `You are a contract clause extraction specialist. Extract all clauses from the provided contract text and output as a JSON array.Each object must have:- id: clause number (string)- title: clause title- content: full clause text- riskLevel: "low" | "medium" | "high"Output ONLY valid JSON, no other text.`,maxTurns: 1,temperature: 0.1,}const complianceCheckerConfig: AgentConfig = {name: 'compliance-checker',model: 'claude-sonnet-4-6',provider: 'anthropic',systemPrompt: `You are a contract compliance auditor. Review each clause for regulatory and operational compliance.For each clause, output:- clauseId: clause number- isCompliant: true/false- issues: array of issues found (empty array if none)- riskCategory: "none" | "regulatory" | "operational" | "legal"Output ONLY valid JSON array, no other text.`,maxTurns: 1,temperature: 0.1,beforeRun: (context) => {const attempt = getAndIncrementAttempt('compliance-checker')// Only trigger FORCE_FAIL on first attempt (attempt=1)if (attempt === 1 && process.env.FORCE_FAIL === 'task2') {throw new Error('[FORCE_FAIL_TRIGGERED]')}return context},}const summarizerConfig: AgentConfig = {name: 'summarizer',model: 'claude-sonnet-4-6',provider: 'anthropic',systemPrompt: `You are a contract summary specialist. Generate an executive summary from the extracted clause list.Include:- Contract overview (main areas covered)- Key clause summary (3-5 most important points)- Risk callouts (highlight high-risk clauses)- Recommended next stepsOutput in Markdown format.`,maxTurns: 1,temperature: 0.2,}const notifierConfig: AgentConfig = {name: 'notifier',model: 'claude-sonnet-4-6',provider: 'anthropic',systemPrompt: `You are a report writer. Combine the compliance check results and summary into a final contract review report.Include:- Executive Summary- Compliance Results- Risk Details- Recommended ActionsOutput in Markdown format.`,maxTurns: 1,temperature: 0.2,}// ---------------------------------------------------------------------------// Step 2: Task configurations (4 tasks)// ---------------------------------------------------------------------------interface TaskConfig {title: stringdescription: stringassignee?: stringdependsOn?: readonly string[]maxRetries?: numberretryDelayMs?: numberretryBackoff?: number}// Task configs will be created after reading contract text// ---------------------------------------------------------------------------// Step 3: Progress tracking// ---------------------------------------------------------------------------interface TaskTiming {startTime: numberendTime?: number}const taskTimings = new Map<string, TaskTiming>()const taskStartTimes = new Map<string, number>()function handleProgress(event: OrchestratorEvent): void {const ts = new Date().toISOString()switch (event.type) {case 'task_start': {const task = event.data as { id: string; title: string } | undefinedif (task) {const now = Date.now()taskTimings.set(task.id, { startTime: now })taskStartTimes.set(task.title, now) // Use title as key for easier verificationconsole.log(`[${ts}] task_start: ${task.title}`)}break}case 'task_complete': {const task = event.data as { id: string; title: string } | undefinedif (task) {const timing = taskTimings.get(task.id)if (timing) {timing.endTime = Date.now()const duration = timing.endTime - timing.startTimeconsole.log(`[${ts}] task_complete: ${task.title} (${duration}ms)`)}}break}case 'task_retry':console.log(`[${ts}] task_retry:`, JSON.stringify(event.data))breakcase 'task_skipped':console.log(`[${ts}] task_skipped: ${event.task}`)breakcase 'agent_start':if (event.agent) {console.log(`[${ts}] agent_start: ${event.agent}`)}breakcase 'agent_complete':if (event.agent) {console.log(`[${ts}] agent_complete: ${event.agent}`)}breakcase 'message':if (typeof event.data === 'string') {console.log(`[${ts}] message: ${event.data}`)}breakcase 'error':console.log(`[${ts}] error:`, JSON.stringify(event.data))breakdefault:break}}// ---------------------------------------------------------------------------// Step 4: Parallelism verification// ---------------------------------------------------------------------------function verifyParallelism(): void {const t2Start = taskStartTimes.get('compliance-check')const t3Start = taskStartTimes.get('summary')if (t2Start !== undefined && t3Start !== undefined) {const diff = Math.abs(t2Start - t3Start)console.log('\n=== Parallelism Check ===')console.log(`Task 2 (compliance-check) start: ${t2Start}`)console.log(`Task 3 (summary) start: ${t3Start}`)console.log(`Time difference: ${diff}ms`)if (diff >= 500) {console.error(`ASSERTION FAILED: Task 2 and Task 3 start times differ by ${diff}ms (>= 500ms). ` +`Expected parallel execution after Task 1 completes.`,)process.exit(1)}console.log(`Parallel execution (< 500ms): YES`)console.log('========================\n')} else {console.log('\n=== Parallelism Check: Unable to verify (missing timing data) ===\n')}}// ---------------------------------------------------------------------------// Step 5: Orchestrator and Team configuration// ---------------------------------------------------------------------------const orchestrator = new OpenMultiAgent({defaultModel: 'claude-sonnet-4-6',defaultProvider: 'anthropic',onProgress: handleProgress,})const team = orchestrator.createTeam('contract-review-team', {name: 'contract-review-team',agents: [extractorConfig, complianceCheckerConfig, summarizerConfig, notifierConfig],sharedMemory: true,})// ---------------------------------------------------------------------------// Step 6: Read contract text// ---------------------------------------------------------------------------const contractText = readFileSync(join(__dirname, '..', 'fixtures', 'sample-contract.txt'),'utf-8',)// ---------------------------------------------------------------------------// Step 6b: Create task configs with contract text// ---------------------------------------------------------------------------const taskConfigs: TaskConfig[] = [{title: 'extract-clauses',description: `Extract all clauses from the following contract text into structured JSON.\n\n=== CONTRACT TEXT ===\n${contractText}\n=== END CONTRACT ===\n\nOutput only valid JSON array.`,assignee: 'extractor',},{title: 'compliance-check',description: 'Check each clause for regulatory and operational compliance. Using the clause list from Task 1 above.',assignee: 'compliance-checker',dependsOn: ['extract-clauses'],maxRetries: 2,retryDelayMs: 500,retryBackoff: 2,},{title: 'summary',description: 'Generate executive summary of the contract. Using the clause list from Task 1 above.',assignee: 'summarizer',dependsOn: ['extract-clauses'],maxRetries: 2,retryDelayMs: 500,retryBackoff: 2,},{title: 'notify',description: 'Generate final markdown report with all analysis results',assignee: 'notifier',dependsOn: ['compliance-check', 'summary'],maxRetries: 2,retryDelayMs: 500,retryBackoff: 2,},]console.log('Contract Review DAG with Step-Level Retry')console.log('='.repeat(60))console.log(`\nContract: ${contractText.split('\n')[0]}`)console.log(`Length: ${contractText.split(/\s+/).length} words\n`)console.log(`FORCE_FAIL mode: ${process.env.FORCE_FAIL === 'task2' ? 'ENABLED (task2)' : 'disabled'}\n`)// ---------------------------------------------------------------------------// Step 7: Run the DAG// ---------------------------------------------------------------------------async function main(): Promise<void> {try {console.log('[Orchestration] Starting DAG execution...\n')const result = await orchestrator.runTasks(team, taskConfigs)// Verify parallelismverifyParallelism()console.log('\n' + '='.repeat(60))console.log('FINAL RESULT')console.log('='.repeat(60))console.log(`Success: ${result.success}`)console.log(`Total tasks: ${result.tasks?.length ?? taskConfigs.length}`)console.log(`Total token usage: ${result.totalTokenUsage.input_tokens} input, ${result.totalTokenUsage.output_tokens} output`)// Output each agent's resultconsole.log('\n' + '='.repeat(60))console.log('AGENT RESULTS')console.log('='.repeat(60))for (const [agentName, agentResult] of result.agentResults) {console.log(`\n--- ${agentName} ---`)console.log(agentResult.success ? agentResult.output : `[FAILED] ${agentResult.output}`)}if (result.success) {console.log('\n--- Final Report ---')const notifierResult = result.agentResults.get('notifier')if (notifierResult?.output) {console.log(notifierResult.output)}} else {console.log('\nWorkflow failed.')for (const task of result.tasks ?? []) {if (task.status === 'failed') {console.log(` - ${task.title}: failed`)}}}console.log('\n' + '='.repeat(60))console.log('Done.')} catch (error) {console.error('Fatal error:', error)process.exit(1)}}main()
open-multi-agent is MIT-licensed and free to run yourself. When you need it delivered, integrated, or supported on a deadline, 元定义科技 (YuanASI) offers commercial delivery and support.