Isomorphic Packages
Publish reusable workflow packages that work both inside and outside the workflow runtime.
This is an advanced guide. It dives into workflow internals and is not required reading to use workflow.
The Challenge
If you're a library author publishing a package that integrates with workflow, your code needs to handle two environments:
- Inside a workflow run —
getWorkflowMetadata()works,"use step"directives are transformed, and the full workflow runtime is available. - Outside a workflow — your package is imported in a regular Node.js process, a test suite, or a project that doesn't use workflow at all.
A hard dependency on workflow will crash at import time for users who don't have it installed.
Pattern 1: Feature-Detect with getWorkflowMetadata
Use a try/catch to detect whether you're running inside a workflow. This lets you add durable behavior when available and fall back to standard execution otherwise.
import { getWorkflowMetadata } from "workflow";
export async function processPayment(amount: number, currency: string) {
"use workflow";
let runId: string | undefined;
try {
const metadata = getWorkflowMetadata();
runId = metadata.workflowRunId;
} catch {
// Not running inside a workflow — proceed without durability
runId = undefined;
}
if (runId) {
// Inside a workflow: use the run ID as an idempotency key
return await chargeWithIdempotency(amount, currency, runId);
} else {
// Outside a workflow: standard charge
return await chargeStandard(amount, currency);
}
}
async function chargeWithIdempotency(amount: number, currency: string, idempotencyKey: string) {
"use step";
// Stripe charge with idempotency key from workflow run ID
return { charged: true, amount, currency, idempotencyKey };
}
async function chargeStandard(amount: number, currency: string) {
"use step";
return { charged: true, amount, currency };
}Pattern 2: Dynamic Imports
Avoid importing workflow at the top level. Use dynamic import() so the module is only loaded when actually needed.
export async function createDurableTask(name: string, payload: unknown) {
"use workflow";
let sleep: ((duration: string) => Promise<void>) | undefined;
try {
const wf = await import("workflow");
sleep = wf.sleep;
} catch {
// workflow not installed — use setTimeout fallback
sleep = undefined;
}
await executeTask(name, payload);
if (sleep) {
// Inside workflow: durable sleep that survives restarts
await sleep("5m");
} else {
// Outside workflow: plain timer (not durable)
await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000));
}
await sendNotification(name);
}
async function executeTask(name: string, payload: unknown) {
"use step";
return { executed: true, name, payload };
}
async function sendNotification(name: string) {
"use step";
return { notified: true, name };
}Pattern 3: Optional Peer Dependencies
In your package.json, declare workflow as an optional peer dependency. This signals to package managers that your library can use workflow but doesn't require it.
{
"name": "@acme/payments",
"peerDependencies": {
"workflow": ">=1.0.0"
},
"peerDependenciesMeta": {
"workflow": {
"optional": true
}
}
}Then guard all workflow imports with dynamic import() and try/catch as shown above.
Real-World Examples
Mux AI
The Mux team published a reusable workflow package for video processing. Their library detects the workflow runtime and falls back to standard async processing when workflow isn't available.
World ID
World ID's identity verification library uses getWorkflowMetadata() to attach run IDs to their human-in-the-loop verification hooks, but the same library works in non-workflow environments for simple verification flows.
Guidelines for Library Authors
- Never hard-import
workflowat the top level if your package should work without it. - Use
getWorkflowMetadata()in a try/catch as the canonical runtime detection pattern. - Mark
workflowas an optional peer dependency inpackage.json. - Test both paths: run your test suite with and without the workflow runtime to catch import errors.
- Document the dual behavior: make it clear in your README which features require workflow and which work standalone.
Key APIs
"use workflow"— declares the orchestrator function"use step"— marks functions for durable executiongetWorkflowMetadata— runtime detection and run ID access