Basic workflow patterns
A standard lint-then-test pipeline using job dependencies (needs):
import { workflow, job, step, pr } from '@kici-dev/sdk';
const lint = job('lint', { runsOn: 'linux', steps: [ step('install', async ({ $ }) => { await $`pnpm install --frozen-lockfile`; }), step('check', async ({ $ }) => { await $`pnpm lint`; await $`pnpm format:check`; }), ],});
const test = job('test', { runsOn: 'linux', needs: [lint], steps: [ step('install', async ({ $ }) => { await $`pnpm install --frozen-lockfile`; }), step('test', async ({ $ }) => { await $`pnpm test`; }), ],});
const typecheck = job('typecheck', { runsOn: 'linux', needs: [lint], steps: [ step('install', async ({ $ }) => { await $`pnpm install --frozen-lockfile`; }), step('typecheck', async ({ $ }) => { await $`pnpm typecheck`; }), ],});
export default workflow('ci', { on: pr({ target: 'main' }), jobs: [lint, test, typecheck],});The test and typecheck jobs both depend on lint, so they run in parallel after lint succeeds. KiCI validates the dependency graph at compile time — cycles and missing references are caught before you commit. At runtime, jobs are gated on upstream completion: a job only dispatches after every entry in its needs array reaches a terminal status that satisfies the edge. If an upstream fails, downstream jobs skip by default (override per-edge with when: 'always'). See Job dependencies (needs) in the SDK reference for the full matrix of needs forms (string, Job ref, { name, when }, dynamicGroup()) and needs-scheduler for the dispatch semantics.
Single-step jobs don’t need a steps array. When a job only does one thing, pass run to job() instead of wrapping it in steps: [step(...)]:
import { job, workflow, push } from '@kici-dev/sdk';
const smoke = job('smoke', { runsOn: 'default', run: async ({ $, log }) => { await $`curl -fsS https://example.com/health`; log.info('Health check passed'); },});
export default workflow('smoke', { on: push({ branches: 'main' }), jobs: [smoke],});run is mutually exclusive with steps (throws at compile time if both are set). Outputs are flat on job.result (no step-name nesting). See Single-step job shorthand in the SDK reference.
PR-only workflow with branch filters
Section titled “PR-only workflow with branch filters”Use pr() to filter by events, target branches, source branches, and file paths:
import { workflow, job, step, pr } from '@kici-dev/sdk';
// Only trigger on opened/synchronize events targeting main,// and only when source code files changeconst trigger = pr({ events: ['opened', 'synchronize'], target: ['main', 'develop'], paths: ['src/**', 'packages/**', '!**/*.md', '!docs/**'],});
const build = job('build', { runsOn: 'linux', steps: [ step('build', async ({ $ }) => { await $`pnpm build`; }), ],});
export default workflow('pr-checks', { on: trigger, jobs: [build],});PR trigger options
Section titled “PR trigger options”| Option | Type | Description |
|---|---|---|
target | string | RegExp | (string | RegExp)[] | Match target branches (glob or regex) |
source | string | RegExp | (string | RegExp)[] | Match source branches (glob or regex) |
events | PrEvent[] | Filter PR event types |
paths | string[] | Only trigger when matching files change. Use ! prefix for exclusions (e.g., '!docs/**') |
description | string | Add a human-readable description |
Default PR events (when events is not specified): opened, synchronize, reopened, closed.
Push trigger with branch filters
Section titled “Push trigger with branch filters”Use push() for push-based workflows:
import { workflow, job, step, push } from '@kici-dev/sdk';
// Deploy on pushes to mainconst deploy = job('deploy', { runsOn: 'linux', steps: [ step('deploy', async ({ $ }) => { await $`pnpm build`; await $`pnpm deploy`; }), ],});
export default workflow('deploy', { on: push({ branches: 'main' }), jobs: [deploy],});Push trigger options
Section titled “Push trigger options”| Option | Type | Description |
|---|---|---|
branches | string | RegExp | (string | RegExp)[] | Match branch names (glob or regex) |
tags | string | RegExp | (string | RegExp)[] | Match tag names (glob or regex) |
paths | string[] | Only trigger when matching files change. Use ! prefix for exclusions (e.g., '!docs/**') |
description | string | Add a human-readable description |
Regex branch patterns
Section titled “Regex branch patterns”Both pr() and push() accept regex patterns alongside glob strings:
// Glob patternpush({ branches: 'release/*' });
// Regex patternpush({ branches: /^release\/v\d+\.\d+$/ });Multiple triggers
Section titled “Multiple triggers”A workflow can respond to multiple trigger types:
import { workflow, job, step, pr, push } from '@kici-dev/sdk';
const test = job('test', { runsOn: 'linux', steps: [ step('test', async ({ $ }) => { await $`pnpm test`; }), ],});
export default workflow('ci', { on: [pr({ target: 'main' }), push({ branches: 'main' })], jobs: [test],});Manual / local-only workflow (no git events)
Section titled “Manual / local-only workflow (no git events)”Sometimes you want a workflow that does not fire on pushes, pull requests, tags, or any other git activity — only when you explicitly ask for it. Use dispatch() as the trigger: it corresponds to GitHub’s repository_dispatch event, which is never emitted by commits, PRs, tags, releases, or any other automatic git action. The workflow stays idle until someone explicitly invokes it.
There are two ways to “explicitly invoke” a dispatch() workflow:
- Locally from your laptop, with
kici run dispatch --local— this machine joins as an ephemeral agent through the warm local dev plane, so no orchestrator deployment is required. This is the path to use while you haven’t wired the repo to a deployed KiCI orchestrator. - Remotely, if the repo is connected to a KiCI orchestrator via a GitHub App, by calling GitHub’s repository-dispatch API:
curl -X POST -H "Authorization: token <PAT>" -H "Accept: application/vnd.github+json" https://api.github.com/repos/<owner>/<repo>/dispatches -d '{"event_type":"hello"}'. GitHub fans the webhook out to the App, the orchestrator normalizes it into a KiCIdispatchevent (seepackages/orchestrator/src/providers/github/normalizer.ts), and the matched workflow runs.
Note that GitHub’s workflow_dispatch event (the “Run workflow” button / /actions/workflows/.../dispatches API) is GitHub-Actions-internal and is not delivered to KiCI. The SDK has no workflowDispatch() trigger. Only repository_dispatch reaches KiCI.
import { workflow, job, step, dispatch } from '@kici-dev/sdk';
export default workflow('hello-world', { on: dispatch(), jobs: [ job('greet', { runsOn: 'linux', steps: [ step('say-hello', async ({ $ }) => { await $`echo "Hello, World!"`; }), ], }), ],});Run it locally, without any orchestrator deployment:
npx kici compile # regenerate .kici/kici.lock.jsonnpx kici run dispatch --localkici run dispatch --local compiles the workflow, matches triggers against a simulated dispatch event, and executes the matched jobs on this machine — which joins as an ephemeral agent through the warm local dev plane — with DAG-based scheduling. No webhook, no GitHub, no deployed orchestrator involved. See kici run <event> --local for options like --env, --in-place, and --offline.
Unfiltered vs typed dispatch()
Section titled “Unfiltered vs typed dispatch()”Leave dispatch() unfiltered while you drive it from kici run <event> --local. The CLI simulates a dispatch event with no event type (i.e. action is undefined), so a trigger defined as dispatch({ types: ['deploy', 'rollback'] }) will not match kici run dispatch --local — the typed form is intended for real repository_dispatch deliveries from the orchestrator.