KB LabsDocs

Spec Reference

Last updated April 7, 2026


Every field in WorkflowSpec, JobSpec, StepSpec with Zod validation rules.

This page documents the workflow spec schema in full. Every field, every constraint, every default value is taken from platform/kb-labs-workflow/packages/workflow-contracts/src/schemas.ts — the Zod schemas are the single source of truth.

The conceptual picture is in Overview; this page is for copying field names out of.

WorkflowSpec

Top-level workflow definition.

TypeScript
interface WorkflowSpec {
  name: string;                 // required, min length 1
  version: string;              // required, min length 1
  description?: string;
  target?: ExecutionTarget;
  isolation?: IsolationProfile;
  on: WorkflowTrigger;          // required
  inputs?: Record<string, WorkflowInputField>;
  env?: Record<string, string>;
  secrets?: string[];
  jobs: Record<string, JobSpec>; // required, at least one job
  phases?: Record<string, Phase>; // presentation
}

Validation rules

  • name, version — non-empty strings.
  • on — at least one trigger type must be set (see below).
  • jobs — at least one job with a non-empty string key. Zod refine catches empty job records and empty-string keys.
  • Every inner schema (jobs, steps, triggers, inputs) validates recursively.

description

Free-form, used in CLI --help output and Studio listings.

target (workflow-level)

Default ExecutionTarget for all jobs that don't override it:

TypeScript
interface ExecutionTarget {
  environmentId?: string;
  workspaceId?: string;
  namespace?: string;
  workdir?: string;
}

All fields optional; all min(1) when present. See IEnvironmentProvider and IWorkspaceProvider for how these IDs resolve.

isolation (workflow-level)

TypeScript
type IsolationProfile = 'strict' | 'balanced' | 'relaxed';

Default isolation profile for jobs that don't specify one. See Concepts → Execution Model for what the three profiles mean.

env

Non-secret environment variables shared across all jobs. Keys and values are strings. Job-level env overrides workflow-level env for that job.

secrets

List of secret names the workflow needs. Values don't live in the spec — they're resolved at runtime from the platform's secret store. The spec only declares which names the workflow will reference.

JSON
{ "secrets": ["NPM_TOKEN", "GITHUB_TOKEN"] }

phases (presentation)

TypeScript
interface Phase {
  label: string;               // min length 1
  description?: string;
}

Semantic grouping of steps for display. Steps reference phases by key via their phase field. Purely presentational — the engine doesn't interpret them.

JSON
{
  "phases": {
    "build":  { "label": "Build",  "description": "Compile and package" },
    "test":   { "label": "Test",   "description": "Unit and integration tests" },
    "deploy": { "label": "Deploy", "description": "Ship to production" }
  }
}

WorkflowTrigger (on)

TypeScript
interface WorkflowTrigger {
  manual?: boolean;
  push?: boolean;
  webhook?: boolean | {
    secret?: string;
    path?: string;
    headers?: Record<string, string>;
  };
  schedule?: {
    cron: string;              // min length 1
    timezone?: string;         // min length 1
  };
}

Validation

  • At least one trigger must be set. The Zod refine catches empty trigger objects with the message "At least one trigger must be defined".
  • webhook: true is shorthand for "accept webhook, any path, no secret".
  • webhook: { secret } — verify HMAC signature against this secret.
  • webhook: { path } — mount at a specific route instead of the default.
  • webhook: { headers } — require specific headers on the incoming request.
  • schedule.cron — standard cron expression. The engine uses the adapter's cron implementation; 5-field expressions (0 * * * *) are the common case.
  • schedule.timezone — IANA timezone name (Europe/London, America/New_York). Defaults to the engine's timezone when omitted.

WorkflowInputField

TypeScript
interface WorkflowInputField {
  type: 'string' | 'number' | 'boolean';
  description?: string;
  required?: boolean;
  default?: unknown;
}

Three primitive types only. There's no object, array, or custom type — if you need structured input, use webhook triggers with a custom payload schema enforced by the webhook handler.

Keys in the inputs record become addressable as ${{ trigger.payload.<key> }} inside if expressions and string interpolations.

JobSpec

TypeScript
interface JobSpec {
  runsOn: 'local' | 'sandbox';        // required
  target?: ExecutionTarget;
  isolation?: IsolationProfile;
  concurrency?: JobConcurrency;
  steps: StepSpec[];                   // required, at least one step
  artifacts?: JobArtifacts;
  hooks?: JobHooks;
  if?: string;
  timeoutMs?: number;                  // positive int, ≤ 24h
  retries?: RetryPolicy;
  env?: Record<string, string>;
  secrets?: string[];
  needs?: string[];                    // job IDs
  priority?: 'high' | 'normal' | 'low';
}

