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.
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. Zodrefinecatches 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:
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)
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.
{ "secrets": ["NPM_TOKEN", "GITHUB_TOKEN"] }phases (presentation)
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.
{
"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)
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
refinecatches empty trigger objects with the message "At least one trigger must be defined". webhook: trueis 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
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
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 onplatform.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
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
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
interface JobHooks {
pre?: StepSpec[];
post?: StepSpec[];
onFailure?: StepSpec[];
onSuccess?: StepSpec[];
}pre— run before the mainsteps. If anyprestep fails, the main steps are skipped.post— run after the mainsteps, regardless of success or failure.onFailure— run only if the mainstepsfail.onSuccess— run only if the mainstepssucceed.
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
interface RetryPolicy {
max: number; // non-negative int
backoff: 'exp' | 'lin'; // default 'exp'
initialIntervalMs: number; // positive int, default 1000
maxIntervalMs?: number; // positive int
}max: 0disables retries.backoff: 'exp'— exponential:initialInterval × 2^attempt, capped bymaxIntervalMs.backoff: 'lin'— linear:initialInterval × attempt, capped bymaxIntervalMs.initialIntervalMsdefaults 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
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
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'sworkflows.handlers[]section.'workflow:<workflow-id>'— invokes another workflow as a sub-step. ViaparseWorkflowUses(), this resolves to aWorkflowInvocationSpecwithmode: '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.
{ "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:
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:
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
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
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
nameorversion. onwith no trigger set.jobsis empty or has an empty-string key.- A step missing
name. timeoutMsover 24 hours.retries.maxis negative.concurrency.groupempty or longer than 256 characters.- Invalid step
id(doesn't match[a-zA-Z0-9_-]).
What to read next
- Overview — conceptual picture with examples.
- Triggers, Jobs, Steps, Artifacts — per-section deep dives.
- Gates & Approvals, Retries & Error Handling, Patterns — recipes.