Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/framework-shared-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@workflow/nest": minor
"@workflow/next": minor
"@workflow/nitro": minor
---

Add typed shared configuration support to the Next.js, Nitro, and Nest integrations.
6 changes: 6 additions & 0 deletions .changeset/lazy-world-factories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@workflow/config": minor
"@workflow/world": minor
---

Add lazy World factories and a shared Workflow configuration schema.
8 changes: 8 additions & 0 deletions .changeset/shared-config-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@workflow/builders": minor
"@workflow/cli": minor
"@workflow/core": minor
"workflow": minor
---

Load shared Workflow configuration across runtime, build, and CLI entry points.
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,19 @@ prerequisites:
- /docs/getting-started/next
---

Configures webpack/turbopack loaders to transform workflow code (`"use step"`/`"use workflow"` directives)

## Usage

To enable `"use step"` and `"use workflow"` directives while developing locally or deploying to production, wrap your `nextConfig` with `withWorkflow`.

```typescript title="next.config.ts" lineNumbers
import { withWorkflow } from "workflow/next"; // [!code highlight]
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
// … rest of your Next.js config
};

// not required but allows configuring workflow options
const workflowConfig = {}

export default withWorkflow(nextConfig, workflowConfig); // [!code highlight]
export default withWorkflow(nextConfig); // [!code highlight]
```

<Callout type="warn">
Expand Down Expand Up @@ -58,7 +53,9 @@ Use the smallest directory that contains every workspace package imported by you

## Options

`withWorkflow` accepts an optional second argument to configure the Next.js integration.
Use [`workflow.config.ts`](/docs/foundations/configuration) for shared build and
Next.js settings. The optional second argument to `withWorkflow` overrides
environment variables and `workflow.config.ts`.

```typescript title="next.config.ts" lineNumbers
import type { NextConfig } from "next";
Expand All @@ -80,7 +77,7 @@ export default withWorkflow(nextConfig, {
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `workflows.lazyDiscovery` | `boolean` | `true` | Defers workflow discovery until files are requested instead of scanning eagerly at startup. Set to `false` to force eager discovery (scanning the project up front). Requires a Next.js version that supports deferred entries; older versions fall back to eager discovery automatically. |
| `workflows.local.port` | `number` | — | Overrides the `PORT` environment variable for local development. Has no effect when deployed to Vercel. |
| `workflows.local.port` | `number` | — | Sets the local Workflow server port. Has no effect when deployed to Vercel. |
| `workflows.sourcemap` | `boolean \| 'inline' \| 'linked' \| 'external' \| 'both'` | `'inline'` | Controls source maps on generated workflow bundles. See [Source maps](#source-maps) below. |

### Source maps
Expand All @@ -101,19 +98,21 @@ Setting `sourcemap: false` is the main escape hatch for users hitting the Vercel
Setting `sourcemap` explicitly affects **all** generated bundles (steps, workflows, webhook). The legacy `WORKFLOW_EMIT_SOURCEMAPS_FOR_DEBUGGING=1` environment variable is narrower — it only toggles source maps on the final workflow wrapper and webhook bundle (which default to off). It continues to work, but new code should use the `sourcemap` option or the `WORKFLOW_SOURCEMAP` environment variable instead.
</Callout>

The option can also be set via the `WORKFLOW_SOURCEMAP` environment variable, which accepts the same values plus `'0'` / `'1'` as aliases for `false` / `true`. Precedence is: explicit config > `WORKFLOW_SOURCEMAP` > per-bundle default.
The option can also be set via the `WORKFLOW_SOURCEMAP` environment variable,
which accepts the same values plus `'0'` / `'1'` as aliases for `false` /
`true`. Precedence is: explicit `withWorkflow` option >
`WORKFLOW_SOURCEMAP` > `workflow.config.ts` > per-bundle default.

<Callout type="info">
The `workflows.local` options only affect local development. When deployed to Vercel, the runtime ignores `local` settings and uses the Vercel world automatically.
</Callout>

## Exporting a Function


If you are exporting a function in your `next.config` you will need to ensure you call the function returned from `withWorkflow`.

```typescript title="next.config.ts" lineNumbers
import { NextConfig } from "next";
import type { NextConfig } from "next";
import { withWorkflow } from "workflow/next";
import createNextIntlPlugin from "next-intl/plugin";

Expand Down
9 changes: 7 additions & 2 deletions docs/content/docs/v5/api-reference/workflow-nitro/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ related:
- /docs/getting-started/nitro
---

