API Reference

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.json

Configuration Resolution

Configuration flows through multiple layers with precedence:

  1. Adapter options (highest priority)
  2. workflow.config. file*
  3. 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:

  1. workflow.config.mjs
  2. workflow.config.js
  3. workflow.config.cjs
  4. workflow.config.ts
  5. workflow.config.mts
  6. workflow.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:

  1. Resolve configuration
  2. Get server env defaults
  3. 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

  1. Reads workflow bundles from .well-known/workflow/v1/
  2. Packages into .workflow-studio/dist/artifact.tgz
  3. 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-proxy header to all requests
  • Adds x-workflow-correlation-id for tracing
  • Adds x-workflow-run-id when available
  • Date revival for JSON responses
  • Queue handler returns 409 guardrail in remote mode