Static Analysis
Analyze workflow source code to extract structure, dependencies, and generate visualization data without executing the workflow.
Documentation fields: description and markdown on workflows and steps are the canonical way to feed static analysis and doc generation. See Documenting workflows for how to set them. JSDoc above workflow and step declarations is also extracted and available as jsdocDescription on the root and step nodes.
Step names: step() requires a string literal as the first argument. The analyzer uses that string as the step display name (for docs/diagrams). There is no inference; a missing or non-string first argument is reported as an error. See Steps for usage.
Step metadata: The analyzer also reads optional step metadata when present (intent, domain, owner, tags, stateChanges, emits, calls, errorMeta) and includes it in the IR and generated diagrams/tooling — e.g. step('charge', () => charge(), { domain: 'payments', intent: 'charge customer' }).
CLI Tool
Section titled “CLI Tool”The awaitly-analyze CLI analyzes TypeScript workflow files and outputs Mermaid diagrams or JSON:
# Output Mermaid diagram (default)npx awaitly-analyze ./src/workflows/checkout.ts
# Output JSONnpx awaitly-analyze ./src/workflows/checkout.ts --format=json
# Pipe to filenpx awaitly-analyze ./src/workflows/checkout.ts > workflow.md
# Show step cache keysnpx awaitly-analyze ./src/workflows/checkout.ts --keys
# Change diagram direction (TB, LR, BT, RL)npx awaitly-analyze ./src/workflows/checkout.ts --direction=LR
# Compare two workflow filesnpx awaitly-analyze --diff v1.ts v2.ts
# Compare HEAD vs working copy (single file shorthand)npx awaitly-analyze --diff src/workflows/checkout.ts
# Compare a git ref against a local filenpx awaitly-analyze --diff main:src/workflows/checkout.ts src/workflows/checkout.ts
# Diff all workflow files in a GitHub PRnpx awaitly-analyze --diff gh:#123
# Diff as JSON or Mermaid diagramnpx awaitly-analyze --diff v1.ts v2.ts --format=jsonnpx awaitly-analyze --diff v1.ts v2.ts --format=mermaid
# Flag removed steps as regressionsnpx awaitly-analyze --diff v1.ts v2.ts --regressionInteractive HTML
Section titled “Interactive HTML”Generate a self-contained HTML file with an interactive Mermaid diagram, click-to-inspect node details, and a built-in theme picker:
# Generate interactive HTML (writes <basename>.html next to source)npx awaitly-analyze ./src/workflows/checkout.ts --html
# Custom output pathnpx awaitly-analyze ./src/workflows/checkout.ts --html --html-output=./docs/checkout-diagram.htmlThe generated HTML file is fully self-contained — it loads Mermaid from a CDN and embeds all workflow metadata inline. Open it in any browser; no dev server needed.
What’s included:
- Mermaid diagram rendered client-side via CDN
- Click-to-inspect — click any node to see its step ID, callee, retry/timeout config, types, data flow, and source location in a side panel
- 6 color themes — 4 dark (Midnight, Ocean, Ember, Forest) and 2 light (Daylight, Paper)
- System preference detection — auto-selects a dark or light theme based on
prefers-color-scheme - localStorage persistence — your theme choice is remembered across sessions
Programmatic usage:
import { analyze, renderStaticMermaid, extractNodeMetadata, generateInteractiveHTML,} from 'awaitly-analyze';
const ir = analyze('./checkout.ts').single();const mermaid = renderStaticMermaid(ir);const metadata = extractNodeMetadata(ir);const html = generateInteractiveHTML(mermaid, metadata, { theme: 'midnight', // optional: force a specific theme title: 'Checkout', // optional: override page title direction: 'TB', // optional: diagram direction});
// Write to diskimport { writeFileSync } from 'node:fs';writeFileSync('checkout.html', html);See Renderers → Interactive HTML renderer for the full API.
Multi-workflow output
Section titled “Multi-workflow output”When a file contains multiple workflows, each gets its own section with a markdown header and mermaid diagram:
## Workflow: checkoutWorkflow
(mermaid diagram for checkout workflow)
## Workflow: paymentWorkflow
(mermaid diagram for payment workflow)Basic usage
Section titled “Basic usage”import { analyze } from 'awaitly-analyze';
// Analyze a single-workflow fileconst ir = analyze('./src/workflows/checkout.ts').single();console.log(`Steps: ${ir.metadata.stats.totalSteps}`);
// Analyze a multi-workflow fileconst workflows = analyze('./src/workflows/all.ts').all();for (const ir of workflows) { console.log(`${ir.root.workflowName}: ${ir.metadata.stats.totalSteps} steps`);}
// Get a specific workflow by nameconst checkout = analyze('./src/workflows/all.ts').named('checkoutWorkflow');Fluent API methods
Section titled “Fluent API methods”| Method | Returns | Throws | Use when |
|---|---|---|---|
.single() | Single IR | If 0 or >1 workflows | File has exactly one workflow |
.singleOrNull() | IR or null | Never | Checking if file has one workflow |
.all() | IR array | Never | Processing all workflows in file |
.named(name) | Single IR | If not found | Targeting specific workflow by name |
.first() | Single IR | If empty | Getting first workflow, don’t care about count |
.firstOrNull() | IR or null | Never | Safely getting first workflow |
Choosing the right method:
// Most common: single workflow per fileconst ir = analyze('./checkout.ts').single();
// Multi-workflow files: iterate allfor (const ir of analyze('./workflows.ts').all()) { console.log(ir.root.workflowName);}
// Target by name when you know what you wantconst payment = analyze('./workflows.ts').named('paymentWorkflow');
// Safe access when file might be emptyconst ir = analyze(filePath).firstOrNull();if (ir) { // process workflow}Analyzing source strings
Section titled “Analyzing source strings”Analyze workflow code directly without reading from a file:
import { analyze } from 'awaitly-analyze';
const source = ` const checkout = createWorkflow('workflow', deps);
async function run() { return await checkout.run(async ({ step, deps }) => { const cart = await step('fetchCart', () => deps.fetchCart(), { key: 'cart' }); const total = await step('calculateTotal', () => deps.calculateTotal(cart), { key: 'total' }); return { cart, total }; }); }`;
const ir = analyze.source(source).single();console.log(ir.metadata.stats);// { totalSteps: 2, conditionalCount: 0, parallelCount: 0, ... }What gets detected
Section titled “What gets detected”The analyzer extracts:
| Feature | Detection |
|---|---|
| Steps | step(), step.retry(), step.withTimeout() |
| Conditionals | if/else, when(), unless(), whenOr(), unlessOr() |
| Loops | for, while, for-of, for-in |
| Parallel | step.parallel(), allAsync(), allSettledAsync() |
| Race | step.race(), anyAsync() |
| Retry/Timeout | Options in step config, step.retry(), step.withTimeout() |
| Workflow refs | Calls to other workflows |
Supported callback patterns
Section titled “Supported callback patterns”The analyzer handles various callback patterns:
// Simple identifierworkflow.run(async ({ step, deps }) => { ... });
// Destructuringworkflow.run(async ({ step }) => { ... });
// Destructuring with aliasworkflow.run(async ({ step: runStep }) => { ... });
// Destructuring with defaultworkflow.run(async ({ step = defaultStep }) => { ... });
// Destructuring with alias and defaultworkflow.run(async ({ step: runStep = fallback }) => { ... });IR structure
Section titled “IR structure”The analyzer returns a StaticWorkflowIR with:
interface StaticWorkflowIR { root: StaticWorkflowNode; // Workflow tree metadata: StaticAnalysisMetadata; references: Map<string, StaticWorkflowIR>;}
interface StaticAnalysisMetadata { analyzedAt: number; filePath: string; warnings: AnalysisWarning[]; stats: AnalysisStats;}
interface AnalysisStats { totalSteps: number; conditionalCount: number; parallelCount: number; raceCount: number; loopCount: number; workflowRefCount: number; unknownCount: number;}Node types
Section titled “Node types”The workflow tree contains these node types:
type StaticFlowNode = | StaticStepNode // step() call | StaticSequenceNode // Sequential steps | StaticParallelNode // Parallel execution | StaticRaceNode // Race execution | StaticConditionalNode // if/else or conditional helpers | StaticLoopNode // for/while loops | StaticWorkflowRefNode // Call to another workflow | StaticUnknownNode; // Unanalyzable codeAPI types reference
Section titled “API types reference”Main static-analysis node types and when fields are populated:
-
StaticWorkflowNode (root):
workflowName,source,dependencies,children,description,markdown,jsdocDescription?,errorTypes.descriptionandmarkdownare set only forcreateWorkflow/createSagaWorkflow(from options or deps). They are undefined forrun()/runSaga().jsdocDescriptionis extracted from JSDoc above the workflow variable when present. -
StaticStepNode:
stepId,callee,name,key,description,markdown,jsdocDescription?,retry,timeout,errors?,out?,reads?.stepIdis required and comes from the first argument tostep('id', fn, opts).descriptionandmarkdowncome from step options.jsdocDescriptionis extracted from JSDoc above the step statement. -
StaticSagaStepNode:
callee,name,description,markdown,jsdocDescription?,hasCompensation,compensationCallee,isTryStep.descriptionandmarkdowncome from saga step options. -
DependencyInfo:
name,typeSignature?,errorTypes.typeSignatureis the TypeScript type when the type checker is available.errorTypesis not yet inferred from types.
Walking the tree
Section titled “Walking the tree”Use the built-in helpers to traverse nodes:
import { analyze, isStaticStepNode, isStaticConditionalNode, getStaticChildren} from 'awaitly-analyze';
function walkWorkflow(node: StaticFlowNode) { if (isStaticStepNode(node)) { console.log(`Step: ${node.key || node.name}`); }
if (isStaticConditionalNode(node)) { console.log(`Condition: ${node.condition}`); }
// Recursively walk children for (const child of getStaticChildren(node)) { walkWorkflow(child); }}
const ir = analyze('./checkout.ts').single();for (const child of ir.root.children) { walkWorkflow(child);}Multiple workflows
Section titled “Multiple workflows”A single file can contain multiple workflows:
import { analyze } from 'awaitly-analyze';
const source = ` const workflowA = createWorkflow('workflowA', depsA); const workflowB = createWorkflow('workflowB', depsB);
async function runA() { return await workflowA.run(async ({ step }) => { ... }); }
async function runB() { return await workflowB.run(async ({ step }) => { ... }); }`;
const result = analyze.source(source);
// Get all workflowsconst workflows = result.all();console.log(workflows.length); // 2
// Get by nameconst wfA = analyze.source(source).named('workflowA');const wfB = analyze.source(source).named('workflowB');Use cases
Section titled “Use cases”Generating documentation
Section titled “Generating documentation”Use description and markdown on workflows and steps (see Documenting workflows) so the analyzer can extract them. Then:
- Call
analyze(filePath).all()to get all workflows in a file. - For each IR: use
root.workflowName,root.description,root.markdownfor the workflow title and body. - Walk
root.children(and nested sequences) to list steps; use each step’sname,key,description,markdownfor step tables or lists. - Optionally use
generatePaths(ir)andrenderStaticMermaid(ir)for path coverage and diagrams.
Minimal recipe:
import { analyze, getStaticChildren, isStaticStepNode } from 'awaitly-analyze';
const workflows = analyze('./src/workflows/checkout.ts').all();
for (const ir of workflows) { const { root } = ir; console.log(`# ${root.workflowName}\n`); if (root.description) console.log(root.description + '\n'); if (root.markdown) console.log(root.markdown + '\n'); console.log(`- ${ir.metadata.stats.totalSteps} steps\n`);
function listSteps(node) { if (isStaticStepNode(node)) { const name = node.stepId; console.log(`- **${name}**${node.description ? ` — ${node.description}` : ''}`); } for (const child of getStaticChildren(node)) listSteps(child); } for (const child of root.children) listSteps(child);}An optional script that does this is provided in the repo: run node packages/awaitly-analyze/scripts/generate-workflow-docs.mjs <path-to-workflow.ts> from the repo root (after building awaitly-analyze) to output one markdown section per workflow.
Build test coverage matrix
Section titled “Build test coverage matrix”import { analyze, isStaticConditionalNode } from 'awaitly-analyze';
function countPaths(node: StaticFlowNode): number { if (isStaticConditionalNode(node)) { const thenPaths = node.consequent.reduce((n, c) => n * countPaths(c), 1); const elsePaths = (node.alternate || []).reduce((n, c) => n * countPaths(c), 1); return thenPaths + elsePaths; } // ... handle other node types return 1;}
const ir = analyze('./checkout.ts').single();console.log(`Execution paths: ${countPaths(ir.root)}`);Validate workflow complexity
Section titled “Validate workflow complexity”import { analyze } from 'awaitly-analyze';
const ir = analyze('./checkout.ts').single();const stats = ir.metadata.stats;
if (stats.totalSteps > 20) { console.warn('Consider breaking this workflow into smaller pieces');}
if (stats.conditionalCount > 5) { console.warn('High cyclomatic complexity');}Renderers
Section titled “Renderers”Generate output from the IR using built-in renderers:
Mermaid renderer
Section titled “Mermaid renderer”import { analyze, renderStaticMermaid } from 'awaitly-analyze';
const ir = analyze('./checkout.ts').single();const mermaid = renderStaticMermaid(ir, { direction: 'TB', // TB, LR, BT, RL showKeys: false, // Show step cache keys showConditions: true, // Show condition text});
console.log(mermaid);// flowchart TB// start(("▶"))// step_1["Fetch cart"]// ...JSON renderer
Section titled “JSON renderer”import { analyze, renderStaticJSON, renderMultipleStaticJSON } from 'awaitly-analyze';
const ir = analyze('./checkout.ts').single();const json = renderStaticJSON(ir, { pretty: true });
// For multiple workflowsconst workflows = analyze('./checkout.ts').all();const multiJson = renderMultipleStaticJSON(workflows, './checkout.ts', { pretty: true,});JSON output shape
Section titled “JSON output shape”The output of renderStaticJSON(ir) has this structure:
- Top level:
{ root, metadata?, references? } - StaticWorkflowNode:
type: "workflow",id,workflowName,source?,dependencies[],errorTypes[],children[],description?,markdown?,jsdocDescription? - Flow nodes (discriminated by
type):"step"(includes requiredstepIdfrom the first argument),"saga-step","sequence","parallel","race","conditional","switch","loop","stream","workflow-ref","unknown"
A JSON Schema for validation is available at schema/static-workflow-ir.schema.json in the awaitly-analyze package.
Interactive HTML renderer
Section titled “Interactive HTML renderer”Generate a self-contained HTML file with an interactive Mermaid diagram and click-to-inspect panel. This is the same output produced by the --html CLI flag.
The pipeline has two steps: extract metadata from the IR, then combine it with the Mermaid text into HTML:
import { analyze, renderStaticMermaid, extractNodeMetadata, generateInteractiveHTML,} from 'awaitly-analyze';
const ir = analyze('./checkout.ts').single();const mermaid = renderStaticMermaid(ir);const metadata = extractNodeMetadata(ir);const html = generateInteractiveHTML(mermaid, metadata, { theme: 'midnight' });extractNodeMetadata(ir) walks the IR tree and returns a WorkflowMetadata object containing per-node metadata (step IDs, callees, retry/timeout config, types, source locations) keyed by the Mermaid node ID so the HTML click handler can look up details.
generateInteractiveHTML(mermaidText, metadata, options?) returns a complete HTML string. Options:
| Option | Type | Default | Description |
|---|---|---|---|
title | string | workflow name | Page title |
theme | string | auto-detect | Initial theme (midnight, ocean, ember, forest, daylight, paper) |
mermaidCdnUrl | string | latest v11 | Mermaid CDN URL |
direction | string | TB | Diagram direction |
Custom names
Section titled “Custom names”Parallel and race nodes support custom names that appear in diagrams:
// Object form with name optionawait step.parallel('Fetch user data', { user: () => deps.fetchUser(), posts: () => deps.fetchPosts(),});
// Array form with nameawait step.parallel('Fetch all', () => allAsync([ deps.fetchUser(), deps.fetchPosts(),]));Both forms render with the custom name in Mermaid diagrams instead of generic “Parallel (all)”.
Workflow Diff
Section titled “Workflow Diff”Compare two versions of a workflow to detect added, removed, renamed, and moved steps, plus structural changes like new parallel blocks.
CLI diff mode
Section titled “CLI diff mode”Pass --diff with one or two sources. Sources can be local files, git refs (ref:path), or a GitHub PR (gh:#N). The default output format is markdown; use --format to switch to JSON or Mermaid.
# Two local filesnpx awaitly-analyze --diff before.ts after.ts
# Single file — compares HEAD against working copynpx awaitly-analyze --diff src/workflows/checkout.ts
# Git ref vs local filenpx awaitly-analyze --diff main:src/workflows/checkout.ts src/workflows/checkout.ts
# GitHub PR — auto-discovers changed workflow filesnpx awaitly-analyze --diff gh:#123
# GitHub PR — specific file onlynpx awaitly-analyze --diff gh:#123 src/workflows/checkout.ts
# Output formatsnpx awaitly-analyze --diff before.ts after.ts --format=jsonnpx awaitly-analyze --diff before.ts after.ts --format=mermaid
# Flag removed steps as regressionsnpx awaitly-analyze --diff before.ts after.ts --regressionThe git ref syntax uses ref:path where ref is any valid git ref (branch, tag, or SHA). GitHub PR mode requires the GitHub CLI (gh) to be installed and authenticated.
Programmatic API
Section titled “Programmatic API”import { analyze, diffWorkflows, renderDiffMarkdown, renderDiffJSON, renderDiffMermaid,} from 'awaitly-analyze';
const before = analyze('./v1.ts').single();const after = analyze('./v2.ts').single();
const diff = diffWorkflows(before, after, { detectRenames: true, // match renamed steps by callee + position (default: true) regressionMode: false, // flag removals as regressions (default: false)});
// Render as markdownconst md = renderDiffMarkdown(diff, { showUnchanged: true });
// Render as JSONconst json = renderDiffJSON(diff);
// Render as Mermaid (needs the "after" IR for the base diagram)const mermaid = renderDiffMermaid(after, diff, { showRemovedSteps: true, // add ghost nodes for removed steps (default: true) direction: 'TB', // diagram direction (default: TB)});What gets detected
Section titled “What gets detected”| Change | Description |
|---|---|
| Added | Step exists in the after workflow but not in the before |
| Removed | Step exists in the before workflow but not in the after |
| Renamed | Same callee and position, different step ID |
| Moved | Same step ID, different container (e.g. sequential to parallel) |
| Structural | Container-level changes — added/removed parallel, race, conditional, or loop blocks |
Output formats
Section titled “Output formats”Markdown — human-readable report with sections for each change kind:
# Workflow Diff: checkout → checkout
## Summary- 1 added, 1 removed, 1 renamed, 0 moved, 2 unchanged
## Added Steps- `apply-discount` (deps.applyDiscount)
## Removed Steps- `send-receipt` (deps.sendReceipt)
## Renamed Steps- `validate-cart` → `verify-cart` (deps.validateCart)
## Unchanged Steps- `fetch-cart` (deps.fetchCart)- `charge-payment` (deps.chargePayment)The regressionMode option prepends a warning icon to the Removed Steps heading.
JSON — structured WorkflowDiff object for CI checks, dashboards, or piping into other tools:
{ "steps": [ { "kind": "unchanged", "stepId": "fetch-cart", "callee": "deps.fetchCart" }, { "kind": "unchanged", "stepId": "charge-payment", "callee": "deps.chargePayment" }, { "kind": "renamed", "stepId": "verify-cart", "previousStepId": "validate-cart", "callee": "deps.validateCart" }, { "kind": "removed", "stepId": "send-receipt", "callee": "deps.sendReceipt" }, { "kind": "added", "stepId": "apply-discount", "callee": "deps.applyDiscount" } ], "summary": { "stepsAdded": 1, "stepsRemoved": 1, "stepsRenamed": 1, "stepsMoved": 0, "stepsUnchanged": 2, "hasRegressions": false }}Mermaid — renders the “after” workflow diagram with diff styling overlaid. Added steps get a green fill, renamed steps orange, moved steps blue. Removed steps appear as dashed ghost nodes:
flowchart TB start((Start)) step_1["fetch-cart"] step_2["verify-cart"] step_3["apply-discount"] step_4["charge-payment"] end_node((End)) step_1 --> step_2 step_2 --> step_3 step_3 --> step_4 start --> step_1 step_4 --> end_node classDef diffAddedStyle fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px classDef diffRemovedStyle fill:#ffcdd2,stroke:#c62828,stroke-width:2px,stroke-dasharray:5 classDef diffRenamedStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px classDef stepStyle fill:#e1f5fe,stroke:#01579b classDef startStyle fill:#c8e6c9,stroke:#2e7d32 classDef endStyle fill:#ffcdd2,stroke:#c62828 class start startStyle class end_node endStyle class step_1 stepStyle class step_4 stepStyle removed_1["❌ send-receipt"] class step_2 diffRenamedStyle class step_3 diffAddedStyle class removed_1 diffRemovedStyle
Style classes diffAddedStyle, diffRemovedStyle, diffRenamedStyle, and diffMovedStyle are injected automatically.
Diff options reference
Section titled “Diff options reference”diffWorkflows(before, after, options?)
| Option | Type | Default | Description |
|---|---|---|---|
detectRenames | boolean | true | Match steps with the same callee and position index as renames instead of remove + add |
regressionMode | boolean | false | Flag removed steps as regressions in the summary |
renderDiffMarkdown(diff, options?)
| Option | Type | Default | Description |
|---|---|---|---|
showUnchanged | boolean | true | Include the Unchanged Steps section |
title | string | auto | Override the report title |
renderDiffMermaid(afterIR, diff, options?)
| Option | Type | Default | Description |
|---|---|---|---|
showRemovedSteps | boolean | true | Add ghost nodes for removed steps |
direction | string | TB | Mermaid flowchart direction (TB, LR, BT, RL) |
Diff types
Section titled “Diff types”All diff types are exported from awaitly-analyze:
import type { WorkflowDiff, StepDiffEntry, StepChangeKind, StructuralChange, DiffSummary, DiffOptions, DiffMarkdownOptions, DiffMermaidOptions,} from 'awaitly-analyze';Limitations
Section titled “Limitations”- Dynamic code: Template literals in step keys show as
<dynamic> - External imports: Referenced workflows must be in the same file to resolve