UNPKG

workflow

Version:

Workflow SDK - Build durable, resilient, and observable workflows

219 lines (160 loc) • 12.1 kB
--- title: withWorkflow description: Configure webpack and Turbopack to transform workflow directives in Next.js. type: reference summary: Wrap your Next.js config with withWorkflow to enable workflow directive transformation. prerequisites: - /docs/getting-started/next --- Configures webpack and Turbopack loaders to transform workflow code (`"use step"` and `"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] ``` <Callout type="warn"> If a package in `serverExternalPackages` contains workflow code (`"use step"`, `"use workflow"`, or serialization classes), `withWorkflow()` automatically removes it from `serverExternalPackages` for the current build and prints a warning. Workflow still compiles the package so its directives are transformed. Remove that package from `serverExternalPackages` in your `next.config` to silence the warning. </Callout> ### Workflow discovery in Next.js `withWorkflow()` discovers workflows by scanning your Next.js entrypoints (App Router `route`, `page`, and `layout` files under `app/` or `src/app/`, and any file under `pages/` or `src/pages/`) for `start()` calls imported from `workflow/api`. The workflow and step files themselves can live anywhere (for example `src/workflows/`); they are discovered transitively through imports, as long as a `start()` call in an entrypoint statically reaches them. <Callout type="info"> Call `start()` from server-side entrypoints, including Route Handlers and Server Actions. Don't call workflow functions directly, which bypasses the workflow runtime. </Callout> ### Next.js server actions and `"use server"` Don't put a top-level `"use server"` directive in modules imported by workflow or step functions. Workflow transformation wraps imported modules in synchronous initializers, and Next.js rejects a `"use server"` directive inside that wrapper with errors like `Server Actions must be async functions`. Keep `"use server"` on the files that define your Server Actions, and move shared logic into separate modules that don't carry the directive. ### Monorepos and workspace imports By default, Next.js detects the correct workspace root automatically. If your Next.js app lives in a subdirectory such as `apps/web` and workspace resolution is not working correctly, you can set `outputFileTracingRoot` as a workaround: ```typescript title="apps/web/next.config.ts" lineNumbers import { resolve } from "node:path"; import type { NextConfig } from "next"; import { withWorkflow } from "workflow/next"; const nextConfig: NextConfig = { outputFileTracingRoot: resolve(process.cwd(), "../.."), }; export default withWorkflow(nextConfig); ``` <Callout type="info"> Use the smallest directory that contains every workspace package imported by your workflows. If your app already lives at the repository root, you do not need to set `outputFileTracingRoot`. </Callout> ## Options `withWorkflow` accepts an optional second argument to configure the Next.js integration. ```typescript title="next.config.ts" lineNumbers import type { NextConfig } from "next"; import { withWorkflow } from "workflow/next"; const nextConfig: NextConfig = {}; export default withWorkflow(nextConfig, { workflows: { local: { port: 4000, }, sourcemap: false, }, }); ``` | Option | Type | Default | Description | | --- | --- | --- | --- | | `workflows.local.port` | `number` | — | Overrides the `PORT` environment variable for local development. Has no effect when deployed to Vercel. | | `workflows.sourcemap` | `boolean \| 'inline' \| 'linked' \| 'external' \| 'both'` | `'inline'` (dev) / `false` (prod) | Controls source maps on generated workflow bundles. See [Source maps](#source-maps) below. | | `workflows.experimentalRoutePrefix` | `string` | — | Serves the generated workflow routes below a route segment this app owns, such as `/dashboard`. See [Workflow route prefix](#workflow-route-prefix) below. | ### Source maps The step bundle and intermediate workflow bundle default to `'inline'` source maps **in development** (so stack traces from step errors and workflow virtual machine (VM) errors point at your source files) and to **`false` in production**, so function bundles stay small. The `sourcemap` option lets you change that: | Value | Behavior | | --- | --- | | `true` / `'inline'` | Base64-encode the source map and append it to the bundle (default in development). | | `'linked'` | Write a separate `.map` file and add a `sourceMappingURL` comment. | | `'external'` | Write a separate `.map` file without the comment. | | `'both'` | Emit both inline and external source maps. | | `false` | Omit source maps entirely. | In production, source maps are already off by default. Setting `sourcemap: false` explicitly also turns them off in development, and it drops the inline source map from every bundle while skipping the source-map-support runtime shim on the Vercel step function (the same behavior production gets by default), the main lever for staying under the Vercel 250 MB function size limit. The tradeoff is that workflow VM stack traces will reference generated code (for example, `evalmachine.<anonymous>`) rather than your source files. <Callout type="info"> 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` > the environment-aware default (`'inline'` in development, `false` in production). Development is detected from `next dev` / `NODE_ENV=development`, so the config option and the env var both let you force either behavior in either environment. <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> ### Workflow route prefix By default the integration generates its routes at the root of your app, so the flow route answers at `/.well-known/workflow/v1/flow`. Some deployments cannot own those paths. In a [Microfrontends](https://vercel.com/docs/microfrontends) setup, for example, a child app is routed a path such as `/dashboard/*` while requests for `/.well-known/*` go to the default app, so queue deliveries and webhooks aimed at the root never reach the child. `workflows.experimentalRoutePrefix` moves the generated workflow routes below a segment the app does own: ```typescript title="next.config.ts" lineNumbers import type { NextConfig } from "next"; import { withWorkflow } from "workflow/next"; const nextConfig: NextConfig = {}; export default withWorkflow(nextConfig, { workflows: { experimentalRoutePrefix: "/dashboard", // [!code highlight] }, }); ``` With that prefix, the build emits: | Artifact | Path | | --- | --- | | Flow route | `app/dashboard/.well-known/workflow/v1/flow/route.js` | | Webhook route | `app/dashboard/.well-known/workflow/v1/webhook/[token]/route.js` | | Vercel function config | `app/dashboard/.well-known/workflow/v1/config.json` | | Manifest | `app/dashboard/.well-known/workflow/v1/manifest.json` | The queue trigger that makes the flow route a queue consumer is written next to the prefixed route, so deliveries reach it without any additional routing configuration. Webhook URLs, health checks, and the local and Postgres queue targets are all built from the prefix, so `createWebhook()` returns a URL under `/dashboard/.well-known/workflow/v1/webhook/:token`. `getWorkflowMetadata().url` includes the prefix too (for example `https://example.com/dashboard`), so don't build URLs to the rest of your app from it. The prefix is a plain path of one or more segments. Leading and trailing slashes are optional. Anything Next.js would not serve at a fixed path is rejected when the config is created: dynamic segments such as `/[team]`, route groups such as `/(group)`, and `_`-prefixed segments, which Next.js excludes from routing entirely. Segments starting with `.` are rejected too, because the build could not clean up routes generated below them after the prefix changes. <Callout type="warn"> If you cache your build with Turborepo, update the `outputs` globs to the prefixed paths. `app/.well-known/workflow/**` no longer matches `app/dashboard/.well-known/workflow/**`, and a cache hit would restore a build with no workflow routes in it. </Callout> <Callout type="warn"> If the app has `proxy.ts` or middleware, its matcher must exclude the prefixed path. A pattern such as `/((?!.well-known/workflow/).*)` only matches from the start of the path, so it no longer skips `/dashboard/.well-known/workflow/v1/flow`; exclude `dashboard/.well-known/workflow/` instead. </Callout> <Callout type="warn"> Your routing layer must send the prefixed paths to this app. With Microfrontends, that means choosing a prefix the child app is already routed in the group's [routing configuration](https://vercel.com/docs/microfrontends/routing). </Callout> This option is experimental and its name may change. #### Compared with `basePath` The integration already honors Next.js' [`basePath`](https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath): Next.js serves every route, including the generated workflow routes, below it, and `withWorkflow` passes it to the runtime so webhook and queue URLs include it. That only helps when the whole app can move under one path. A Microfrontends child app usually can't: its pages already live at `app/dashboard/...`, so `basePath: "/dashboard"` would serve them at `/dashboard/dashboard/...`. `experimentalRoutePrefix` moves only the generated workflow routes and their function config into the app tree; the rest of your routes keep their paths. The two compose: with `basePath: "/v2"` and `experimentalRoutePrefix: "/dashboard"`, the flow route answers at `/v2/dashboard/.well-known/workflow/v1/flow`. <Callout type="info"> The local CLI and web UI find a prefixed manifest such as `app/dashboard/.well-known/workflow/v1/manifest.json` on their own; set [`WORKFLOW_MANIFEST_PATH`](/docs/configuration/cli-and-web-ui#workflow_manifest_path) to pick another. Pass the base path to [`workflow health`](/docs/configuration/cli-and-web-ui#--base-path), for example `workflow health --base-path /dashboard`. </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 { withWorkflow } from "workflow/next"; import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin(); export default async function config( phase: string, ctx: { defaultConfig: NextConfig } ): Promise<NextConfig> { let nextConfig: NextConfig | typeof config = {}; for (const configModifier of [withNextIntl, withWorkflow]) { nextConfig = configModifier(nextConfig); if (typeof nextConfig === "function") { nextConfig = await nextConfig(phase, ctx); } } return nextConfig; } ```