---
title: Isomorphic Packages
description: Publish reusable workflow packages that work both inside and outside the workflow runtime.
type: guide
summary: Use try/catch around getWorkflowMetadata, dynamic imports, and optional peer dependencies to build libraries that run in workflows and in plain Node.js.
---

# Isomorphic Packages



<Callout>
  This is an advanced guide. It dives into workflow internals and is not required reading to use workflow.
</Callout>

## 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 run** — `getWorkflowMetadata()` 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.

```typescript lineNumbers
import { getWorkflowMetadata } from "workflow";

export async function processPayment(amount: number, currency: string) {
  "use workflow";

  let runId: string | undefined;
  try {
    const metadata = getWorkflowMetadata(); // [!code highlight]
    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); // [!code highlight]
  } 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.

```typescript lineNumbers
export async function createDurableTask(name: string, payload: unknown) {
  "use workflow";

  let sleep: ((duration: string) => Promise<void>) | undefined;

  try {
    const wf = await import("workflow"); // [!code highlight]
    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"); // [!code highlight]
  } 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.

```json
{
  "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

* [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
* [`"use step"`](/docs/api-reference/workflow/use-step) — marks functions for durable execution
* [`getWorkflowMetadata`](/docs/api-reference/workflow/get-workflow-metadata) — runtime detection and run ID access


## Sitemap
[Overview of all docs pages](/sitemap.md)
