Static Analysis
TL;DR
Use awaitly-analyze to extract workflow structure from TypeScript, then render Mermaid/JSON/HTML artifacts for docs, review, and CI checks without running workflows.
When To Use
- You want architecture diagrams from source code, not runtime traces.
- You need deterministic workflow docs in CI.
- You want workflow diffs in PR review.
- You need rule-style diagnostics with stable slug codes.
Static Analysis Pipeline
Analyze Source
Parse workflow file and extract IR tree, stats, and references.
Render Artifact
Generate Mermaid, JSON, markdown report, or interactive HTML.
Apply In Workflow
Use output in docs, PRs, CI gates, and architecture reviews.
Deterministic diagrams from real code
Section titled “Deterministic diagrams from real code”awaitly-analyze reads executable TypeScript and derives the diagram from the code that runs.
Imperative control flow (if, for, computed step ids) produces diagrams with unstable structure. Use declarative control flow for steps that need stable diagram nodes:
- Use
step.if,when, orunlessfor branches that contain steps. Branches keep stable, labelled ids. - Use
step.forEachfor loops. Each iteration gets a structured id. - Use literal step ids and put dynamic parts in
key. Each node keeps a stable identity.
The analyzer reads native if/else, for...of, and while blocks. It derives a branch id from the condition and a loop id from the iterable, so plain TypeScript produces deterministic diagrams without a DSL. Run --assert-diagrammable in CI. It flags conditions and iterables that require runtime values, such as a call result or computed member. Hoist the value into a named variable to give the graph a stable identity.
See Control flow and diagrammability for the workflow authoring patterns.
Fast start
Section titled “Fast start”# Mermaid markdown (default)npx awaitly-analyze ./src/workflows/checkout.ts
# JSONnpx awaitly-analyze ./src/workflows/checkout.ts --format=json
# Interactive HTML artifactnpx awaitly-analyze ./src/workflows/checkout.ts --html
# Workflow diffnpx awaitly-analyze --diff v1.ts v2.ts
# Strict diagnosticsnpx awaitly-analyze ./src/workflows/checkout.ts --doctor
# CI gate: fail if a workflow has unstable graph identitiesnpx awaitly-analyze ./src/workflows/checkout.ts --assert-diagrammable
# Overlay a recorded run's executed path onto the static diagramnpx awaitly-analyze ./src/workflows/checkout.ts --trace=./run-events.json
# Live inspector: static graph + runtime trace overlay in the browsernpx awaitly-analyze ./src/workflows/checkout.ts --devnpx awaitly-analyze ./src/workflows/checkout.ts --dev --port=5000What the analyzer reads
Section titled “What the analyzer reads”- Step IDs from
step('id', fn, opts)as canonical names. - Workflow and step docs from
descriptionandmarkdown. - JSDoc descriptions as
jsdocDescriptionfallback. - Optional step metadata:
intent,domain,owner,tags,stateChanges,emits,calls,errorMeta.
Follow one checkout through the analyzer
Section titled “Follow one checkout through the analyzer”This checkout has a free-order branch and a paid branch. The payment dependency carries a timeout and retry policy. The receipt dependency has a fallback, so RECEIPT_FAILED does not reach the final error union.
import { fallback, ok, retry, run, timeout } from 'awaitly';import { charge, loadCart, sendReceipt } from './checkout-deps';
export const checkout = (cartId: string) => run( { loadCart, charge: retry(timeout(charge, 2_000), { attempts: 3, delay: 100, backoff: 'exponential', }), sendReceipt: fallback(sendReceipt, () => ok({ queued: true })), }, async (steps) => { const cart = await steps.loadCart(cartId);
if (cart.total === 0) { return { kind: 'free' as const, cart }; }
const payment = await steps.charge(cart.total); const receipt = await steps.sendReceipt(cart.email); return { kind: 'paid' as const, cart, payment, receipt }; }, );Use this file to answer four review questions without charging a card.
Check the resilience policy
Section titled “Check the resilience policy”Read the dependency facts from JSON:
npx awaitly-analyze ./checkout.ts --format=json --no-types \ | jq '.workflows[0].root.dependencies[] | { name, errorTypes, policies }'The charge record contains the timeout followed by the retry wrapper. The sendReceipt record shows a fallback. Reviewers can inspect the exact policy chain without tracing through wrapper types or running the workflow.
Generate tests from both branches
Section titled “Generate tests from both branches”import { analyze, formatTestMatrixMarkdown, generatePaths, generateTestMatrix,} from 'awaitly-analyze';
const workflow = analyze('./checkout.ts').single();const paths = generatePaths(workflow);const matrix = generateTestMatrix(paths, {}, workflow);
console.log(formatTestMatrixMarkdown(matrix));The matrix includes the cart.total === 0 path and the paid path through charge and sendReceipt. Pass the workflow IR as the third argument to rank paths using errorMeta when your steps classify business and infrastructure failures.
Block diagram drift in CI
Section titled “Block diagram drift in CI”- name: Check workflow diagrams run: | pnpm exec awaitly-analyze src/workflows/checkout.ts \ --doctor --format=json --no-types pnpm exec awaitly-analyze src/workflows/checkout.ts \ --assert-diagrammable --no-typesThe first command reports source-level diagnostics, including missing ids and unmodeled awaits. Each diagnostic includes a location. The second command returns a non-zero exit code when the analyzer cannot assign stable identities to the graph.
Review the executed path after a failure
Section titled “Review the executed path after a failure”Capture WorkflowEvent[] from onEvent, save the array as run-events.json, and overlay it:
npx awaitly-analyze ./checkout.ts \ --trace=./run-events.json --no-types \ -o --suffix=failed-runThe output keeps both checkout branches visible and marks the steps from that run. unmatched trace ids expose runtime steps that have no node in the static graph.
Browse Analyzer Showcase for source and generated diagrams covering retries, resources, loops, workflow composition, and sagas.
CLI reference
Section titled “CLI reference”Core flags
Section titled “Core flags”| Flag | Default | Description |
|---|---|---|
--format=<fmt> |
mermaid |
mermaid, json, markdown |
--direction=<dir> |
TB |
TB, TD, LR, BT, RL |
--railway |
off | Linear happy-path + ok/err branching |
--keys |
off | Show step cache keys |
--errors / --no-errors |
on | Show/hide error nodes |
--html |
off | Generate interactive HTML artifact |
--html-output=<path> |
auto | Write HTML to custom path |
Output control
Section titled “Output control”| Flag | Default | Description |
|---|---|---|
--types / --no-types |
on | Generate .types.ts |
--test / --no-test |
off | Generate test stubs |
--test-runner=<runner> |
vitest |
vitest, jest, mocha |
-o, --output-adjacent |
off | Write output next to source |
--suffix=<value> |
workflow |
Adjacent output file suffix |
--no-stdout |
off | Suppress stdout output |
--dsl-output=<value> |
off |
Write DSL output |
--write-dsl |
off | Same as --dsl-output=.awaitly |
--watch |
off | Re-analyze on source changes |
Diagrammability and trace
Section titled “Diagrammability and trace”| Flag | Default | Description |
|---|---|---|
--assert-diagrammable |
off | Exit non-zero if a workflow has unstable graph identities |
--trace=<path> |
- | Overlay a recorded run’s executed path onto the static Mermaid diagram |
--dev |
off | Serve a live workflow inspector at http://localhost:4747 (SSE + runtime trace overlay) |
--port=<n> |
4747 |
Port for --dev (use with --dev) |
Live inspector
Section titled “Live inspector”--dev starts a local HTTP server that serves the static workflow diagram for a file, watches the source for changes, and accepts runtime event streams from running workflows. Each run’s trace is overlaid on the static graph: the full shape stays visible while live runs paint their path.
npx awaitly-analyze ./src/workflows/checkout.ts --dev# → http://localhost:4747
npx awaitly-analyze ./src/workflows/checkout.ts --dev --port=5000Wire onEvent to stream runs into the inspector. Use devEvents from awaitly-visualizer (see Visualization →) or POST events yourself to /events. Updates use Server-Sent Events; no WebSocket server required.
Lock the diagram to reality
Section titled “Lock the diagram to reality”Pass a declared graph at workflow creation or per run so each step and decision id must appear in the graph. Awaitly rejects a runtime id that does not appear in the graph, which keeps execution aligned with the static diagram.
import { createWorkflow } from 'awaitly';
const workflow = createWorkflow('checkout', deps, { graph: ['fetchUser', 'chargeCard', 'premium-check'],});
// Plain id list or WorkflowDiagramDSL from awaitly-analyzeawait run(async ({ step }) => { /* ... */ }, { graph: ['fetchUser', 'item-{i}'], // {placeholder} matches item-0, item-1, ...});Pair with --assert-diagrammable in CI: the analyzer checks that source is diagrammable; graph enforces that runtime matches the declared shape. See Workflow options →.
Interactive HTML artifact
Section titled “Interactive HTML artifact”# Writes <basename>.html next to workflow sourcenpx awaitly-analyze ./src/workflows/checkout.ts --html
# Custom pathnpx awaitly-analyze ./src/workflows/checkout.ts --html --html-output=./docs/checkout-diagram.htmlThe HTML output includes:
- Mermaid rendered client-side.
- Click-to-inspect node details.
- Built-in theme picker with persistence.
- Self-contained payload for easy sharing.
Programmatic API
Section titled “Programmatic API”import { analyze, renderStaticMermaid, renderStaticJSON, extractNodeMetadata, generateInteractiveHTML,} from 'awaitly-analyze';
const ir = analyze('./src/workflows/checkout.ts').single();
const mermaid = renderStaticMermaid(ir, { direction: 'TB', showKeys: false });const json = renderStaticJSON(ir, { pretty: true });
const metadata = extractNodeMetadata(ir);const html = generateInteractiveHTML(mermaid, metadata, { title: 'Checkout Workflow', direction: 'TB',});Diagrammability
Section titled “Diagrammability”Check whether each graph node has a stable identity. Each issue names the construct that closes the gap:
import { analyze, computeDiagrammability } from 'awaitly-analyze';
const ir = analyze('./src/workflows/checkout.ts').single();const report = computeDiagrammability(ir);
report.deterministic; // true when there are no gapsreport.score; // 0-100, share of nodes with a stable identityreport.issues; // [{ kind, message, suggestion, location, nodeId }]// kinds: 'dynamic-step-id' | 'dynamic-decision-id' | 'raw-conditional'// | 'raw-loop' | 'unbounded-loop' | 'unknown-node'Use --assert-diagrammable on the CLI to turn this into a CI gate.
Runtime trace overlay
Section titled “Runtime trace overlay”Render the static skeleton and highlight the path a real run took. Record events via the workflow onEvent option, then overlay them onto the static diagram:
import { analyze, traceFromEvents, renderStaticMermaidWithTrace,} from 'awaitly-analyze';
const ir = analyze('./src/workflows/checkout.ts').single();const trace = traceFromEvents(recordedEvents); // WorkflowEvent[] → per-step status
const { mermaid, matched, unmatched } = renderStaticMermaidWithTrace(ir, trace);// `unmatched` lists trace steps with no static node; empty when the workflow// has stable identities (literal step ids).Fluent selection helpers
Section titled “Fluent selection helpers”| Method | Returns | Throws | Best for |
|---|---|---|---|
.single() |
Single IR | If 0 or >1 workflows | Single-workflow file |
.singleOrNull() |
IR or null | Never | Optional single workflow |
.all() |
IR array | Never | Multi-workflow iteration |
.named(name) |
Single IR | If not found | Named workflow selection |
.first() |
Single IR | If empty | First workflow only |
.firstOrNull() |
IR or null | Never | Safe first workflow |
Diff workflows
Section titled “Diff workflows”# Local filesnpx awaitly-analyze --diff before.ts after.ts
# HEAD vs working copynpx awaitly-analyze --diff src/workflows/checkout.ts
# Git ref vs localnpx awaitly-analyze --diff main:src/workflows/checkout.ts src/workflows/checkout.ts
# GitHub PRnpx awaitly-analyze --diff gh:#123import { 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, regressionMode: false,});
const md = renderDiffMarkdown(diff, { showUnchanged: true });const json = renderDiffJSON(diff);const mermaid = renderDiffMermaid(after, diff, { showRemovedSteps: true, direction: 'TB' });Diff options
Section titled “Diff options”| Option | Default | Description |
|---|---|---|
detectRenames |
true |
Match same callee+position as rename |
regressionMode |
false |
Mark removals as regressions |
--doctor diagnostics
Section titled “--doctor diagnostics”Use strict slug-keyed diagnostics aligned with runtime and ESLint rule naming.
awaitly-analyze ./src/workflows/checkout.ts --doctorawaitly-analyze ./src/workflows/checkout.ts --doctor --format=jsonSample output:
✗ [step-require-id]:12:4 Step "<missing>" uses legacy signature without explicit ID Docs: https://jagreehal.github.io/awaitly/rules/#step-require-id Fix: Use step('id', fn, opts) instead of step(fn, opts)Analyzer output model
Section titled “Analyzer output model”interface StaticWorkflowIR { root: StaticWorkflowNode; metadata: StaticAnalysisMetadata; references: Map<string, StaticWorkflowIR>;}
interface AnalysisStats { totalSteps: number; conditionalCount: number; parallelCount: number; raceCount: number; loopCount: number; workflowRefCount: number; unknownCount: number;}Node categories:
StaticStepNodeStaticSequenceNodeStaticParallelNodeStaticRaceNodeStaticConditionalNodeStaticLoopNodeStaticWorkflowRefNodeStaticUnknownNode
Feature detection coverage
Section titled “Feature detection coverage”| Feature | Detection |
|---|---|
| Steps | step(), step.retry(), step.withTimeout() |
| Conditionals | if/else, when*, unless* |
| Loops | for, while, for-of, for-in |
| Parallel | step.all(), allAsync(), allSettledAsync() |
| Race | step.race(), anyAsync() |
| Workflow refs | Child workflow run calls |
Which Output Should You Use?
| Choice | Best for | Tradeoff |
|---|---|---|
--format=mermaid | Readable architecture diagrams in docs and PRs. | Less detail than full JSON payload. |
--format=json | CI, automation, and custom tooling. | Harder for humans to scan at a glance. |
--html | Shareable deep review with click-to-inspect details. | Heavier artifact than markdown output. |
--diff | PR-level workflow change review. | Requires clean baseline/version selection. |
--doctor | Strict diagnostics and policy enforcement. | Can surface warnings that require migration work. |
--assert-diagrammable | CI gate that blocks non-deterministic workflow diagrams. | Requires declarative control flow (step.if, when, step.forEach). |
--trace | Highlight executed path on a static diagram from a recorded run. | Requires capturing onEvent output and a diagrammable workflow. |
--dev | Local live inspector: static graph + runtime trace overlay in the browser. | Dev-only; wire onEvent (e.g. devEvents) to stream runs. |
Limitations
Section titled “Limitations”- Dynamic step IDs can appear as
<dynamic>. - The analyzer resolves external workflow references when their source is available in the project.
- An awaited call inside a workflow callback that static analysis cannot model (helper function, bare
await deps.fn()) surfaces as anunknownnode with anUNANALYZED_AWAITwarning in--doctoroutput rather than being dropped. Wrap the async work instep()to close the gap.