Overview

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:

  1. Inside a workflow rungetWorkflowMetadata() works, "use step" directives are transformed, and the full workflow runtime is available.
  2. 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

  1. Never hard-import workflow at the top level if your package should work without it.
  2. Use getWorkflowMetadata() in a try/catch as the canonical runtime detection pattern.
  3. Mark workflow as an optional peer dependency in package.json.
  4. Test both paths: run your test suite with and without the workflow runtime to catch import errors.
  5. Document the dual behavior: make it clear in your README which features require workflow and which work standalone.

Key APIs