You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Workflow SDK configuration is currently spread across ~40 environment variables, typed plugin/createWorld options, and CLI flags — three uncoordinated surfaces with no single source of truth, inconsistent precedence rules, and no documentation page that lists them all. This issue inventories the current state and proposes a standardized, typed workflow.config.ts with a well-defined override hierarchy (defaults → workflow.config.ts → env var → CLI flag), plus a dedicated Configuration docs page.
This is a planning / RFC issue — feedback on the shape welcome before we commit to an API.
Problem: three config surfaces, no source of truth
Today, configuration is set in three different places depending on which knob you want to turn:
Environment variables — the bulk of configuration (WORKFLOW_*, plus VERCEL_*, DATABASE_URL, etc.). Untyped, undiscoverable, undocumented as a set.
Typed plugin / factory options — withWorkflow(nextConfig, { workflows: { lazyDiscovery, local: { port }, sourcemap } }), createVercelWorld(config), createPostgresWorld({ connectionString, pool, maxPoolSize, jobPrefix }), createLocalWorld({ dataDir, port, baseUrl }), and the builder BaseWorkflowConfig (dirs, projectRoot, externalPackages, distDir, sourcemap, …).
Precedence is inconsistent across the codebase. Two examples that contradict each other:
sourcemap: explicit config wins over env var (packages/builders/src/base-builder.ts — "config wins over env var, env var wins over the default").
lazyDiscovery: env var wins over the plugin option (packages/next/src/index.ts — "The WORKFLOW_NEXT_LAZY_DISCOVERY environment variable, if set, takes precedence over the option").
There is no documented, repo-wide rule for which source wins.
Env vars are doing double duty as an internal transport.withWorkflow() translates its typed options intoprocess.env (process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = '1', process.env.WORKFLOW_TARGET_WORLD = 'local', process.env.WORKFLOW_LOCAL_DATA_DIR = '.next/workflow-data'). So env vars are simultaneously (a) a public user-facing config surface and (b) an internal IPC mechanism between the plugin and the runtime/builders. These two roles should be separated.
No discoverability / no docs. There is no page that enumerates the configuration options. The only near-complete list lives implicitly in packages/web/app/server/workflow-server-actions.server.ts (a per-world env allowlist) and scattered package READMEs.
WORKFLOW_WEB_VERCEL_BUILD (toggles Vercel preset in the web build), WORKFLOW_EMBEDDED_DATA_DIR (legacy embedded data dir), plus shared WORKFLOW_MANIFEST_PATH, WORKFLOW_OBSERVABILITY_CWD.
The proposal picks one rule and applies it everywhere.
Proposal: workflow.config.ts
A single, typed, framework-agnostic config file at the project root, loaded once and consumed by core, builders, the active world, and the CLI.
Shape
// workflow.config.tsimport{defineConfig}from'workflow/config';exportdefaultdefineConfig({// ── Which backend ("world") runs your workflows ──────────────────world: {// 'local' | 'vercel' | '@workflow/world-postgres' | custom package/pathtarget: 'local',// Per-world options are typed by the chosen target (discriminated union).local: {dataDir: '.workflow-data',baseUrl: 'http://localhost:3000',queue: {concurrency: 10,maxVisibilityMs: 30_000},},// vercel: { env, team, project, projectName, backendUrl, serverUrl, queue: { maxDelaySeconds }}// postgres: { connectionString, jobPrefix, workerConcurrency, maxPoolSize }//// Secrets (auth tokens, connection strings) are NOT inlined here — they// stay in env vars and are referenced, never committed. See "Secrets".},// ── Runtime behavior (@workflow/core) ────────────────────────────runtime: {replayTimeoutMs: 240_000,// WORKFLOW_REPLAY_TIMEOUT_MSinlineStepTimeoutMs: 120_000,// WORKFLOW_V2_TIMEOUT_MStraceMode: 'linked',// 'linked' | 'continuous'debug: 'workflow:*',// DEBUG filter},// ── Queue ─────────────────────────────────────────────────────────queue: {namespace: 'myapp',// WORKFLOW_QUEUE_NAMESPACE},// ── Build / compiler (@workflow/builders + framework adapters) ────build: {dirs: ['workflows'],// where to discover workflow filesprojectRoot: '.',// resolves #2074externalPackages: [],distDir: '.next',sourcemap: 'inline',// WORKFLOW_SOURCEMAPpublicManifest: false,// WORKFLOW_PUBLIC_MANIFESTnext: {lazyDiscovery: true},},// ── Observability / web UI ────────────────────────────────────────observability: {localUi: false,webPort: 3456,manifestPath: undefined,},});
defineConfig is a pure identity helper that gives full type inference + autocomplete and lets us evolve the schema with deprecation warnings. The file is TS-first (matching the platform's move to vercel.ts), but a workflow.config.js/.mjs and a "workflow" key in package.json should also be accepted.
Rationale: the file is the durable, committed source of truth; env vars override it for per-environment differences (CI, preview vs prod, secrets) without editing code; explicit CLI flags / call-site plugin args win because they are the most local and intentional. This is a deliberate, single decision that replaces today's contradictory sourcemap (config>env) vs lazyDiscovery (env>config) behavior.
Note this flips the current sourcemap behavior (today config beats env). We'd document this as a breaking change for that one knob and keep a one-release shim.
Every option keeps an env override (documented mapping)
The config file does not remove env vars — it gives them a typed home and a documented 1:1 mapping. Every field has a canonical env var:
Config path
Env override
runtime.replayTimeoutMs
WORKFLOW_REPLAY_TIMEOUT_MS
runtime.inlineStepTimeoutMs
WORKFLOW_V2_TIMEOUT_MS
runtime.traceMode
WORKFLOW_TRACE_MODE
world.target
WORKFLOW_TARGET_WORLD
world.local.dataDir
WORKFLOW_LOCAL_DATA_DIR
build.sourcemap
WORKFLOW_SOURCEMAP
build.publicManifest
WORKFLOW_PUBLIC_MANIFEST
queue.namespace
WORKFLOW_QUEUE_NAMESPACE
…
… (full table generated from the schema)
A code-generation step can emit this table into the docs from the schema so it never drifts.
CLI arg overrides
CLI flags map onto the same schema and sit at the top of the precedence chain, e.g. workflow inspect --backend vercel --project … --env preview overrides world.target/world.vercel.* for that invocation only. Flags that are purely presentational (--json, --verbose, --web) stay flags-only.
Secrets
Connection strings and auth tokens (WORKFLOW_VERCEL_AUTH_TOKEN, WORKFLOW_POSTGRES_URL, VERCEL_TOKEN, WORKFLOW_REDIS_URI, …) are never inlined in workflow.config.ts. They remain env-only. The config can reference them, and the schema marks fields as secret so the web UI / inspect know to redact them (ties into #594). The existing WORLD_SENSITIVE_ENV_KEYS set in workflow-server-actions.server.ts becomes derived from the schema.
Loading mechanism
A small workflow/config loader (in @workflow/core or a new @workflow/config) resolves + validates the file once, merges defaults/env/flags per the precedence rule, and exposes a typed resolveConfig().
Replaces the withWorkflow() → process.env bridge. Today the Next plugin stuffs typed options into process.env so downstream builders/runtime can read them. With a real config object, that internal transport goes away — frameworks read the resolved config directly. Env vars go back to being only a user-facing override surface, not an IPC channel.
Framework adapters (next, nitro, sveltekit, astro, nuxt, vite, nest, hono, express, fastify) read the resolved config; their existing plugin options remain supported and map to the same fields (call-site args win, per precedence).
Docs: a dedicated "Configuration" page
New page under docs/content/docs/<version>/ (e.g. foundations/configuration or a top-level configuration):
The full workflow.config.ts reference (every field, type, default).
The generated config-path → env-var mapping table.
Schema + loader + defineConfig in a new @workflow/config (or workflow/config subpath). No behavior change yet — resolveConfig() reads defaults + existing env vars so current setups keep working.
Adopt internally: route core/builders/worlds reads through resolveConfig() instead of direct process.env access. Standardize the precedence rule here. Keep all env vars working.
Wire frameworks: have adapters consume the resolved config; remove the withWorkflow → process.env internal bridge (options still accepted).
CLI: flags feed the same resolver.
Docs: ship the Configuration page + generated mapping table.
Deprecations: warn on the few behavior changes (e.g. sourcemap precedence flip, WORKFLOW_EMIT_SOURCEMAPS_FOR_DEBUGGING → build.sourcemap, WORKFLOW_EMBEDDED_DATA_DIR → world.local.dataDir). One-release shims.
Backwards compatibility is the priority: workflow.config.ts is additive. Projects with zero config and only env vars must continue to work unchanged.
Open questions
Package home: new @workflow/config vs a workflow/config subpath export? (Leaning subpath to avoid an extra install.)
File formats: TS-only, or also .js/.mjs/package.json#workflow? TS needs a loader (jiti/bundle) at build & CLI time.
Precedence for lazyDiscovery/sourcemap: confirm we're OK flipping sourcemap to env-over-config for consistency, or keep per-field exceptions documented.
Summary
Workflow SDK configuration is currently spread across ~40 environment variables, typed plugin/
createWorldoptions, and CLI flags — three uncoordinated surfaces with no single source of truth, inconsistent precedence rules, and no documentation page that lists them all. This issue inventories the current state and proposes a standardized, typedworkflow.config.tswith a well-defined override hierarchy (defaults → workflow.config.ts → env var → CLI flag), plus a dedicated Configuration docs page.This is a planning / RFC issue — feedback on the shape welcome before we commit to an API.
Problem: three config surfaces, no source of truth
Today, configuration is set in three different places depending on which knob you want to turn:
WORKFLOW_*, plusVERCEL_*,DATABASE_URL, etc.). Untyped, undiscoverable, undocumented as a set.withWorkflow(nextConfig, { workflows: { lazyDiscovery, local: { port }, sourcemap } }),createVercelWorld(config),createPostgresWorld({ connectionString, pool, maxPoolSize, jobPrefix }),createLocalWorld({ dataDir, port, baseUrl }), and the builderBaseWorkflowConfig(dirs,projectRoot,externalPackages,distDir,sourcemap, …).--backend,--project,--team,--env,--authToken,--json,--web,--webPort,--localUi,--port, …Concrete pain points
Precedence is inconsistent across the codebase. Two examples that contradict each other:
sourcemap: explicit config wins over env var (packages/builders/src/base-builder.ts— "config wins over env var, env var wins over the default").lazyDiscovery: env var wins over the plugin option (packages/next/src/index.ts— "TheWORKFLOW_NEXT_LAZY_DISCOVERYenvironment variable, if set, takes precedence over the option").Env vars are doing double duty as an internal transport.
withWorkflow()translates its typed options intoprocess.env(process.env.WORKFLOW_NEXT_LAZY_DISCOVERY = '1',process.env.WORKFLOW_TARGET_WORLD = 'local',process.env.WORKFLOW_LOCAL_DATA_DIR = '.next/workflow-data'). So env vars are simultaneously (a) a public user-facing config surface and (b) an internal IPC mechanism between the plugin and the runtime/builders. These two roles should be separated.Config options silently ignored. [nitro] workflow.dirs config option is ignored by VercelBuilder and LocalBuilder #1684 —
workflow.dirsis ignored by the Vercel/Local builders. @workflow/nitro: steps bundle externalizes sibling workspace packages — no way to set projectRoot (breaks Nuxt/Nitro monorepo dev) #2074 — there is no way to setprojectRoot, which breaks Nuxt/Nitro monorepo dev.No discoverability / no docs. There is no page that enumerates the configuration options. The only near-complete list lives implicitly in
packages/web/app/server/workflow-server-actions.server.ts(a per-world env allowlist) and scattered package READMEs.Self-hosting friction & secret exposure. [web] Self-hosted deployment: UI doesn't reflect server-side config + auth token exposure risk #594 — self-hosted UI doesn't reflect server-side config, and auth tokens can be exposed.
Feature requests blocked on config surface. feat - flow control & concurrency / parallelism limits #1206 (flow control / concurrency limits) effectively needs a real place to express per-workflow/per-deployment config.
Current state: full configuration inventory
Core runtime —
@workflow/coreWORKFLOW_REPLAY_TIMEOUT_MS240000(clamped[30000, 780000])WORKFLOW_V2_TIMEOUT_MS120000WORKFLOW_TRACE_MODElinkedlinked|continuousDEBUGdebugpkg). Supports wildcards + negation.workflow:*,-workflow:telemetry:*WORKFLOW_TARGET_WORLDvercelifVERCEL_DEPLOYMENT_IDelselocallocal|vercel|@workflow/world-postgres| custom pkg/pathPlatform-injected, read by core for detection:
VERCEL,VERCEL_URL,VERCEL_DEPLOYMENT_ID,VERCEL_PROJECT_ID.Queue (cross-cutting) —
@workflow/world,@workflow/buildersWORKFLOW_QUEUE_NAMESPACE^[a-z][a-z0-9]*$.__wkf_*)World: local —
@workflow/world-localWORKFLOW_LOCAL_DATA_DIR.workflow-data(.next/workflow-datawhen set by@workflow/next)WORKFLOW_LOCAL_BASE_URLPORT).http://localhost:3000(CLI), else port autodetectWORKFLOW_LOCAL_QUEUE_CONCURRENCYWORKFLOW_LOCAL_QUEUE_MAX_VISIBILITYPORTWorld: Vercel —
@workflow/world-vercelWORKFLOW_VERCEL_ENVWORKFLOW_VERCEL_TEAMWORKFLOW_VERCEL_PROJECTWORKFLOW_VERCEL_PROJECT_NAMEWORKFLOW_VERCEL_AUTH_TOKENWORKFLOW_VERCEL_BACKEND_URLVERCEL_WORKFLOW_SERVER_URLVERCEL_QUEUE_MAX_DELAY_SECONDS82800(23h)VERCEL_PROJECT_IDVERCEL_TOKENVERCEL_DEPLOYMENT_KEYWorld: Postgres —
@workflow/world-postgresWORKFLOW_POSTGRES_URLpostgres://world:world@localhost:5432/worldDATABASE_URLWORKFLOW_POSTGRES_URL).WORKFLOW_POSTGRES_JOB_PREFIXWORKFLOW_POSTGRES_WORKER_CONCURRENCY50WORKFLOW_POSTGRES_MAX_POOL_SIZEpg.Poolmax size (whenpoolomitted).10Community worlds (referenced via
worlds-manifest.json)WORKFLOW_TURSO_DATABASE_URL;WORKFLOW_MONGODB_URI,WORKFLOW_MONGODB_DATABASE_NAME;WORKFLOW_REDIS_URI;JAZZ_API_KEY,JAZZ_WORKER_ACCOUNT,JAZZ_WORKER_SECRET.Build / compiler (cross-framework) —
@workflow/builders+ adaptersWORKFLOW_SOURCEMAPtrue/inline/linked/external/both/false).inline; explicit config > env > defaultWORKFLOW_EMIT_SOURCEMAPS_FOR_DEBUGGING=1WORKFLOW_PUBLIC_MANIFEST=1WORKFLOW_QUEUE_NAMESPACENext.js integration —
@workflow/nextWORKFLOW_NEXT_LAZY_DISCOVERYtrueWORKFLOW_NEXT_PRIVATE_BUILTWORKFLOW_NEXT_DIST_DIR.nextWORKFLOW_PROJECT_ROOTWORKFLOW_SOCKET_INFO_PATH/WORKFLOW_SOCKET_PORT/WORKFLOW_SOCKET_AUTHWATCHPACK_WATCHER_LIMIT20).TURBOPACK,NODE_ENV,NEXT_RUNTIME,PORTCLI —
@workflow/cliEnv:
WORKFLOW_NO_UPDATE_CHECK,WORKFLOW_JSON_MODE(set by--json),WORKFLOW_OBSERVABILITY_CWD,WORKFLOW_LOCAL_UI(set by--localUi),WORKFLOW_MANIFEST_PATH.Flags:
--verbose/-v,--json/-j,--cursor,--backend/-b,--authToken/-a,--project,--team,--env/-e,--web/-w,--webPort,--noBrowser,--localUi,--sort,--port.Web UI —
@workflow/webWORKFLOW_WEB_VERCEL_BUILD(toggles Vercel preset in the web build),WORKFLOW_EMBEDDED_DATA_DIR(legacy embedded data dir), plus sharedWORKFLOW_MANIFEST_PATH,WORKFLOW_OBSERVABILITY_CWD.CI detection (generic, terminal formatting only)
CI,CONTINUOUS_INTEGRATION,BUILD_NUMBER,GITHUB_ACTIONS,GITLAB_CI,CIRCLECI,TRAVIS,JENKINS_URL,BUILDKITE,TF_BUILD.Existing precedence models worth standardizing on
The repo already has good local precedence chains we can generalize:
world-local/src/config.ts):config.baseUrl→WORKFLOW_LOCAL_BASE_URL→config.port→PORT→ autodetect.world-postgres):createWorld({ maxPoolSize })→WORKFLOW_POSTGRES_MAX_POOL_SIZE→pgdefault.builders): explicit config →WORKFLOW_SOURCEMAP→ default.The proposal picks one rule and applies it everywhere.
Proposal:
workflow.config.tsA single, typed, framework-agnostic config file at the project root, loaded once and consumed by core, builders, the active world, and the CLI.
Shape
defineConfigis a pure identity helper that gives full type inference + autocomplete and lets us evolve the schema with deprecation warnings. The file is TS-first (matching the platform's move tovercel.ts), but aworkflow.config.js/.mjsand a"workflow"key inpackage.jsonshould also be accepted.Precedence (single repo-wide rule)
From lowest to highest priority:
Rationale: the file is the durable, committed source of truth; env vars override it for per-environment differences (CI, preview vs prod, secrets) without editing code; explicit CLI flags / call-site plugin args win because they are the most local and intentional. This is a deliberate, single decision that replaces today's contradictory
sourcemap(config>env) vslazyDiscovery(env>config) behavior.Every option keeps an env override (documented mapping)
The config file does not remove env vars — it gives them a typed home and a documented 1:1 mapping. Every field has a canonical env var:
runtime.replayTimeoutMsWORKFLOW_REPLAY_TIMEOUT_MSruntime.inlineStepTimeoutMsWORKFLOW_V2_TIMEOUT_MSruntime.traceModeWORKFLOW_TRACE_MODEworld.targetWORKFLOW_TARGET_WORLDworld.local.dataDirWORKFLOW_LOCAL_DATA_DIRbuild.sourcemapWORKFLOW_SOURCEMAPbuild.publicManifestWORKFLOW_PUBLIC_MANIFESTqueue.namespaceWORKFLOW_QUEUE_NAMESPACEA code-generation step can emit this table into the docs from the schema so it never drifts.
CLI arg overrides
CLI flags map onto the same schema and sit at the top of the precedence chain, e.g.
workflow inspect --backend vercel --project … --env previewoverridesworld.target/world.vercel.*for that invocation only. Flags that are purely presentational (--json,--verbose,--web) stay flags-only.Secrets
Connection strings and auth tokens (
WORKFLOW_VERCEL_AUTH_TOKEN,WORKFLOW_POSTGRES_URL,VERCEL_TOKEN,WORKFLOW_REDIS_URI, …) are never inlined inworkflow.config.ts. They remain env-only. The config can reference them, and the schema marks fields assecretso the web UI /inspectknow to redact them (ties into #594). The existingWORLD_SENSITIVE_ENV_KEYSset inworkflow-server-actions.server.tsbecomes derived from the schema.Loading mechanism
workflow/configloader (in@workflow/coreor a new@workflow/config) resolves + validates the file once, merges defaults/env/flags per the precedence rule, and exposes a typedresolveConfig().withWorkflow()→process.envbridge. Today the Next plugin stuffs typed options intoprocess.envso downstream builders/runtime can read them. With a real config object, that internal transport goes away — frameworks read the resolved config directly. Env vars go back to being only a user-facing override surface, not an IPC channel.next,nitro,sveltekit,astro,nuxt,vite,nest,hono,express,fastify) read the resolved config; their existing plugin options remain supported and map to the same fields (call-site args win, per precedence).Docs: a dedicated "Configuration" page
New page under
docs/content/docs/<version>/(e.g.foundations/configurationor a top-levelconfiguration):workflow.config.tsreference (every field, type, default).workflow.config.ts.Keep the existing agent-discoverable docs sitemap behavior intact (per repo CLAUDE.md).
Rollout plan (non-breaking first)
defineConfigin a new@workflow/config(orworkflow/configsubpath). No behavior change yet —resolveConfig()reads defaults + existing env vars so current setups keep working.resolveConfig()instead of directprocess.envaccess. Standardize the precedence rule here. Keep all env vars working.withWorkflow → process.envinternal bridge (options still accepted).sourcemapprecedence flip,WORKFLOW_EMIT_SOURCEMAPS_FOR_DEBUGGING→build.sourcemap,WORKFLOW_EMBEDDED_DATA_DIR→world.local.dataDir). One-release shims.Backwards compatibility is the priority:
workflow.config.tsis additive. Projects with zero config and only env vars must continue to work unchanged.Open questions
@workflow/configvs aworkflow/configsubpath export? (Leaning subpath to avoid an extra install.).js/.mjs/package.json#workflow? TS needs a loader (jiti/bundle) at build & CLI time.lazyDiscovery/sourcemap: confirm we're OK flippingsourcemapto env-over-config for consistency, or keep per-field exceptions documented.Related issues
workflow.dirsconfig ignored by Vercel/Local builders (config option not honored).projectRoot; breaks Nuxt/Nitro monorepo dev (missing config surface).Inventory compiled from a full
process.envaudit ofpackages/*(excluding tests/e2e/CHANGELOGs).file:linereferences available on request.