← All examples
// Use-case Recipe

Contract Review DAG with Step-Level Retry

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

Enterprise support
01 The scenario

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.

A contract review scenario

The bundled example supplies the contract content and a four-step review plan.

02 How it works

A team, with a clear handoff.

Each specialist sees the evidence it needs. Independent work runs together; dependent work waits for the right inputs.

  1. Extract the clauses

    One upstream task prepares the shared material needed by the reviewers.

    Clause extractor

    Identifies and structures the relevant contract clauses.

  2. Review from two angles

    Both tasks start after extraction and do not block each other.

    runs in parallel
    Compliance checker

    Checks the extracted clauses and retries this step if it fails.

    Summarizer

    Creates a concise contract summary.

  3. Finish the handoff

    The last task waits until both review branches are complete.

    Notifier

    Combines the completed work into the final Markdown notification.

03 The result

A completed review notification

The final artifact is produced only after the compliance check and summary have both succeeded.

  1. 01Extracted clause context
  2. 02Compliance-check result
  3. 03Contract summary and final notification
  • Parallel review shortens the middle of the workflow.
  • Dependencies make the handoffs explicit.
  • Step-level retry repeats only the failed work, with exponential backoff.
Scope and limits
This is an orchestration and retry example, not legal advice or a production compliance policy. A forced-failure command is included so the retry path can be observed safely.
04 For builders

Run it, then inspect the implementation.

The business view above is editorial. The commands, prerequisites, APIs, and source below stay synchronized with the real repository.

Implementation facts OpenMultiAgent
Use-case Recipe 368 lines

Run it

From a clone of the repo — this exact file:

terminal
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.

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 →

Open the complete, synchronized source · 368 lines

The complete example, synchronized from the pinned Framework commit.

cookbook/contract-review-dag.ts
/**
* 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) + 1
attemptCounter.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 steps
Output 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 Actions
Output in Markdown format.`,
maxTurns: 1,
temperature: 0.2,
}
 
// ---------------------------------------------------------------------------
// Step 2: Task configurations (4 tasks)
// ---------------------------------------------------------------------------
 
interface TaskConfig {
title: string
description: string
assignee?: string
dependsOn?: readonly string[]
maxRetries?: number
retryDelayMs?: number
retryBackoff?: number
}
 
// Task configs will be created after reading contract text
 
// ---------------------------------------------------------------------------
// Step 3: Progress tracking
// ---------------------------------------------------------------------------
 
interface TaskTiming {
startTime: number
endTime?: 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 } | undefined
if (task) {
const now = Date.now()
taskTimings.set(task.id, { startTime: now })
taskStartTimes.set(task.title, now) // Use title as key for easier verification
console.log(`[${ts}] task_start: ${task.title}`)
}
break
}
 
case 'task_complete': {
const task = event.data as { id: string; title: string } | undefined
if (task) {
const timing = taskTimings.get(task.id)
if (timing) {
timing.endTime = Date.now()
const duration = timing.endTime - timing.startTime
console.log(`[${ts}] task_complete: ${task.title} (${duration}ms)`)
}
}
break
}
 
case 'task_retry':
console.log(`[${ts}] task_retry:`, JSON.stringify(event.data))
break
 
case 'task_skipped':
console.log(`[${ts}] task_skipped: ${event.task}`)
break
 
case 'agent_start':
if (event.agent) {
console.log(`[${ts}] agent_start: ${event.agent}`)
}
break
 
case 'agent_complete':
if (event.agent) {
console.log(`[${ts}] agent_complete: ${event.agent}`)
}
break
 
case 'message':
if (typeof event.data === 'string') {
console.log(`[${ts}] message: ${event.data}`)
}
break
 
case 'error':
console.log(`[${ts}] error:`, JSON.stringify(event.data))
break
 
default:
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 parallelism
verifyParallelism()
 
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 result
console.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()
View & edit on GitHub
// Enterprise

Taking this to production?

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.

Enterprise support