runsOn

  • 'local' — the job runs in the workflow daemon's own process. Fastest, no isolation. Use for coordination steps that don't touch user code.
  • 'sandbox' — the job runs in an isolated execution backend (worker pool, container, or remote, depending on platform.execution.mode). Use for anything that runs user or plugin code.

See Concepts → Execution Model.

target and isolation (job-level)

Override the workflow-level defaults. Same types as on WorkflowSpec.

concurrency

TypeScript
interface JobConcurrency {
  group: string;              // min length 1, max 256
  cancelInProgress?: boolean;
}

All jobs sharing a concurrency.group are serialized by the scheduler — at most one runs at a time. When cancelInProgress: true, a new job in the same group cancels any currently running job.

Use this for deploys ("only one deploy at a time, cancel the running one"), expensive indexing ("only one reindex at a time"), etc.

steps

An array of at least one StepSpec. See below.

artifacts

TypeScript
interface JobArtifacts {
  produce?: string[];             // artifact names this job creates
  consume?: string[];             // artifact names this job reads
  merge?: {
    strategy: 'append' | 'overwrite' | 'json-merge';
    from: { runId: string; jobId?: string }[];  // min 1
  };
}

merge lets a job pull artifacts from previous runs and combine them. Strategies:

  • 'append' — concatenate list/array content.
  • 'overwrite' — later sources win.
  • 'json-merge' — deep-merge JSON objects.

merge.from must have at least one source. See Artifacts for the full model.

hooks

TypeScript
interface JobHooks {
  pre?: StepSpec[];
  post?: StepSpec[];
  onFailure?: StepSpec[];
  onSuccess?: StepSpec[];
}
  • pre — run before the main steps. If any pre step fails, the main steps are skipped.
  • post — run after the main steps, regardless of success or failure.
  • onFailure — run only if the main steps fail.
  • onSuccess — run only if the main steps succeed.

Each hook is itself a list of steps with the full StepSpec shape. Hook steps can use builtin:shell, builtin:approval, builtin:gate, or any plugin handler.

if (job-level)

A boolean expression. When it evaluates false, the entire job is marked skipped and its dependents (via needs) still fire. See the expression language for syntax.

timeoutMs

Positive integer, maximum 24 hours (enforced by Zod max(1000 * 60 * 60 * 24)). If the job exceeds this, it's terminated and marked failed. No default — omit for no timeout.

retries

TypeScript
interface RetryPolicy {
  max: number;                    // non-negative int
  backoff: 'exp' | 'lin';         // default 'exp'
  initialIntervalMs: number;      // positive int, default 1000
  maxIntervalMs?: number;         // positive int
}
  • max: 0 disables retries.
  • backoff: 'exp' — exponential: initialInterval × 2^attempt, capped by maxIntervalMs.
  • backoff: 'lin' — linear: initialInterval × attempt, capped by maxIntervalMs.
  • initialIntervalMs defaults to 1000 (1 second) if omitted.

env and secrets (job-level)

Same shape as on WorkflowSpec. Merged with workflow-level values — job overrides workflow for overlapping keys.

needs

List of job IDs that must reach a terminal state before this job starts. Forms the dependency DAG. The engine topologically sorts jobs at run creation time — circular dependencies are rejected at validation.

A dependency being skipped does not block the dependent (the dependent still runs). A dependency being failed or cancelled does block the dependent (the dependent is marked skipped with a pending-dependency reason).

priority

TypeScript
type Priority = 'high' | 'normal' | 'low';

Scheduler hint. When multiple jobs are ready to run and the execution backend has limited slots, higher-priority jobs go first. No default — the scheduler treats unset as 'normal'.

StepSpec

TypeScript
interface StepSpec {
  name: string;                     // required, min length 1
  uses?: StepUses;                  // builtin or plugin/workflow ref
  id?: string;                      // [a-zA-Z0-9_-]{1,64}, for ${{ steps.<id>.outputs.* }}
  if?: string;                      // expression
  with?: Record<string, unknown>;   // handler params
  env?: Record<string, string>;
  secrets?: string[];
  timeoutMs?: number;               // positive int, ≤ 24h
  continueOnError?: boolean;
 
  // Presentation
  summary?: string;
  phase?: string;                   // references WorkflowSpec.phases
  progress?: StepProgress;
  artifacts?: Record<string, StepArtifact>;
}

uses