Nitro integration for Workflow SDK. The `workflow/nitro` entry point's default export is a [Nitro module](https://v3.nitro.build/guide/modules) — it has no callable API. You enable it by adding it to the `modules` array of your Nitro config and configure it via the `workflow` key.
Add the [`workflow/nitro` Nitro module](https://v3.nitro.build/guide/modules) to
transform workflow directives, build bundles, and register runtime routes.

## Usage

Expand All @@ -20,6 +21,10 @@ export default defineConfig({
});
```

Shared build and Nitro settings can also be placed in
[`workflow.config.ts`](/docs/foundations/configuration). Values under
`workflow` in `nitro.config.ts` take precedence.

When enabled, the module:

- Transforms `"use workflow"` and `"use step"` directives during bundling.
Expand All @@ -30,7 +35,7 @@ When enabled, the module:

## Module Options

Options are read from the `workflow` key of your Nitro config. The option type is exported as `ModuleOptions`:
The `workflow` key accepts the exported `ModuleOptions` type:

```typescript title="nitro.config.ts" lineNumbers
import { defineConfig } from "nitro";
Expand Down
95 changes: 95 additions & 0 deletions docs/content/docs/v5/foundations/configuration.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
title: Configuration
description: Configure Workflow SDK with a typed workflow.config.ts file.
type: conceptual
summary: Configure the World, builds, and framework integration for your app.
prerequisites:
- /docs/foundations/workflows-and-steps
related:
- /docs/deploying/world/local-world
- /docs/deploying/world/postgres-world
- /docs/api-reference/workflow-next/with-workflow
---

Create `workflow.config.ts` in your application:

```typescript title="workflow.config.ts" lineNumbers
import type { WorkflowConfig } from "workflow/config";
import { createWorld } from "@workflow/world-postgres";

const config: WorkflowConfig = {
world: createWorld,
build: {
dirs: ["workflows"],
sourcemap: false,
},
integration: {
type: "next",
lazyDiscovery: true,
},
};

export default config;
```

<Callout type="info">
Next.js projects also wrap `next.config.ts` with `withWorkflow()`.
</Callout>

In a monorepo, place a config file in each application that needs its own
settings.

## Precedence

From highest to lowest priority:

1. Explicit CLI flags or framework options
2. Environment variables
3. `workflow.config.ts`
4. Built-in defaults

## World

`world` is the function used to create the application's World. Assign a
`createWorld` function directly, or wrap it when passing options.

## Worlds by Environment

Choose a World inside the factory based on the runtime environment:

```typescript title="workflow.config.ts" lineNumbers
import type { WorkflowConfig } from "workflow/config";
import { createLocalWorld } from "@workflow/world-local";
import { createWorld as createPostgresWorld } from "@workflow/world-postgres";

const config: WorkflowConfig = {
world: () => {
switch (process.env.NODE_ENV) {
case "production":
return createPostgresWorld();
case "development":
case "test":
return createLocalWorld();
default:
throw new Error(`Unexpected NODE_ENV: ${process.env.NODE_ENV}`);
}
},
};

export default config;
```

## Integration Settings

Use `integration` for framework-specific settings. Set `type` to the framework
you are configuring:

{/* @skip-typecheck: mutually exclusive config fragments */}

```typescript
// Next.js
integration: { type: "next", lazyDiscovery: true }

// Nitro
integration: { type: "nitro", typescriptPlugin: true, runtime: "nodejs24.x" }
```
1 change: 1 addition & 0 deletions docs/content/docs/v5/foundations/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"cancellation",
"serialization",
"idempotency",
"configuration",
"versioning"
],
"defaultOpen": true
Expand Down
2 changes: 2 additions & 0 deletions packages/builders/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@
},
"dependencies": {
"@swc/core": "catalog:",
"@workflow/config": "workspace:*",
"@workflow/core": "workspace:*",
"@workflow/errors": "workspace:*",
"@workflow/utils": "workspace:*",
"@workflow/swc-plugin": "workspace:*",
"@workflow/world": "workspace:*",
"builtin-modules": "5.0.0",
"chalk": "5.6.2",
"enhanced-resolve": "catalog:",
Expand Down
66 changes: 51 additions & 15 deletions packages/builders/src/base-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,29 @@ export abstract class BaseBuilder {
return this.config.moduleSpecifierRoot || this.transformProjectRoot;
}

protected get queueNamespace(): string | undefined {
return (
process.env.WORKFLOW_QUEUE_NAMESPACE ??
this.config.workflowConfig?.config.queue?.namespace
);
}

private get runtimeConfigPlugins(): esbuild.Plugin[] {
const path = this.config.workflowConfig?.path;
if (!path) return [];
return [
{
name: 'workflow-runtime-config',
setup(build) {
build.onResolve(
{ filter: /^@workflow\/config\/runtime-binding$/ },
() => ({ path })
);
},
},
];
}

/**
* Whether informational BaseBuilder logs should be printed.
* Subclasses can override this to silence progress logs while keeping warnings/errors.
Expand Down Expand Up @@ -1109,11 +1132,11 @@ export abstract class BaseBuilder {
`${Date.now() - bundleStartTime}ms`
);

if (this.config.workflowManifestPath) {
const resolvedPath = resolve(
process.cwd(),
this.config.workflowManifestPath
);
const workflowManifestPath =
this.config.workflowManifestPath ??
this.config.workflowConfig?.config.build?.manifest?.output;
if (workflowManifestPath) {
const resolvedPath = this.resolvePath(workflowManifestPath);
let prefix = '';

if (resolvedPath.endsWith('.cjs')) {
Expand Down Expand Up @@ -1182,8 +1205,9 @@ export abstract class BaseBuilder {
}
}

const workflowEntrypointOptionsCode =
createWorkflowEntrypointOptionsCode();
const workflowEntrypointOptionsCode = createWorkflowEntrypointOptionsCode(
this.queueNamespace
);

const bundleFinal = async (interimBundle: string) => {
const workflowBundleCode = interimBundle;
Expand Down Expand Up @@ -1241,6 +1265,7 @@ export const POST = workflowEntrypoint(workflowCode${workflowEntrypointOptionsCo
keepNames: true,
minify: false,
external: ['@aws-sdk/credential-provider-web-identity'],
plugins: this.runtimeConfigPlugins,
});

this.logEsbuildMessages(
Expand Down Expand Up @@ -1373,7 +1398,9 @@ export const POST = workflowEntrypoint(workflowCode${workflowEntrypointOptionsCo
// 3. Generate combined route file
const stepsRelativePath = './' + basename(stepsOutfile).replace(/\\/g, '/');
const escapedVMCode = workflowVMCode.replace(/[\\`$]/g, '\\$&');
const workflowEntrypointOptionsCode = createWorkflowEntrypointOptionsCode();
const workflowEntrypointOptionsCode = createWorkflowEntrypointOptionsCode(
this.queueNamespace
);

const combinedFunctionCode = `// biome-ignore-all lint: generated file
/* eslint-disable */
Expand Down Expand Up @@ -1422,6 +1449,7 @@ export const POST = workflowEntrypoint(workflowCode${workflowEntrypointOptionsCo
minify: false,
define: importMetaDefine,
external: ['@aws-sdk/credential-provider-web-identity'],
plugins: this.runtimeConfigPlugins,
});
this.logEsbuildMessages(finalResult, 'combined bundle', true);
this.logBaseBuilderInfo(
Expand All @@ -1446,8 +1474,9 @@ export const POST = workflowEntrypoint(workflowCode${workflowEntrypointOptionsCo
// Create a custom bundleFinal for watch mode that uses workflowEntrypoint
const combinedBundleFinal = async (interimBundleText: string) => {
const escaped = interimBundleText.replace(/[\\`$]/g, '\\$&');
const workflowEntrypointOptionsCode =
createWorkflowEntrypointOptionsCode();
const workflowEntrypointOptionsCode = createWorkflowEntrypointOptionsCode(
this.queueNamespace
);
const code = `// biome-ignore-all lint: generated file
/* eslint-disable */
import { __steps_registered } from '${stepsRelativePath}';
Expand Down Expand Up @@ -1701,6 +1730,7 @@ export const OPTIONS = handler;`;
mainFields: ['module', 'main'],
// Don't externalize anything - bundle everything including workflow packages
external: [],
plugins: this.runtimeConfigPlugins,
});

this.logEsbuildMessages(result, 'webhook bundle creation');
Expand Down Expand Up @@ -1837,10 +1867,14 @@ export const OPTIONS = handler;`;

/**
* Whether the manifest should be exposed as a public HTTP route.
* Controlled by the `WORKFLOW_PUBLIC_MANIFEST` environment variable.
* WORKFLOW_PUBLIC_MANIFEST takes precedence over workflow.config.ts.
*/
protected get shouldExposePublicManifest(): boolean {
return process.env.WORKFLOW_PUBLIC_MANIFEST === '1';
if (process.env.WORKFLOW_PUBLIC_MANIFEST !== undefined) {
return process.env.WORKFLOW_PUBLIC_MANIFEST === '1';
}

return this.config.workflowConfig?.config.build?.manifest?.public ?? false;
}

/**
Expand All @@ -1865,14 +1899,16 @@ export const OPTIONS = handler;`;

/**
* Resolve the effective source map mode for a given call site. Precedence:
* explicit `sourcemap` config > `WORKFLOW_SOURCEMAP` env var > the call
* site's default. Returned value is passed directly to esbuild's
* `sourcemap` option.
* builder option > WORKFLOW_SOURCEMAP > workflow.config.ts > the call site's
* default. Returned value is passed directly to esbuild's `sourcemap`
* option.
*/
protected resolveSourcemap(defaultMode: SourcemapMode): SourcemapMode {
if (this.config.sourcemap !== undefined) return this.config.sourcemap;
const envMode = parseSourcemapEnv(process.env.WORKFLOW_SOURCEMAP);
if (envMode !== undefined) return envMode;
const configMode = this.config.workflowConfig?.config.build?.sourcemap;
if (configMode !== undefined) return configMode;
return defaultMode;
}

Expand Down
Loading
Loading