Patterns
Last updated April 7, 2026
Copy-paste recipes for common workflow shapes.
End-to-end examples you can start from. Every pattern on this page is a complete workflow spec — change the names and handlers to fit your case.
If you're looking for reference material (every field, every validation rule), go to Spec Reference. This page is for building things.
CI pipeline: build, test, lint in parallel
Classic fan-out. build runs first, then test and lint run in parallel, then the run is done.
{
"name": "ci",
"version": "1",
"on": { "push": true, "manual": true },
"env": { "CI": "true" },
"jobs": {
"build": {
"runsOn": "sandbox",
"isolation": "balanced",
"timeoutMs": 600000,
"steps": [
{ "name": "Install", "uses": "builtin:shell", "with": { "command": "pnpm install --frozen-lockfile" } },
{ "name": "Build", "uses": "builtin:shell", "with": { "command": "pnpm build" } }
],
"artifacts": { "produce": ["dist"] }
},
"test": {
"runsOn": "sandbox",
"needs": ["build"],
"artifacts": { "consume": ["dist"] },
"retries": { "max": 2, "backoff": "exp", "initialIntervalMs": 5000 },
"steps": [
{ "name": "Test", "uses": "builtin:shell", "with": { "command": "pnpm test" } }
]
},
"lint": {
"runsOn": "sandbox",
"needs": ["build"],
"artifacts": { "consume": ["dist"] },
"steps": [
{ "name": "Lint", "uses": "builtin:shell", "with": { "command": "pnpm lint" } }
]
}
}
}What makes this work:
testandlintbothneeds: ['build'], so they're ready at the same time and the scheduler runs them in parallel.- Retries are scoped to
testonly, because lint failures are not transient. produce/consumeguarantees both downstream jobs see the build output, independent ofneeds.
CI/CD: build → test → manual approval → deploy
Adds a human gate before production deploy. Only runs on manual trigger by authorized actors.
{
"name": "ci-cd",
"version": "1",
"on": { "push": true, "manual": true },
"jobs": {
"build": { "runsOn": "sandbox", "steps": [...], "artifacts": { "produce": ["dist"] } },
"test": { "runsOn": "sandbox", "needs": ["build"], "artifacts": { "consume": ["dist"] }, "steps": [...] },
"deploy-staging": {
"runsOn": "sandbox",
"needs": ["test"],
"secrets": ["STAGING_DEPLOY_KEY"],
"steps": [
{ "name": "Deploy to staging", "uses": "plugin:release:deploy", "with": { "env": "staging" } }
]
},
"approve-prod": {
"runsOn": "local",
"needs": ["deploy-staging"],
"if": "${{ trigger.type == 'manual' && contains(trigger.actor, '@company.com') }}",
"steps": [
{
"name": "Approve prod deploy",
"id": "approve",
"uses": "builtin:approval",
"with": {
"title": "Deploy to production?",
"context": {
"version": "${{ trigger.payload.version }}",
"staging": "https://staging.example.com"
},
"instructions": "Verify the staging deploy before approving.",
"timeoutMs": 3600000
}
}
]
},
"deploy-prod": {
"runsOn": "sandbox",
"needs": ["approve-prod"],
"concurrency": { "group": "prod-deploy" },
"secrets": ["PROD_DEPLOY_KEY"],
"steps": [
{ "name": "Deploy to production", "uses": "plugin:release:deploy", "with": { "env": "production" } }
],
"hooks": {
"onSuccess": [
{ "name": "Tag release", "uses": "plugin:release:tag" },
{ "name": "Notify success", "uses": "plugin:notify:slack", "with": { "message": "Prod deploy complete" } }
],
"onFailure": [
{ "name": "Rollback", "uses": "plugin:release:rollback" },
{ "name": "Notify failure", "uses": "plugin:notify:slack", "with": { "message": "Prod deploy FAILED" } }
]
}
}
}
}What makes this work:
approve-prodruns onlocal(no sandbox needed for a pause).deploy-prod.concurrencyguarantees only one prod deploy at a time, across all runs.- Success/failure hooks decouple notifications and rollback from the main deploy step.
- Approval has a 1-hour timeout so an abandoned request doesn't block other runs forever.
Scheduled maintenance
Runs nightly, does cleanup, alerts only on failure.
{
"name": "nightly-maintenance",
"version": "1",
"on": {
"schedule": { "cron": "0 3 * * *", "timezone": "Europe/London" },
"manual": true
},
"jobs": {
"cleanup": {
"runsOn": "sandbox",
"timeoutMs": 1800000,
"steps": [
{ "name": "Rotate logs", "uses": "builtin:shell", "with": { "command": "./scripts/rotate-logs.sh" } },
{ "name": "GC workspaces", "uses": "plugin:devkit:gc-workspaces" },
{ "name": "Vacuum db", "uses": "plugin:admin:vacuum-db" }
],
"hooks": {
"onFailure": [
{ "name": "Alert on-call", "uses": "plugin:notify:pagerduty", "with": { "severity": "warning" } }
]
}
}
}
}Why manual: true too: when the scheduled run fails, you want a "re-run now" button — not "wait until 3am tomorrow".
Loop: apply → review → gate
Automated code change with bounded iteration. See Gates & Approvals for the full version; here's the compact pattern.
{
"name": "auto-fix-loop",
"version": "1",
"on": { "manual": true },
"jobs": {
"fix-loop": {
"runsOn": "sandbox",
"steps": [
{
"name": "Apply fixes",
"id": "apply",
"uses": "plugin:auto-fix:run",
"with": { "retry": "${{ trigger.payload.retry == true }}" }
},
{
"name": "Review",
"id": "review",
"uses": "plugin:ai-review:run"
},
{
"name": "Gate",
"uses": "builtin:gate",
"with": {
"decision": "steps.review.outputs.verdict",
"routes": {
"approved": "continue",
"rejected": "fail",
"needs-fix": { "restartFrom": "apply", "context": { "retry": true } }
},
"default": "fail",
"maxIterations": 5
}
},
{
"name": "Commit",
"uses": "plugin:commit:commit"
}
]
}
}
}Multi-repo parallel work
Fan out the same job across several repos, then aggregate.
{
"name": "multi-repo-audit",
"version": "1",
"on": { "manual": true, "schedule": { "cron": "0 4 * * 1" } },
"jobs": {
"audit-core": {
"runsOn": "sandbox",
"target": { "workspaceId": "kb-labs-core" },
"steps": [ { "name": "Audit", "uses": "plugin:security:audit" } ],
"artifacts": {
"produce": ["audit-report"]
}
},
"audit-cli": {
"runsOn": "sandbox",
"target": { "workspaceId": "kb-labs-cli" },
"steps": [ { "name": "Audit", "uses": "plugin:security:audit" } ],
"artifacts": { "produce": ["audit-report"] }
},
"audit-workflow": {
"runsOn": "sandbox",
"target": { "workspaceId": "kb-labs-workflow" },
"steps": [ { "name": "Audit", "uses": "plugin:security:audit" } ],
"artifacts": { "produce": ["audit-report"] }
},
"aggregate": {
"runsOn": "local",
"needs": ["audit-core", "audit-cli", "audit-workflow"],
"artifacts": { "consume": ["audit-report"] },
"steps": [
{ "name": "Combine and render", "uses": "plugin:security:aggregate-reports" }
]
}
}
}Trade-off: you're copy-pasting job blocks. A future matrix mechanism would let you declare this once and expand at run time, but today you write the jobs explicitly. If you're doing this a lot, generate the spec programmatically instead of hand-writing it.
Webhook-driven release
GitHub webhook triggers a release flow. Uses the webhook payload to pick the version.
{
"name": "gh-release",
"version": "1",
"on": {
"webhook": {
"secret": "${{ secrets.GITHUB_WEBHOOK_SECRET }}",
"path": "/v1/webhooks/github/release",
"headers": { "X-GitHub-Event": "release" }
},
"manual": true
},
"jobs": {
"publish": {
"if": "${{ trigger.payload.action == 'published' }}",
"runsOn": "sandbox",
"secrets": ["NPM_TOKEN"],
"steps": [
{
"name": "Checkout tag",
"uses": "builtin:shell",
"with": { "command": "git checkout ${{ trigger.payload.release.tag_name }}" }
},
{ "name": "Build", "uses": "builtin:shell", "with": { "command": "pnpm install && pnpm build" } },
{ "name": "Publish", "uses": "builtin:shell", "with": { "command": "pnpm publish" } }
]
}
}
}Notes:
- The
headersfilter makes GitHub the only valid sender (combined with the signature check fromsecret). - The
ifon the job filters toaction: 'published'— GitHub fires thereleasewebhook for other actions (created, edited, deleted), and we only want publishes.
Aggregation across runs
Pull the last 7 days of lint reports and render a trend. The merge config resolves source runs at schedule time — you'd normally populate from via a helper plugin that queries the run history.
{
"name": "weekly-quality",
"version": "1",
"on": { "schedule": { "cron": "0 9 * * 1" } },
"jobs": {
"aggregate-lint": {
"runsOn": "local",
"artifacts": {
"consume": ["lint-report"],
"merge": {
"strategy": "append",
"from": [
{ "runId": "${{ trigger.payload.runs[0] }}" },
{ "runId": "${{ trigger.payload.runs[1] }}" },
{ "runId": "${{ trigger.payload.runs[2] }}" }
]
}
},
"steps": [
{ "name": "Render trend report", "uses": "plugin:quality:trend" }
]
}
}
}Long-running job with progress updates
Shell commands stream stdout/stderr as events. Studio shows them live. The pattern is just builtin:shell with a long timeout and a command that prints progress.
{
"jobs": {
"reindex": {
"runsOn": "sandbox",
"timeoutMs": 7200000,
"steps": [
{
"name": "Reindex",
"id": "reindex",
"uses": "builtin:shell",
"with": {
"command": "pnpm kb mind rag-index --scope default --progress",
"timeout": 7200000
},
"progress": {
"source": "outputs.chunksProcessed",
"format": "{value} chunks processed"
}
}
]
}
}
}The progress binding (optional) tells clients to read a live value from step outputs and render it. Combined with the ::kb-output:: marker in the command's output, you get a live progress counter in the UI.
Fire-and-forget sub-workflow
Invoke another workflow without waiting for it.
{
"name": "parent",
"version": "1",
"on": { "manual": true },
"jobs": {
"main": {
"runsOn": "sandbox",
"steps": [
{ "name": "Do thing", "uses": "builtin:shell", "with": { "command": "./main.sh" } },
{
"name": "Kick off audit",
"uses": "workflow:audit",
"with": { "mode": "fire-and-forget", "inputs": { "sourceRunId": "${{ trigger.runId }}" } }
},
{ "name": "Continue", "uses": "builtin:shell", "with": { "command": "./finish.sh" } }
]
}
}
}The audit workflow runs independently; the parent continues immediately. The parent's step records the child run ID but doesn't wait.
Default invocation mode is 'wait'. Use 'fire-and-forget' when the child's outcome doesn't affect the parent's decisions.
Things that aren't really patterns
"Run everything in parallel with no dependencies" — just define multiple jobs with no needs. They'll run concurrently subject to execution backend capacity.
"Run one job at a time, serialized" — give them all the same concurrency.group.
"Skip if condition" — put the if on the job or the step, not a separate gate.
"Conditional sub-workflow" — use if on the step with workflow:<id>, same as any other step.
What to read next
- Spec Reference — every field documented.
- Gates & Approvals — deep dive on the human-in-the-loop pieces.
- Retries & Error Handling — when and how to retry.
- Services → Workflow Daemon — the service that runs these.