workflow-studio
Config resolution, framework adapters, and CLI internals
Overview
This document covers the internal architecture of the workflow-studio package for contributors working on the codebase.
Package Structure
packages/workflow-studio/
├── src/
│ ├── adapters/
│ │ ├── common.ts # Config resolution and proxy utilities
│ │ ├── common.test.ts # Config resolution tests
│ │ └── types.ts # TypeScript interfaces
│ ├── topology/
│ │ ├── execution-adapters.ts # Local vs Remote execution
│ │ ├── queue-adapters.ts # Queue backend factories
│ │ ├── stream-adapters.ts # Stream backend factories
│ │ └── types.ts # Topology type definitions
│ ├── config.ts # defineWorkflowStudioConfig helper
│ ├── next.ts # Next.js framework adapter
│ ├── nitro.ts # Nitro framework adapter
│ ├── sveltekit.ts # SvelteKit framework adapter
│ ├── cli.ts # CLI implementation
│ ├── cli.test.ts # CLI tests
│ ├── ownership.ts # Diagnostics handler
│ └── index.ts # Public exports
├── world-remote.cjs # Remote world implementation
├── remote-ownership-state.cjs # Global state for ownership tracking
└── package.jsonConfiguration Resolution
Configuration flows through multiple layers with precedence:
- Adapter options (highest priority)
- workflow.config. file*
- Environment variables (lowest priority)
Resolution Flow
// src/adapters/common.ts
export async function resolveWorkflowStudioAdapterConfig(
config?: WorkflowStudioAdapterConfig,
options: ResolveAdapterConfigOptions = {}
): Promise<ResolvedWorkflowStudioAdapterConfig> {
const fileConfig = await loadWorkflowStudioConfig(cwd);
const remoteBaseUrl = normalizeBaseUrl(
config?.remote?.baseUrl ?? // 1. Adapter options
remoteFromFile?.baseUrl ?? // 2. Config file
env.WORKFLOW_COMPUTE_BASE_URL // 3. Environment
);
// Similar precedence for execution mode, API key, etc.
}Config File Discovery
The system searches for config files in this order:
workflow.config.mjsworkflow.config.jsworkflow.config.cjsworkflow.config.tsworkflow.config.mtsworkflow.config.cts
export async function loadWorkflowStudioConfig(
cwd = process.cwd()
): Promise<WorkflowStudioConfig | undefined> {
const candidates = [
'workflow.config.mjs',
'workflow.config.js',
'workflow.config.cjs',
'workflow.config.ts',
'workflow.config.mts',
'workflow.config.cts',
].map((name) => resolve(cwd, name));
// Try each until one succeeds
}Framework Adapters
Each framework adapter follows the same pattern:
- Resolve configuration
- Get server env defaults
- Merge into framework-specific config location
Next.js Adapter
// src/next.ts
export async function withWorkflowStudioNext<T extends NextConfigLike>(
nextConfig: T,
options: WorkflowStudioNextOptions = {}
): Promise<T> {
const resolved = await resolveWorkflowStudioAdapterConfig(options, options);
const serverDefaults = getServerEnvDefaults(resolved);
return {
...nextConfig,
workflowStudio: { // NOT nextConfig.env (client-exposed)
...(nextConfig.workflowStudio ?? {}),
...serverDefaults,
},
};
}Key security decision: configuration goes under workflowStudio key, not env, to avoid bundling sensitive values to the client.
Nitro Adapter
// src/nitro.ts
return {
...nitroConfig,
runtimeConfig: {
...(nitroConfig.runtimeConfig ?? {}),
workflowStudio: {
...((nitroConfig.runtimeConfig?.workflowStudio as Record<string, unknown>) ?? {}),
...serverDefaults,
},
},
};SvelteKit Adapter
// src/sveltekit.ts
return {
...config,
kit: {
...(config.kit ?? {}),
env: {
...(config.kit?.env ?? {}),
workflowStudio: {
...((config.kit?.env as Record<string, unknown> | undefined)?.workflowStudio ?? {}),
...serverDefaults,
},
},
},
};Proxy Helpers
The proxyTrigger function validates configuration before forwarding:
// src/adapters/common.ts
export async function proxyTrigger(
input: TriggerProxyInput,
context: ProxyContext = {}
): Promise<ComputeTriggerResponse> {
const resolved = await resolveWorkflowStudioAdapterConfig(context.config, {
cwd: context.cwd,
env,
});
if (!resolved.remote.baseUrl) {
throw new Error('Remote compute is not configured...');
}
if (resolved.mode !== 'proxy-trigger') {
throw new Error(`proxyTrigger requires mode=proxy-trigger...`);
}
const client = createClient(resolved, env);
return await client.trigger(input.payload, { idempotencyKey });
}Strict Remote Guard
Enforces WORKFLOW_TARGET_WORLD in production:
// src/adapters/common.ts
function ensureRemoteWorldTargetGuard(
executionMode: 'local' | 'remote',
remoteWorldTarget: string,
env: NodeJS.ProcessEnv
): void {
if (executionMode !== 'remote' || !shouldApplyStrictRemoteGuard(env)) {
return;
}
const currentTarget = env.WORKFLOW_TARGET_WORLD;
if (currentTarget !== remoteWorldTarget) {
throw new Error(
`Remote execution guard failed: set WORKFLOW_TARGET_WORLD=${remoteWorldTarget}...`
);
}
}CLI Architecture
The CLI uses a simple command dispatcher:
// src/cli.ts
export async function runWorkflowStudioCli(argv: string[]): Promise<void> {
const [scope, command, ...rest] = argv;
if (scope !== 'compute' || !command) {
console.log(usage());
return;
}
switch (command) {
case 'build':
await cmdComputeBuild(rest);
return;
case 'deploy':
await cmdComputeDeploy(rest);
return;
// ...
}
}Build Command
- Reads workflow bundles from
.well-known/workflow/v1/ - Packages into
.workflow-studio/dist/artifact.tgz - Generates manifest at
.workflow-studio/tmp/<deploymentId>/manifest.json
Validate Ownership Command
Performs multiple checks with different severity levels:
-
Hard failures (exit code 1 in strict mode):
- Worker unreachable
- No active deployment
- Target world mismatch
- Diagnostics status = fail
-
Soft warnings (exit code 2):
- Diagnostics endpoint unavailable
- Soft ownership signals missing
Remote World Implementation
The world-remote.cjs file provides a world implementation that forwards all operations to the remote worker:
// world-remote.cjs
function createWorld() {
return {
async getDeploymentId() {
const response = await requestJson('/v1/world/deployment-id', {
method: 'GET',
});
return response.deploymentId;
},
async queue(queueName, message, opts) {
return await requestJson('/v1/queue/publish', {
method: 'POST',
body: JSON.stringify({ queueName, message, opts }),
});
},
createQueueHandler() {
// Returns 409 guardrail response
},
// ... runs, steps, events, hooks, streams
};
}Key features:
- Adds
x-workflow-lane: world-proxyheader to all requests - Adds
x-workflow-correlation-idfor tracing - Adds
x-workflow-run-idwhen available - Date revival for JSON responses
- Queue handler returns 409 guardrail in remote mode