Three forms, enforced by Zod union:

  • 'builtin:shell' — runs a shell command (see Gates & Approvals and Steps for parameters).
  • 'builtin:approval' — pauses the run until a human approves.
  • 'builtin:gate' — evaluates a condition and halts if false.
  • 'plugin:<plugin-id>:<handler-id>' — calls a workflow handler defined in a plugin's workflows.handlers[] section.
  • 'workflow:<workflow-id>' — invokes another workflow as a sub-step. Via parseWorkflowUses(), this resolves to a WorkflowInvocationSpec with mode: 'wait' by default.
  • Any string matching /^(plugin:|workflow:)?[a-zA-Z0-9@/_:+#.-]+$/.

uses is optional: a step with no uses is a placeholder or approval marker depending on context.

id

Addressable identifier used in expressions: ${{ steps.<id>.outputs.<key> }}. Must match /^[a-zA-Z0-9_-]+$/ and be 1–64 characters. Without an id, the step's outputs aren't referenceable from later steps.

if

Boolean expression. When false, the step is skipped. Follow-up steps still run (unlike continueOnError, which is about failure).

with

Key-value bag passed to the handler. Values can be any JSON type. Plugin-defined steps validate with against their own schema; built-ins define their own parameter shape.

JSON
{ "uses": "builtin:shell", "with": { "command": "pnpm test", "timeout": 120000 } }

env, secrets

Step-level overrides of job/workflow-level values.

timeoutMs

Positive integer, ≤ 24 hours. Step-level timeouts are independent of the job timeout — a step timing out doesn't necessarily fail the whole job (that's controlled by continueOnError).

continueOnError

When true, a failed step is recorded as failed but the job continues to the next step. When false (default), a failed step fails the job.

Presentation fields

Purely informational — consumed by Studio, the CLI, and external clients for human output. None of them affect execution.

summary — a short human-readable label for the step. Used instead of name in compact displays.

phase — references a key from WorkflowSpec.phases. Groups this step with others in the same phase in status views.

progress:

TypeScript
interface StepProgress {
  source: string;              // min length 1
  format: string;              // min length 1
}

Binds a live value from step outputs to a display slot. source is a dot-path into step outputs (totalFiles), format is a template string for rendering it. Clients resolve the binding at runtime.

artifacts:

TypeScript
interface StepArtifact {
  type: 'markdown' | 'issues' | 'table' | 'diff' | 'log' | 'json' | 'link';
  source: string;              // min length 1, where to find the artifact
  label: string;               // min length 1, display name
  digest?: string;             // content hash for deduplication
  showInSummary?: boolean;     // include in run summary
}

Declares the artifacts this step produces. Clients render them using the declared type — markdown as prose, issues as a list, table as a grid, and so on. The source field tells the client where to resolve the artifact contents from (a file path, an output key, etc.).

Runtime types

These types are produced by the engine as a spec runs — you generally don't author them, but they're in the contracts package for use in integrations and dashboards.

WorkflowRun

TypeScript
interface WorkflowRun {
  id: string;
  tenantId?: string;
  name: string;
  version: string;
  status: RunState;
  createdAt: string;            // ISO 8601
  queuedAt: string;
  startedAt?: string;
  finishedAt?: string;
  durationMs?: number;
  trigger: RunTrigger;
  env?: Record<string, string>;
  secrets?: string[];
  jobs: JobRun[];
  artifacts?: string[];         // aggregate artifact list
  metadata?: RunMetadata;
  result?: ExecutionResult;
}

JobRun and StepRun

Each has its own lifecycle state, timestamps, attempt counter, outputs, and error record. See the Zod schemas for the exact shape — every field is constrained.

RunState, JobState, StepState

TypeScript
type RunState  = 'queued' | 'running' | 'success' | 'failed' | 'cancelled' | 'skipped' | 'dlq';
type JobState  = 'queued' | 'running' | 'success' | 'failed' | 'cancelled' | 'skipped' | 'interrupted';
type StepState =  RunState | 'waiting_approval';

'dlq' (dead letter queue) is run-only — a run exhausts its retries and gets parked for manual inspection. 'interrupted' is job-only — the job was mid-execution when the engine restarted. 'waiting_approval' is step-only — the step is paused on builtin:approval.

RetryPolicy, ExecutionResult, RunMetadata

See the schemas file for exact fields. All are optional on the live run; the engine fills them in as execution progresses.

Validation at creation time

When you create a run via engine.createRun(input), the spec is validated by WorkflowSpecSchema.safeParse(input.spec). Validation errors are Zod errors with full paths into the spec, so "missing name" surfaces as [{ path: ['name'], message: 'String must contain at least 1 character(s)' }].

Common validation failures:

  • Empty workflow name or version.
  • on with no trigger set.
  • jobs is empty or has an empty-string key.
  • A step missing name.
  • timeoutMs over 24 hours.
  • retries.max is negative.
  • concurrency.group empty or longer than 256 characters.
  • Invalid step id (doesn't match [a-zA-Z0-9_-]).
Spec Reference — KB Labs Docs