From 7b276e8f57c5f159bc1aa7f52edc1411fe02b3d5 Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Fri, 23 Jan 2026 16:19:12 -0800 Subject: [PATCH 1/6] perf(builders): optimize workflow discovery with hybrid SWC approach Apply SWC transforms only to files containing workflow/step directives instead of all files. Uses regex for fast filtering, then SWC where needed. - esbuild follows imports to build dependency graph - regex detects 'use workflow'/'use step' directives - SWC applied only to files with directives (<1% typically) - other files use esbuild's built-in TS handling --- .changeset/optimize-workflow-discovery.md | 5 +++++ packages/builders/src/base-builder.ts | 1 + .../src/discover-entries-esbuild-plugin.ts | 22 +++++++++++++------ 3 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 .changeset/optimize-workflow-discovery.md diff --git a/.changeset/optimize-workflow-discovery.md b/.changeset/optimize-workflow-discovery.md new file mode 100644 index 0000000000..b28fdd1d7f --- /dev/null +++ b/.changeset/optimize-workflow-discovery.md @@ -0,0 +1,5 @@ +--- +"@workflow/builders": patch +--- + +Optimize workflow directive discovery by applying SWC transforms only to files that contain workflow/step directives. Uses regex to quickly filter files, then applies SWC only where needed (<1% of files typically). This maintains correct transformation behavior while reducing overhead for large codebases. diff --git a/packages/builders/src/base-builder.ts b/packages/builders/src/base-builder.ts index 83c41d665d..c6949bd31f 100644 --- a/packages/builders/src/base-builder.ts +++ b/packages/builders/src/base-builder.ts @@ -114,6 +114,7 @@ export abstract class BaseBuilder { }; const discoverStart = Date.now(); + try { await esbuild.build({ treeShaking: true, diff --git a/packages/builders/src/discover-entries-esbuild-plugin.ts b/packages/builders/src/discover-entries-esbuild-plugin.ts index 4c6b25e3c2..b913b27790 100644 --- a/packages/builders/src/discover-entries-esbuild-plugin.ts +++ b/packages/builders/src/discover-entries-esbuild-plugin.ts @@ -89,15 +89,23 @@ export function createDiscoverEntriesPlugin(state: { state.discoveredSteps.push(normalizedPath); } - const { code: transformedCode } = await applySwcTransform( - args.path, - source, - false - ); + // Only apply SWC transform to files with workflow/step directives + if (hasUseWorkflow || hasUseStep) { + const { code: transformedCode } = await applySwcTransform( + args.path, + source, + false + ); + + return { + contents: transformedCode, + loader, + }; + } return { - contents: transformedCode, - loader, + contents: source, + loader: isTypeScript ? 'ts' : loader, }; } catch (_) { // ignore trace errors during discover phase From 6ca843a95aebfd025a418985e1e373b52cde169f Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Fri, 23 Jan 2026 17:59:01 -0800 Subject: [PATCH 2/6] Remove blank line --- packages/builders/src/base-builder.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/builders/src/base-builder.ts b/packages/builders/src/base-builder.ts index c6949bd31f..83c41d665d 100644 --- a/packages/builders/src/base-builder.ts +++ b/packages/builders/src/base-builder.ts @@ -114,7 +114,6 @@ export abstract class BaseBuilder { }; const discoverStart = Date.now(); - try { await esbuild.build({ treeShaking: true, From ad923474119260ae090d9d95805d5e1a4a0e4a89 Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Fri, 23 Jan 2026 18:39:51 -0800 Subject: [PATCH 3/6] feat(next): add configurable `dirs` option to withWorkflow Allow users to specify which directories to scan for workflow directives, helping reduce memory usage and build times in large Next.js applications. - Add `dirs` option that overrides the default directories - Document all configuration options in API reference - Add troubleshooting guide for OOM errors during build --- .changeset/configurable-dirs-next.md | 5 ++ .../workflow-next/with-workflow.mdx | 70 ++++++++++++++++++- packages/next/src/index.ts | 17 ++++- 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 .changeset/configurable-dirs-next.md diff --git a/.changeset/configurable-dirs-next.md b/.changeset/configurable-dirs-next.md new file mode 100644 index 0000000000..64998ff3f4 --- /dev/null +++ b/.changeset/configurable-dirs-next.md @@ -0,0 +1,5 @@ +--- +"@workflow/next": minor +--- + +Add configurable `dirs` option to `withWorkflow()` to specify which directories to scan for workflow directives. This helps reduce memory usage and build times in large Next.js applications by allowing users to limit scanning to only directories containing workflows. diff --git a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx index 5c64284577..8b82fc1515 100644 --- a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx +++ b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx @@ -48,4 +48,72 @@ export default async function config( } return nextConfig; } -``` +``` + +## Configuration + +The second argument to `withWorkflow` accepts the following options: + +### `dirs` + +Directories to scan for workflows and steps. If provided, this completely overrides the defaults. + +- **Type:** `string[]` +- **Default:** `['pages', 'app', 'src/pages', 'src/app']` + +```typescript title="next.config.ts" lineNumbers +import { withWorkflow } from "workflow/next"; +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default withWorkflow(nextConfig, { + dirs: ['workflows', 'src/workflows'], // [!code highlight] +}); +``` + +### `workflows.local.port` + +Port for the local workflow server during development. + +- **Type:** `number` +- **Default:** Uses the `PORT` environment variable + +### `workflows.local.dataDir` + +Directory for storing local workflow data during development. + +- **Type:** `string` +- **Default:** `'.next/workflow-data'` + +## Troubleshooting + +### Out of Memory (OOM) during build + +When using `withWorkflow()` in a large Next.js application, the "Discovering workflow directives" phase may consume excessive memory, causing builds to fail with OOM errors on standard build machines (e.g., 16 GB RAM). + +**Solution:** Use the `dirs` option to limit the directories scanned for workflow directives. By default, `withWorkflow` scans `['pages', 'app', 'src/pages', 'src/app']`, which in large applications can include thousands of files. + +If your workflows are located in a specific directory, configure `dirs` to only scan that directory: + +```typescript title="next.config.ts" lineNumbers +import { withWorkflow } from "workflow/next"; +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default withWorkflow(nextConfig, { + // Only scan the workflows directory instead of the entire app + dirs: ['workflows'], // [!code highlight] +}); +``` + +If you need workflows in both app routes and a dedicated directory: + +```typescript title="next.config.ts" lineNumbers +export default withWorkflow(nextConfig, { + dirs: ['app/api', 'workflows'], // [!code highlight] +}); +``` + +This significantly reduces memory usage and build times by avoiding scanning unrelated application code. diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index 0fc8e4d846..124eca7f35 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -2,6 +2,12 @@ import type { NextConfig } from 'next'; import semver from 'semver'; import { getNextBuilder } from './builder.js'; +/** + * Default directories to scan for workflows and steps. + * These are the standard Next.js app directories. + */ +const DEFAULT_WORKFLOW_DIRS = ['pages', 'app', 'src/pages', 'src/app']; + export function withWorkflow( nextConfigOrFn: | NextConfig @@ -11,6 +17,7 @@ export function withWorkflow( ) => Promise), { workflows, + dirs, }: { workflows?: { local?: { @@ -18,6 +25,13 @@ export function withWorkflow( dataDir?: string; }; }; + /** + * Directories to scan for workflows and steps. + * If provided, this completely overrides the defaults. + * + * @default ['pages', 'app', 'src/pages', 'src/app'] + */ + dirs?: string[]; } = {} ) { if (!process.env.VERCEL_DEPLOYMENT_ID) { @@ -121,8 +135,7 @@ export function withWorkflow( const NextBuilder = await getNextBuilder(); const workflowBuilder = new NextBuilder({ watch: shouldWatch, - // discover workflows from pages/app entries - dirs: ['pages', 'app', 'src/pages', 'src/app'], + dirs: dirs ?? DEFAULT_WORKFLOW_DIRS, workingDir: process.cwd(), buildTarget: 'next', workflowsBundlePath: '', // not used in base From 997e41b0d949386af5a1b7d73bd8a8b932377e54 Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Fri, 23 Jan 2026 18:53:39 -0800 Subject: [PATCH 4/6] fix(docs): add missing imports in withWorkflow code example Add missing import statements and nextConfig declaration to the troubleshooting code example to fix docs typecheck. --- .../docs/api-reference/workflow-next/with-workflow.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx index 8b82fc1515..85cdf692af 100644 --- a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx +++ b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx @@ -111,6 +111,11 @@ export default withWorkflow(nextConfig, { If you need workflows in both app routes and a dedicated directory: ```typescript title="next.config.ts" lineNumbers +import { withWorkflow } from "workflow/next"; +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + export default withWorkflow(nextConfig, { dirs: ['app/api', 'workflows'], // [!code highlight] }); From 385a492f5690936b0ec9cf2460732c89f2866d55 Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Sat, 24 Jan 2026 14:03:20 -0800 Subject: [PATCH 5/6] docs: move OOM troubleshooting to getting-started/next Move the OOM troubleshooting section from the API reference to the existing troubleshooting section in docs/getting-started/next.mdx for better discoverability. --- .../workflow-next/with-workflow.mdx | 37 ------------------- docs/content/docs/getting-started/next.mdx | 35 ++++++++++++++++++ 2 files changed, 35 insertions(+), 37 deletions(-) diff --git a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx index 85cdf692af..cf98c6b2f4 100644 --- a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx +++ b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx @@ -85,40 +85,3 @@ Directory for storing local workflow data during development. - **Type:** `string` - **Default:** `'.next/workflow-data'` - -## Troubleshooting - -### Out of Memory (OOM) during build - -When using `withWorkflow()` in a large Next.js application, the "Discovering workflow directives" phase may consume excessive memory, causing builds to fail with OOM errors on standard build machines (e.g., 16 GB RAM). - -**Solution:** Use the `dirs` option to limit the directories scanned for workflow directives. By default, `withWorkflow` scans `['pages', 'app', 'src/pages', 'src/app']`, which in large applications can include thousands of files. - -If your workflows are located in a specific directory, configure `dirs` to only scan that directory: - -```typescript title="next.config.ts" lineNumbers -import { withWorkflow } from "workflow/next"; -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = {}; - -export default withWorkflow(nextConfig, { - // Only scan the workflows directory instead of the entire app - dirs: ['workflows'], // [!code highlight] -}); -``` - -If you need workflows in both app routes and a dedicated directory: - -```typescript title="next.config.ts" lineNumbers -import { withWorkflow } from "workflow/next"; -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = {}; - -export default withWorkflow(nextConfig, { - dirs: ['app/api', 'workflows'], // [!code highlight] -}); -``` - -This significantly reduces memory usage and build times by avoiding scanning unrelated application code. diff --git a/docs/content/docs/getting-started/next.mdx b/docs/content/docs/getting-started/next.mdx index 4fc9213043..b1debdd656 100644 --- a/docs/content/docs/getting-started/next.mdx +++ b/docs/content/docs/getting-started/next.mdx @@ -257,6 +257,41 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b ## Troubleshooting +### Out of Memory (OOM) during build + +When using `withWorkflow()` in a large Next.js application, the "Discovering workflow directives" phase may consume excessive memory, causing builds to fail with OOM errors on standard build machines (e.g., 16 GB RAM). + +**Solution:** Use the `dirs` option to limit the directories scanned for workflow directives. By default, `withWorkflow` scans `['pages', 'app', 'src/pages', 'src/app']`, which in large applications can include thousands of files. + +If your workflows are located in a specific directory, configure `dirs` to only scan that directory: + +```typescript title="next.config.ts" lineNumbers +import { withWorkflow } from "workflow/next"; +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default withWorkflow(nextConfig, { + // Only scan the workflows directory instead of the entire app + dirs: ['workflows'], // [!code highlight] +}); +``` + +If you need workflows in both app routes and a dedicated directory: + +```typescript title="next.config.ts" lineNumbers +import { withWorkflow } from "workflow/next"; +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default withWorkflow(nextConfig, { + dirs: ['app/api', 'workflows'], // [!code highlight] +}); +``` + +This significantly reduces memory usage and build times by avoiding scanning unrelated application code. + ### Next.js 16.1+ compatibility If you see this error when upgrading to Next.js 16.1 or later: From 1da647bd5f6ad5c5252040054aefc52744ef94c5 Mon Sep 17 00:00:00 2001 From: Karthik Kalyanaraman Date: Tue, 27 Jan 2026 11:01:41 -0800 Subject: [PATCH 6/6] move dirs inside workflow config --- .changeset/configurable-dirs-next.md | 5 ----- .changeset/optimize-workflow-discovery.md | 5 ----- .changeset/polite-animals-write.md | 5 +++++ .../workflow-next/with-workflow.mdx | 6 +++-- docs/content/docs/getting-started/next.mdx | 12 ++++++---- .../src/discover-entries-esbuild-plugin.ts | 22 ++++++------------- packages/next/src/index.ts | 17 +++++++------- 7 files changed, 32 insertions(+), 40 deletions(-) delete mode 100644 .changeset/configurable-dirs-next.md delete mode 100644 .changeset/optimize-workflow-discovery.md create mode 100644 .changeset/polite-animals-write.md diff --git a/.changeset/configurable-dirs-next.md b/.changeset/configurable-dirs-next.md deleted file mode 100644 index 64998ff3f4..0000000000 --- a/.changeset/configurable-dirs-next.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@workflow/next": minor ---- - -Add configurable `dirs` option to `withWorkflow()` to specify which directories to scan for workflow directives. This helps reduce memory usage and build times in large Next.js applications by allowing users to limit scanning to only directories containing workflows. diff --git a/.changeset/optimize-workflow-discovery.md b/.changeset/optimize-workflow-discovery.md deleted file mode 100644 index b28fdd1d7f..0000000000 --- a/.changeset/optimize-workflow-discovery.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@workflow/builders": patch ---- - -Optimize workflow directive discovery by applying SWC transforms only to files that contain workflow/step directives. Uses regex to quickly filter files, then applies SWC only where needed (<1% of files typically). This maintains correct transformation behavior while reducing overhead for large codebases. diff --git a/.changeset/polite-animals-write.md b/.changeset/polite-animals-write.md new file mode 100644 index 0000000000..f35c79ae44 --- /dev/null +++ b/.changeset/polite-animals-write.md @@ -0,0 +1,5 @@ +--- +"workflow": patch +--- + +Expose `dirs` option in `workflows` config object in `withWorkflow()` diff --git a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx index cf98c6b2f4..384bb7e291 100644 --- a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx +++ b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx @@ -54,7 +54,7 @@ export default async function config( The second argument to `withWorkflow` accepts the following options: -### `dirs` +### `workflows.dirs` Directories to scan for workflows and steps. If provided, this completely overrides the defaults. @@ -68,7 +68,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = {}; export default withWorkflow(nextConfig, { - dirs: ['workflows', 'src/workflows'], // [!code highlight] + workflows: { + dirs: ['workflows', 'src/workflows'], // [!code highlight] + }, }); ``` diff --git a/docs/content/docs/getting-started/next.mdx b/docs/content/docs/getting-started/next.mdx index b1debdd656..bf7052c639 100644 --- a/docs/content/docs/getting-started/next.mdx +++ b/docs/content/docs/getting-started/next.mdx @@ -261,7 +261,7 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b When using `withWorkflow()` in a large Next.js application, the "Discovering workflow directives" phase may consume excessive memory, causing builds to fail with OOM errors on standard build machines (e.g., 16 GB RAM). -**Solution:** Use the `dirs` option to limit the directories scanned for workflow directives. By default, `withWorkflow` scans `['pages', 'app', 'src/pages', 'src/app']`, which in large applications can include thousands of files. +**Solution:** Use the `workflows.dirs` option to limit the directories scanned for workflow directives. By default, `withWorkflow` scans `['pages', 'app', 'src/pages', 'src/app']`, which in large applications can include thousands of files. If your workflows are located in a specific directory, configure `dirs` to only scan that directory: @@ -272,8 +272,10 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = {}; export default withWorkflow(nextConfig, { - // Only scan the workflows directory instead of the entire app - dirs: ['workflows'], // [!code highlight] + workflows: { + // Only scan the workflows directory instead of the entire app + dirs: ['workflows'], // [!code highlight] + }, }); ``` @@ -286,7 +288,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = {}; export default withWorkflow(nextConfig, { - dirs: ['app/api', 'workflows'], // [!code highlight] + workflows: { + dirs: ['app/api', 'workflows'], // [!code highlight] + }, }); ``` diff --git a/packages/builders/src/discover-entries-esbuild-plugin.ts b/packages/builders/src/discover-entries-esbuild-plugin.ts index b913b27790..4c6b25e3c2 100644 --- a/packages/builders/src/discover-entries-esbuild-plugin.ts +++ b/packages/builders/src/discover-entries-esbuild-plugin.ts @@ -89,23 +89,15 @@ export function createDiscoverEntriesPlugin(state: { state.discoveredSteps.push(normalizedPath); } - // Only apply SWC transform to files with workflow/step directives - if (hasUseWorkflow || hasUseStep) { - const { code: transformedCode } = await applySwcTransform( - args.path, - source, - false - ); - - return { - contents: transformedCode, - loader, - }; - } + const { code: transformedCode } = await applySwcTransform( + args.path, + source, + false + ); return { - contents: source, - loader: isTypeScript ? 'ts' : loader, + contents: transformedCode, + loader, }; } catch (_) { // ignore trace errors during discover phase diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index 124eca7f35..b70951436f 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -17,21 +17,20 @@ export function withWorkflow( ) => Promise), { workflows, - dirs, }: { workflows?: { local?: { port?: number; dataDir?: string; }; + /** + * Directories to scan for workflows and steps. + * If provided, this completely overrides the defaults. + * + * @default ['pages', 'app', 'src/pages', 'src/app'] + */ + dirs?: string[]; }; - /** - * Directories to scan for workflows and steps. - * If provided, this completely overrides the defaults. - * - * @default ['pages', 'app', 'src/pages', 'src/app'] - */ - dirs?: string[]; } = {} ) { if (!process.env.VERCEL_DEPLOYMENT_ID) { @@ -135,7 +134,7 @@ export function withWorkflow( const NextBuilder = await getNextBuilder(); const workflowBuilder = new NextBuilder({ watch: shouldWatch, - dirs: dirs ?? DEFAULT_WORKFLOW_DIRS, + dirs: workflows?.dirs ?? DEFAULT_WORKFLOW_DIRS, workingDir: process.cwd(), buildTarget: 'next', workflowsBundlePath: '', // not used in base