SDK reference: runtime
All types are exported from @kici-dev/sdk as type-only imports.
Core types
Section titled “Core types”| Type | Description |
|---|---|
Workflow | Workflow definition returned by workflow() |
WorkflowOptions | Options for workflow() factory |
Job | Job definition returned by job() |
JobOptions | Options for job() factory |
Step<TOutputs> | Step definition returned by step() |
StepOptions<T> | Options for step() factory (full form with outputs) |
StepOptionsBase | Facets shared by the plain and the check variant of StepOptions (outputs, continueOnError, timeout, retry, cache, rules, onCancel, cleanup, approval) |
StepOptionsPlain<T> | StepOptions variant without a check facet: run(ctx) only |
StepOptionsWithCheck<T, TDrift> | StepOptions variant with a check facet: check(ctx) returns drift or null, and summarize(drift) is required. See Idempotent helpers |
NormalizedRetry | Retry policy with defaults filled in (maxAttempts, delayMs, backoff, maxDelayMs, optional retryIf), carried on the built Step |
SourceLocation | { file, line, column } captured at a step() call site |
StepRunFn | Simple step function type: (ctx) => Promise<void> |
BareStepFn | Bare step function (no options, just (ctx) => ...) |
StepInput | Union of step input forms accepted by job() |
OutputSchema | Record of Zod types for step outputs |
InferOutputs<T> | Infer output type from output schema |
ContainerConfig | Container config for job execution: image or dockerfile (with context, target, args), plus env and registry auth. See Container jobs. |
RunsOn | Union of runsOn forms: string | RegExp | (string | RegExp)[] | RunsOnSelector. A plain string matches exactly, a string with glob metacharacters (*?[]{}) is a glob, and a RegExp is a regular expression. See Targeting by pattern. |
RunsOnSelector | Object form for runsOn with labels (required) and exclude (optional) properties. Each element accepts the exact / glob / regex forms on both sides. |
RunsOnPick | Single-agent selection policy when several agents match a runsOn selector: 'deterministic' (stable hash — same job lands on the same host across re-runs) or 'any' (spread load). See runsOnAll for the fan-out forms. |
Fixture | Test fixture definition returned by fixture() |
FixtureOptions | Options for fixture() factory |
Registry | Private npm registry declaration used in WorkflowOptions.registries |
GenericInitConfig | Hand-written per-job init directive. See Per-job init |
MiseInitConfig | Overrides for the mise toolchain preset (cache, timeout, env, shell, version) |
AptInitConfig | Settings for the apt preset (packages, cache, timeout, env, shell). See Toolchain presets |
InitPreset | Typed toolchain preset: 'mise' | { mise: MiseInitConfig } | { apt: AptInitConfig } |
InitItem | One init directive: GenericInitConfig | InitPreset |
InitConfig | Per-job init value: one InitItem, an ordered array of them, 'auto', or false |
ParallelGroup | Concurrent step group returned by parallel(). See Parallel steps |
ParallelOptions | Options for parallel(): failFast, maxParallel, name |
ApprovalConfig | Approval gate declaration: true, an ApproverClause[], or { when?, approvers?, reason?, timeout? }. See Approvals |
ApproverClause | One approver clause: { team } or { user } |
ApprovalWhen | When the gate fires: 'always' (before the element runs) or 'drift' (between a step’s check and run) |
NormalizedApproval | Normalized gate shape written into the lock file: { clauses, reason?, timeoutSeconds?, when } |
InvokeConfig | Options for invokeSource() on an invoke-gate job. See Global workflows |
FilterFn | Workflow-level pre-dispatch predicate: (ctx: FilterContext) => boolean | Promise<boolean> |
FilterContext | Context passed to a filter: sourceRepo, workflowRepo, event, changedFiles, changedFilesStatus, env, $ |
CreateFilterContextInput | Input accepted when a FilterContext is built for a filter |
Trigger types
Section titled “Trigger types”| Type | Description |
|---|---|
Trigger | Trigger definition (trigger config + source location) |
TriggerConfig | Union of all 23 trigger config types |
PrTriggerConfig | PR trigger configuration (from pr()) |
PushTriggerConfig | Push trigger configuration (from push()) |
TagTriggerConfig | Tag trigger configuration (from tag()) |
CommentTriggerConfig | Comment trigger configuration (from comment()) |
ReviewTriggerConfig | Review trigger configuration (from review()) |
ReviewCommentTriggerConfig | Review comment trigger configuration (from reviewComment()) |
ReleaseTriggerConfig | Release trigger configuration (from release()) |
DispatchTriggerConfig | Repository dispatch trigger configuration (from dispatch()) |
CreateTriggerConfig | Ref creation trigger configuration (from create()) |
DeleteTriggerConfig | Ref deletion trigger configuration (from delete()) |
StatusTriggerConfig | Commit status trigger configuration (from status()) |
WorkflowRunTriggerConfig | Workflow run trigger configuration (from workflowRun()) |
ForkTriggerConfig | Fork trigger configuration (from fork()) |
StarTriggerConfig | Star trigger configuration (from star()) |
WatchTriggerConfig | Watch trigger configuration (from watch()) |
WebhookTriggerConfig | Catch-all webhook trigger configuration (from webhook()) |
KiciEventTriggerConfig | Custom event trigger configuration (from kiciEvent()) |
WorkflowCompleteTriggerConfig | Workflow completion trigger configuration (from workflowComplete()) |
WorkflowsFailedBatchTriggerConfig | Batched workflow-failure trigger configuration (from workflowsFailedBatch()) |
JobCompleteTriggerConfig | Job completion trigger configuration (from jobComplete()) |
GenericWebhookTriggerConfig | Generic webhook trigger configuration (from genericWebhook()) |
ScheduleTriggerConfig | Schedule trigger configuration (from schedule()) |
LifecycleTriggerConfig | Lifecycle trigger configuration (from lifecycle()) |
PrConfigInput | Config object for pr() factory |
PushConfigInput | Config object for push() factory |
TagConfigInput, CommentConfigInput, … | Config object for each remaining trigger factory, named <Trigger>ConfigInput |
DispatchInputsMap | Record<string, ZodType> — typed dispatch() inputs |
DefinedDispatchInputs<TMap> | Handle returned by defineDispatchInputs(); .from(ctx) reads the validated inputs. See Triggers |
InferDispatchInputs<TMap> | Value type inferred from a DispatchInputsMap |
BodyMatchPattern | Comment body match: { type: 'glob', pattern } | { type: 'regex', pattern, flags? } |
PushEvent | 'push' |
CommentAction | 'created' | 'edited' | 'deleted' |
CommentSource | 'issue' | 'pr' |
ReviewAction | 'submitted' | 'edited' | 'dismissed' |
ReviewState | 'approved' | 'changes_requested' | 'commented' | 'dismissed' |
ReviewCommentAction | 'created' | 'edited' | 'deleted' |
ReleaseAction | 'published' | 'unpublished' | 'created' | 'edited' | 'deleted' | 'prereleased' | 'released' |
RefType | 'branch' | 'tag' (for create() / delete()) |
StatusState | 'error' | 'failure' | 'pending' | 'success' |
WorkflowRunAction | 'requested' | 'completed' | 'in_progress' |
StarAction | 'created' | 'deleted' |
WatchAction | 'started' |
BranchPattern | { type: 'glob', pattern } | { type: 'regex', pattern, flags? } |
PrEvent | PR event string literal union (17 event types) |
GenericWebhookConfigInput | Config object for genericWebhook() factory |
GenericWebhookAuth | Union of generic webhook auth types (HMAC or API key) |
GenericWebhookHmacAuth | HMAC-SHA256 auth configuration for generic webhooks |
GenericWebhookApiKeyAuth | API key auth configuration for generic webhooks |
GenericWebhookAuthMethod | Auth method string literal ('hmac-sha256' | 'api-key') |
Rule types
Section titled “Rule types”| Type | Description |
|---|---|
Rule | Rule definition returned by rule() / skip() |
RuleCheckFn | (ctx: RuleContext) => Promise<boolean> | boolean |
RuleContext | Context passed to rule check functions |
RuleResult | Result of rule evaluation (label, passed, duration) |
EventPayload | Discriminated union over event type (narrow on type for autocomplete) |
RuleEvaluationResult | Result of evaluating a rule list (allPassed + per-rule results) |
CreateRuleContextInput | Input accepted when a RuleContext is built: event, changedFiles, changedFilesStatus, env, dispatchInputs, fanout, sourceRepo, workflowRepo |
Matrix types
Section titled “Matrix types”| Type | Description |
|---|---|
Matrix | Union: StaticMatrixArray | StaticMatrixObject | DynamicMatrixFn |
StaticMatrixArray | string[] |
StaticMatrixObject | Record<string, string[]> |
DynamicMatrixFn | (ctx) => Promise<StaticMatrixArray | StaticMatrixObject> |
DynamicMatrixContext | Context passed to dynamic matrix functions |
MatrixValues | Values exposed to steps (value? + named dimensions) |
MatrixInclude | Record<string, string> — additional combinations |
MatrixExclude | Record<string, string> — removed combinations |
Hook types
Section titled “Hook types”| Type | Description |
|---|---|
HookConfig | Hook definition returned by hook factories (onCancel(), etc.) |
HookFn | Hook function type: (ctx: HookContext) => Promise<void> |
HookInput | Hook input: HookFn | { run: HookFn; timeout?: number } |
HookContext | Context passed to hook functions |
OutcomeMetadata | Metadata about the outcome that triggered the hook |
Dynamic job types
Section titled “Dynamic job types”| Type | Description |
|---|---|
DynamicJobFn | (ctx) => Promise<Job[]> |
DynamicJobContext | Context for dynamic job generators |
JobOrFactory | Job | DynamicJobFn |
TaggedDynamicJobFn | DynamicJobFn tagged with a group name by dynamicJob() |
ResultAwareDynamicJobConfig | Options-object form of dynamicJob(): { needs?, generate, gitCredentials? } |
ResultAwareDynamicJobFn | Generator tagged with its declared needs (result-aware) |
DynamicJobNeed | One declared upstream edge: a job, a job name, a DynamicGroupRef, { name, when? }, or { group, when? } |
NeedsWhen | Keyword run condition for a needs edge: 'on-success' | 'always' | 'on-skip' | 'on-failure' |
NeedsWhenInput | NeedsWhen, or a raw array of upstream terminal statuses |
DynamicGroupRef | Reference returned by dynamicGroup() |
NeedsContext | The ctx.needs map, keyed by job or group name |
NeedEntry | One ctx.needs value: { result, status } for a job, an array of GroupNeedEntry for a group, or an InvokeNeedEntry for an invoke gate |
GroupNeedEntry | One group member: { name, result, status } |
InvokeNeedEntry | Invoke-gate need: { result: InvokeResult[] }, one entry per triggered run |
InvokeResult | One invoked run: { repo, workflow, runId, status, outputs } |
UpstreamSnapshot | Frozen upstream outputs and statuses that ctx.needs is built from |
Context types
Section titled “Context types”| Type | Description |
|---|---|
StepContext<T> | Context passed to step run functions |
Logger | Logger interface (info, warn, error, debug) |
WorkflowInfo | Workflow metadata: { name: string } |
JobInfo | Job metadata: { name: string, runsOn: string } |
AgentInfo | Facts about the pinned agent (host, labels, platform, arch), set on runsOnAll fan-out jobs |
FanoutPosition | Position of a child within its fan-out (host or matrix), deterministically ordered |
MatrixJobOutputs | Envelope returned by jobOutputs() for a matrix upstream: { byMatrix, merged } |
HostJobOutputs | Envelope returned by jobOutputs() for a runsOnAll upstream, keyed per host |
RepoInfo | Repository metadata available in step context |
StepSecrets | Async accessor interface for step secrets (get, expose, has, getMeta, list, mountFile, exposeFile) |
StepSecretsTyped | Typed step secrets with known key inference |
KnownSecretKeys | String literal union of declared secret context keys |
SecretNotFoundError | Thrown when accessing a nonexistent key in secrets |
TrackedStepSecrets | StepSecrets plus access tracking: getAccessLog(), getMountedKeys(), getMountRecords() (key names only, never values) |
SecretMeta | Result of ctx.secrets.getMeta(key): { value, backend, scope } |
SecretFileOptions | Options for ctx.secrets.mountFile() / exposeFile(): sources, divider?, mode? (default 0o600), name? |
MountedFile | Result of mountFile() / exposeFile(): { path } |
StepSecretMountKind | 'mountFile' | 'exposeFile' |
StepSecretMountRecord | Audit record of one mount: { sources, target, envVar?, kind } |
StepSecretsFileHost, StepSecretsFileWiring, StepSecretsHandle | Host adapter, wiring, and handle types the runner uses to back mountFile() / exposeFile() and remove the files when the step ends |
KiciApi | Type of ctx.kici: infrastructure, inventory, oidc, git, host, bootstrap, scaler |
KiciApiTransport | (method, params?) => Promise<unknown> — the transport a KiciApi is built on |
InfrastructureApi | ctx.kici.infrastructure: list() returns an InfrastructureListResult |
InfrastructureListResult | { scalers, agents } — each entry carries a source ('local' or a peer instance id) |
InventoryApi | ctx.kici.inventory: query(selector?) and get(agentId) |
InventorySelector | Label selector for ctx.kici.inventory.query() |
HostInventoryEntry | One host in the inventory roster |
HostApi | ctx.kici.host: requestReboot({ deadlineMs? }), used by restartHost() |
CacheSpec | { key, paths, restoreKeys? }. See Caching |
CacheInput | CacheSpec | CacheSpec[] |
CacheApi | Type of ctx.cache |
CacheRestoreResult | Result of ctx.cache.restore(): { hit, matchedKey? } |
ArtifactsApi | Type of ctx.artifacts. See Artifacts |
ArtifactResult | Result of an artifact upload or download: { size, sha256 } |
AttestProvenanceOptions | Options for ctx.attestProvenance(): { subject, audience? } |
AttestProvenanceResult | Result of ctx.attestProvenance(): the stored bundle, or { deferred: true, … } when the identity token is minted later |
ProvenanceSubjectInput | The attested artifact: { name, digest } or { name, path } |
Git credential types
Section titled “Git credential types”See Git credentials for how these are used.
| Type | Description |
|---|---|
ForgeName | 'github' | 'gitlab' | 'bitbucket' | 'generic' |
GitCredentialRef | One credential: { kind: 'app' }, { kind: 'token' }, or { kind: 'ssh' }, each field given as a <name>Secret reference or a <name>Value |
GitCredentialMap | Named credentials for a job (gitCredentials); default applies when a call names none |
Sourced<Name> | Field pair where exactly one of <Name>Secret and <Name>Value is set |
ContainerRegistryAuth | Registry credentials for container.auth: a token plus optional username and registry |
GitHubPermissions | GitHub repository permissions (contents, pull_requests, workflows, …); unknown keys pass through to GitHub |
WriteOptions<F> | Options for withWrite(): { permissions } on GitHub, no fields on other forges |
GitGrant | What a credential can do: { scoped: false } or { scoped: true, permissions } |
RepoHandle<F> | A checked-out repository: identifier, path, ref?, sha?, withWrite(opts, fn) |
GitApi | Type of ctx.kici.git: { github: GitHubApi } |
GitHubApi | getToken({ repositories, permissions, credential? }) — mint a token as a value |
GitTokenResult | Result of getToken(): { token, expiresAt, granted } |
Other helpers and constants
Section titled “Other helpers and constants”These are exported from the root entry point. Most workflows never call them directly.
| Export | Description |
|---|---|
isParallelGroup(x) | Type guard: true for a group returned by parallel() |
isEventDefinition(value) | Type guard: true for a value returned by defineEvent() |
isDynamicGroupRef(value) | Type guard: true for a value returned by dynamicGroup() |
getDynamicJobGroup(fn) | Group name a generator was tagged with by dynamicJob(), or undefined |
getDynamicJobNeeds(fn) | Declared needs of a result-aware generator, or undefined for an event-only one |
getDynamicJobGitCredentials(fn) | gitCredentials a generator declared, or undefined |
DYNAMIC_JOB_GROUP_TAG, DYNAMIC_JOB_NEEDS_TAG, DYNAMIC_JOB_GIT_CREDENTIALS_TAG, DYNAMIC_GROUP_TAG | Symbol keys the dynamic-job helpers tag their values with |
CacheSpecSchema | Zod schema for a CacheSpec: non-empty key, at least one non-empty paths entry, optional restoreKeys |
ArtifactNameSchema | Zod schema for an artifact name: 1 to ARTIFACT_NAME_MAX_LENGTH characters from A-Z a-z 0-9 . _ -, not only dots |
ARTIFACT_NAME_MAX_LENGTH | Maximum artifact name length (128) |
provenanceSubjectIsPath(subject) | Type guard: true for a { name, path } provenance subject |
assertSecretName(value, field, subject?) | Throws when a credential field holds what looks like the credential itself (a PEM block or a GitHub token) instead of a secret name |
createStepOutputProxy, createJobOutputProxy, createSnapshotOutputProxy | Build the lazy proxies behind step.result, job.result, and ctx.needs.<job>.result |
resolveStepOutputs, resolveJobOutputs | Read the recorded outputs of a step or job reference |
getStepOutputsMap, getJobOutputsMap, getStepRefMap | Read the maps the output proxies resolve against |
OutputsMap, StepRefMap | Types of those maps: outputs keyed by step or job name, and bare step functions mapped to their step names |
waitForHostAlive, restartHost (WaitForHostAliveOptions, RestartHostOptions) | Host-restart steps. See Host restart |
agentVersionConverge (AgentVersionConvergeOptions, AgentVersionDrift) | Check-step that converges a fleet host’s agent onto the orchestrator’s version. See Distribution |
buildAgentCloudInit (AgentCloudInitCredentials, ClaimCodeCredentials, AgentCloudInitOptions, AgentDeliveryMode, UserDataEncoding, CloudInitWriteFile) | Renders the cloud-init user_data that boots a scaler-provisioned agent. See Autoscaling workflows |
CheckStepOptions | Options for checkStep(). See Idempotent helpers |
StepContext
Section titled “StepContext”The context object passed to every step’s run function:
interface StepContext<TInputs = Record<string, unknown>> { /** zx shell executor for running commands */ $: typeof Shell; /** Structured logger */ log: Logger; /** Environment variables */ env: Record<string, string | undefined>; /** Set an environment variable visible to this step and all subsequent steps */ setEnv(key: string, value: string): void; /** Prepend a directory to PATH, visible to this step and all subsequent steps */ addPath(dir: string): void; /** Aborted when this step should stop early (job cancelled, job timeout, fail-fast sibling) */ signal: AbortSignal; /** Typed inputs from dependency step outputs */ inputs: TInputs; /** Validated workflow-dispatch inputs declared via `dispatch({ inputs })` */ dispatchInputs: Readonly<Record<string, string | number | boolean | null>>; /** Current workflow metadata */ workflow: WorkflowInfo; /** Current job metadata */ job: JobInfo; /** Matrix values for current job instance (undefined without matrix) */ matrix?: MatrixValues; /** Hostname of the pinned agent — set only on `runsOnAll` host fan-out */ host?: string; /** Facts about the pinned agent — set only on `runsOnAll` host fan-out */ agent?: AgentInfo; /** Position of this child within its fan-out (host or matrix); undefined otherwise */ fanout?: FanoutPosition; /** Raw webhook payload from the git provider */ rawPayload?: Record<string, unknown>; /** Which git provider triggered this workflow (e.g. 'github', 'gitlab') */ provider?: string; /** Whether this execution was triggered by `kici run remote` (developer-initiated remote run) */ isTestRun: boolean; /** The job's own checked-out repository (present for every job that checks out); `withWrite` opens a write window for it — see [Git credentials](../patterns/git-credentials.md) */ repo?: { identifier: string; path: string; ref?: string; sha?: string; withWrite( opts: { permissions?: Record<string, string>; credential?: string }, fn: () => Promise<void>, ): Promise<void>; }; /** Registering repo of a global workflow (undefined for non-global workflows) */ workflowRepo?: RepoInfo; /** Repo where the triggering event occurred (undefined for non-global workflows) */ sourceRepo?: RepoInfo; /** The resolved context name for this job (undefined for jobs without a context) */ context?: string; /** Secrets resolved for this job's context. Never injected into env automatically. */ secrets: StepSecretsTyped; /** Emit a custom event that can trigger other workflows — typed or ad-hoc by name */ emit<T extends z.ZodTypeAny>( definition: EventDefinition<T>, payload: z.infer<T>, options?: EventEmitOptions, ): Promise<{ deliveryId: string }>; emit( eventName: string, payload?: Record<string, unknown>, options?: EventEmitOptions, ): Promise<{ deliveryId: string }>; /** Resolve outputs from a preceding step by reference */ outputsOf<T>(ref: { _tag: 'Step'; name: string } | ((...args: any[]) => any)): T; /** Resolve outputs from a preceding job by reference (fan-out upstreams return an envelope) */ jobOutputs<T>(ref: Job<T>): T | MatrixJobOutputs<T> | HostJobOutputs<T>; jobOutputs(ref: { name: string }): Record<string, unknown> | MatrixJobOutputs | HostJobOutputs; /** Publish a secret output value from this job (encrypted before leaving the agent) */ setSecretOutput(key: string, value: string): void; /** Typed KiCI API — orchestrator queries over WS (e.g. `kici.infrastructure.list()`) */ kici: KiciApi; /** Imperative cache API — `cache.restore(spec)` / `cache.save(spec)` */ cache: CacheApi; /** Imperative artifacts API — `artifacts.upload(name, paths)` / `artifacts.download(name)` */ artifacts: ArtifactsApi; /** Build, sign, and persist a build-provenance attestation for a produced artifact */ attestProvenance(opts: AttestProvenanceOptions): Promise<AttestProvenanceResult>; /** Allocate a job-scoped scratch directory, removed automatically when the job ends */ mktemp(label?: string): Promise<TempHandle>; /** Allocate a job-scoped scratch file, removed automatically when the job ends */ mktempFile(label?: string, opts?: { suffix?: string }): Promise<TempHandle>; /** Upstream needs keyed by job or group name — `needs.<job>.result` / `.status` */ needs?: NeedsContext;}Logger
Section titled “Logger”interface Logger { info(message: string, ...args: unknown[]): void; warn(message: string, ...args: unknown[]): void; error(message: string, ...args: unknown[]): void; debug(message: string, ...args: unknown[]): void;}step('example', async ({ $, log, env, matrix, workflow, job }) => { log.info(`Running in workflow: ${workflow.name}`); log.info(`Job: ${job.name} on ${job.runsOn}`);
if (matrix) { log.info(`Matrix value: ${matrix.value}`); }
const token = env.GITHUB_TOKEN; await $`echo "Building..."`;});rawPayload and rule-context parity
Section titled “rawPayload and rule-context parity”ctx.rawPayload carries the same data that rule contexts access via ctx.event.payload — the unmodified webhook body from the git provider. A rule that branches on ctx.event.payload.client_payload.foo and a step body that reads ctx.rawPayload.client_payload.foo see the same value. Use it inside steps when the operator’s dispatch payload (or any other provider-specific field) needs to drive runtime behavior — e.g. a --dry-run toggle or a deploy target — without bouncing the data through an env var.
What’s captured in the dashboard log viewer. KiCI captures user output from every place in a workflow that can run TypeScript:
- Inside a step body — the agent merges three streams into the step’s log:
ctx.log.*structured calls, subprocess stdout/stderr fromctx.$, and any directconsole.log/.error/.warn/.info/.debug(or other library that writes toprocess.stdout/process.stderr). - Inside hooks (
beforeStep,afterStep,onSuccess,onFailure,onCancel,cleanup) — the same three streams are captured; per-step hooks share the step’s log, post-loop hooks get their own dashboard row. - At workflow module top-level, in rule
checkfunctions, and in the workflowconcurrency.groupfunction — captured to the workflow-levelpreparelog bucket for the job, alongside KiCI’s own setup narration. - Inside a dynamic
context/env/concurrencyGroupfunction on a static job — captured to the__init__job’s synthetic step-0 log, which appears in the timeline as “Init: jobname”. - Inside a
DynamicJobFnbody and the per-generated-jobcontext/env/concurrencyGroup/matrixfunctions — captured to the__dynamic__job’s synthetic step-0 log (“Evaluate: jobname” in the timeline). The$parameter in that context is a scoped zx shell, soawait $\…“ subprocess output is captured too.
Use whichever style is convenient — you don’t have to wrap console.log in the provided log parameter to make it visible. One limitation applies to in-process contexts only (init, build, dynamic-eval): direct process.stdout.write / printf is not captured there, because the agent’s own logger uses that path and we don’t want agent-internal output leaking into your step logs. Use console.* or the log parameter instead. See Log streaming for the full capture surface and limits (default 10 MB per step, backpressure behavior).
setEnv(key, value)
Section titled “setEnv(key, value)”Export an environment variable to later steps in the same job. This is the canonical way to hand a value computed in one step to the steps that follow — the equivalent of echo "KEY=VALUE" >> $GITHUB_ENV in GitHub Actions. The value is visible to the current step and all subsequent steps in the job.
step('setup', async (ctx) => { // Install a tool and record its version await ctx.$`npm install -g some-tool`; const version = (await ctx.$`some-tool --version`).stdout.trim(); ctx.setEnv('TOOL_VERSION', version);});
step('use', async (ctx) => { // TOOL_VERSION is available here ctx.log.info(`Using tool version: ${ctx.env.TOOL_VERSION}`);});Behavior:
- Last-write-wins — if multiple steps set the same key, the last value is used
- Cannot override operator-injected secrets (the operator value takes precedence)
- Changes take effect immediately in the current step and persist for all subsequent steps
- Shell commands export the same way by appending to
$KICI_ENV(see Exporting env from shell commands below)
addPath(dir)
Section titled “addPath(dir)”Prepend a directory to PATH for the current step and all subsequent steps in the same job. Useful for tools installed to non-standard locations.
step('install-go', async (ctx) => { await ctx.$`curl -L https://go.dev/dl/go1.22.0.linux-amd64.tar.gz | tar -C /tmp -xz`; ctx.addPath('/tmp/go/bin');});
step('build', async (ctx) => { // `go` is now on PATH await ctx.$`go build ./...`;});Exporting env from shell commands ($KICI_ENV / $KICI_PATH)
Section titled “Exporting env from shell commands ($KICI_ENV / $KICI_PATH)”setEnv and addPath are the TypeScript form of “export env to later steps”. A shell command — including a non-JS toolchain installer — exports env the same way by appending to two files the agent points at before every step:
$KICI_ENV— appendKEY=valuelines. Each becomes an environment variable visible to subsequent steps, exactly likectx.setEnv('KEY', 'value').$KICI_PATH— append one directory per line. Each is prepended toPATHfor subsequent steps, exactly likectx.addPath(dir). The first directory appended ends up first onPATH.
step('install-tool', async (ctx) => { await ctx.$`./install-mytool.sh`; // installs to /opt/mytool // Export from the shell, no JS round-trip needed: await ctx.$`echo "MYTOOL_HOME=/opt/mytool" >> "$KICI_ENV"`; await ctx.$`echo "/opt/mytool/bin" >> "$KICI_PATH"`;});
step('build', async (ctx) => { // MYTOOL_HOME is set and /opt/mytool/bin is on PATH here. await ctx.$`mytool build`;});Format (v1):
- One
KEY=valueper line in$KICI_ENV. The split is on the first=, so the value may contain=. Blank lines and lines without a=are ignored. - One directory per line in
$KICI_PATH. Blank lines are ignored. - Values must be single-line — embedded newlines are not supported in v1.
Behavior (shared with setEnv / addPath):
- Applied after the step completes and visible to every later step in the job.
- Last-write-wins on a repeated key.
- Cannot override an operator-injected secret — a collision is ignored and logged, and the operator value is preserved.
- The files are reset before each step, so each step sees only its own appended lines.
setSecretOutput(key, value)
Section titled “setSecretOutput(key, value)”Publish an encrypted secret output from this job. Downstream jobs that list this job in their needs array receive the value merged into ctx.secrets.
const generateToken = job('generate-token', { steps: [ step('create', async (ctx) => { const token = (await ctx.$`vault write -f auth/token/create`).stdout.trim(); ctx.setSecretOutput('DEPLOY_TOKEN', token); }), ],});
const deploy = job('deploy', { needs: [generateToken], steps: [ step('deploy', async (ctx) => { // DEPLOY_TOKEN is available as a secret (decrypted by the orchestrator) const token = await ctx.secrets.get('DEPLOY_TOKEN'); await ctx.$`DEPLOY_TOKEN=${token} ./deploy.sh`; }), ],});Security model:
- The value is encrypted on the agent before leaving the machine (X25519 ECDH + AES-256-GCM)
- The orchestrator decrypts and re-encrypts with its own key before storing
- The ephemeral key pair is deleted when the run completes (forward secrecy)
- Downstream agents never see the plaintext — they receive it as part of their injected secrets
Limits:
- Maximum 20 secret outputs per job
- Maximum 64 KB per value
ctx.kici.oidc.token({ audience })
Section titled “ctx.kici.oidc.token({ audience })”Request a short-lived OIDC ID token for the current job, bound to an audience. The token is a signed JWT whose identity claims (repository, ref, sha, kici_run_id, kici_job_id) are derived by your orchestrator from the run context — a step cannot spoof them. Use it to authenticate the build to an external service that trusts the orchestrator’s OIDC issuer (for example, when generating build provenance).
The token also carries the event context a cloud trust policy needs to tell a fork pull request from a trusted push — is_fork, head_repository, trust_tier, event_name, and a pull-request-specific sub. See ID-token claims and cloud trust policies for the full claim table and a worked AWS policy.
const publish = job('publish', { steps: [ step('mint', async (ctx) => { const minted = await ctx.kici.oidc.token({ audience: 'sigstore' }); if ('deferred' in minted) throw new Error(`ID token deferred: ${minted.code}`); const { token, expiresIn } = minted; ctx.log.info(`Got an ID token valid for ${expiresIn}s`); // Hand `token` to a tool that exchanges it with the trusting service. }), ],});Behavior:
- The result is the minted token, or
{ deferred: true, code }(unavailable|failed) when the orchestrator could not mint one right now. A step that exchanges the token fails ondeferred;ctx.attestProvenancehandles it by freezing the statement for later fulfilment. - The token is short-lived (about 10 minutes) and scoped to the current run and job.
- The returned token value is automatically masked in step logs.
- The step never holds signing credentials — the orchestrator mints and signs the token on the step’s behalf from its own run records.
- Only available inside a running job step; calling it outside one (a dynamic-job generator, or the workflow module’s top level) rejects with a clear error.
kici run --localruns are supported: the local dev plane mints dev-signed tokens under the clearly-non-production issuerkici-local.
ctx.kici.inventory.query(selector?) / .get(agentId)
Section titled “ctx.kici.inventory.query(selector?) / .get(agentId)”Query the host inventory — the roster of agents in the caller’s orchestrator cluster — from inside a workflow. Each host is a HostInventoryEntry:
interface HostInventoryEntry { agentId: string; labels: string[]; // flat-string grouping/tags dimension properties: Record<string, string | number | boolean>; // typed host-vars dimension hostname: string | null; platform: string | null; arch: string | null; lifecycleClass: 'static' | 'ephemeral'; status: 'ready' | 'unreachable' | 'stale'; lastSeen: string; // ISO timestamp}Two dimensions describe a host. Labels are flat strings used for grouping and targeting (the same labels runsOn / runsOnAll match). Properties are typed host-vars (string | number | boolean) — the place for facts like region, cores, or gpu. A host reports its own properties via the agent’s KICI_PROPERTIES config, and an operator can pre-declare them with kici-admin host declare --prop key=value; the two are shallow-merged (agent-reported keys win). Inventory queries return only confirmed hosts: a host declared from the dashboard appears once its agent registers or an operator confirms it.
labels and hostname come back lowercase — KiCI folds both, so a pool declaring Docker reports docker. Compare against a lowercase value: h.labels.includes('gpu'), not h.labels.includes('GPU'). A label selector passed to query() folds too, so { include: [['GPU']] } matches. agentId and properties keep their case.
// All hosts:const all = await ctx.kici.inventory.query();
// Server-side label filter (OR-of-AND include groups, plus exclude):const dbHosts = await ctx.kici.inventory.query({ include: [[{ kind: 'exact', value: 'role:db' }]],});
// Property filtering is client-side — plain JS in the workflow:const euDbHosts = dbHosts.filter((h) => h.properties.region === 'eu');
// One host by id:const host = await ctx.kici.inventory.get('box-1'); // HostInventoryEntry | nullThe label selector is applied server-side (reusing the same glob/regex matchers as runsOnAll). Property filtering is client-side — you filter the returned array in plain JavaScript, so there is no query DSL to learn.
Headline use — dynamic-job fan-out. A dynamic-job generator can query the inventory and return one job per matching host, fanning a workflow out across a fleet:
const migrate = job('migrate', async (ctx) => { const hosts = await ctx.kici.inventory.query({ include: [[{ kind: 'exact', value: 'role:db' }]], }); return hosts .filter((h) => h.properties.region === 'eu') .map((h) => job(`migrate-${h.agentId}`, { runsOn: [h.agentId], run: async (c) => { await c.$`./migrate.sh`; }, }), );});A runsOn of a single host’s agentId (as in runsOn: [h.agentId] above) pins the job to that host: the orchestrator routes it to that agent only, and queues it with the pin if the host is momentarily offline — the same host-pin path runsOnAll uses. A runsOn with multiple labels or a glob/regex pattern stays ordinary label routing.
ctx.kici.inventory is available to both steps and dynamic-job generators (unlike ctx.kici.oidc.token, which is job-bound — the inventory is cluster-scoped, not job-bound).
Determinism caveat. The inventory is live: it can change between when a dynamic-job generator first runs (at dispatch) and when it re-evaluates (at agent time). Generating jobs from inventory.query() therefore inherits the same non-determinism contract as infrastructure.list() — KiCI warns when the re-evaluated job set drifts (a sibling job name changed) and hard-errors when a targeted job vanishes. Prefer stable inputs where you can, and treat a fanned-out job set as a snapshot of the roster at generation time.
ctx.attestProvenance({ subject })
Section titled “ctx.attestProvenance({ subject })”Build, sign, and persist a build-provenance attestation for an artifact your step produced. KiCI assembles an in-toto SLSA v1.0 provenance statement whose build identity (repository, ref, sha, run/job ids) comes from your orchestrator — not from the step — so it cannot be spoofed. KiCI then signs the statement and stores a verifiable bundle that the dashboard surfaces and the kici verify-attestation CLI checks.
The artifact is caller-supplied: give it either a precomputed digest or a path (relative to the step working directory) that KiCI digests with SHA-256. For a container image, pass the manifest digest your build tool emitted.
const publish = job('publish', { steps: [ step('build', async (ctx) => { await ctx.$`npm pack`; }), step('attest', async (ctx) => { // Digest a file KiCI hashes for you: const result = await ctx.attestProvenance({ subject: { name: 'my-pkg-1.2.3.tgz', path: 'my-pkg-1.2.3.tgz' }, }); ctx.log.info(`Attestation stored at ${result.storageKey}`);
// Or supply a precomputed digest (e.g. a container manifest digest): await ctx.attestProvenance({ subject: { name: 'ghcr.io/acme/app', digest: { sha256: '<manifest-digest>' } }, }); }), ],});Behavior:
- The attestation is a signed DSSE envelope over an in-toto statement carrying the SLSA v1.0 provenance predicate.
- It is signed with an ephemeral key bound to an orchestrator-minted identity token, so it is offline-verifiable against the orchestrator’s published signing keys — no online lookup needed at verify time.
- The bundle is persisted to object storage and recorded so the dashboard can show it and
kici verify-attestationcan retrieve it. - The returned
{ storageKey, subjectDigest, bundleMediaType }identifies the stored bundle. - Only available inside a running job step; calling it outside one (a dynamic-job generator, or the workflow module’s top level) rejects with a clear error.
kici run --localruns are supported: the offline local dev plane signs with a dev identity under the clearly-non-production issuerkici-local, and those bundles verify against a trust root exported withkici local trust-root.
See the build provenance guide for the end-to-end attest →
verify → view journey, including how to verify a bundle with kici verify-attestation.
Secrets
Section titled “Secrets”Workflows access secrets through ctx.secrets on StepContext. Use await ctx.secrets.get('KEY') to retrieve a value (rejects with SecretNotFoundError if the key is missing, fail-fast on typos), ctx.secrets.has('KEY') for a synchronous existence check, and await ctx.secrets.expose('KEY') when you need the value as a process.env entry for a child process.
Declaring the secret context
Section titled “Declaring the secret context”Each job picks its secret context via the context option on job(). The orchestrator resolves the context’s scoped-secret store at dispatch time, evaluates access rules, and sends the decrypted secrets to the agent:
const deploy = job('deploy', { runsOn: 'linux', context: 'production', steps: [/* ... */],});
export default workflow('deploy', { on: push({ branches: 'main' }), jobs: [deploy],});context accepts either a static string or an async function (event) => string | Promise<string> for dynamic resolution at trigger-evaluation time. The resolved context’s secrets are flattened into ctx.secrets.
Accessing secrets (ctx.secrets)
Section titled “Accessing secrets (ctx.secrets)”ctx.secrets provides flat access to the secrets resolved for the job’s context.
step('deploy', async ({ secrets }) => { // get() rejects with SecretNotFoundError if DEPLOY_TOKEN is not found const token = await secrets.get('DEPLOY_TOKEN');
// Safe check before access (no throw, synchronous) if (secrets.has('OPTIONAL_KEY')) { const optional = await secrets.get('OPTIONAL_KEY'); }});Throw behavior: get() rejects with SecretNotFoundError and the message lists all available keys. This catches typos immediately rather than producing silent undefined values.
Complete example
Section titled “Complete example”import { workflow, job, step, push } from '@kici-dev/sdk';
const deploy = job('deploy', { runsOn: 'linux', context: 'production', steps: [ step('deploy', async (ctx) => { const token = await ctx.secrets.get('DEPLOY_TOKEN');
// Safe check before access if (ctx.secrets.has('OPTIONAL_NOTIFICATION_URL')) { const url = await ctx.secrets.get('OPTIONAL_NOTIFICATION_URL'); ctx.log.info('Sending notification...'); }
// Pass to subprocess explicitly (secrets are NOT auto-injected as env vars) await ctx.$`DEPLOY_TOKEN=${token} ./scripts/deploy.sh`; }), ],});
export default workflow('deploy-production', { on: push({ branches: 'main' }), jobs: [deploy],});Security notes
Section titled “Security notes”- Secrets are not automatically injected as environment variables. You must explicitly pass them to subprocesses.
- All secret values are automatically masked in log output. If a step logs a string containing a secret value, the value is replaced with
***. - Secrets flow from the orchestrator to the agent via the authenticated WebSocket channel. The Platform tier never handles secret material.
Enumerating available keys (ctx.secrets.list)
Section titled “Enumerating available keys (ctx.secrets.list)”ctx.secrets.list() returns every secret key available to the step, sorted alphabetically. Synchronous, never throws, names only — call getMeta(key) to inspect backend / scope per key. Useful when the set of provisioned keys isn’t known at workflow-author time, for example to pick up every AGE_KEY_* the operator has seeded:
step('discover', async (ctx) => { const ageKeys = ctx.secrets.list().filter((k) => k.startsWith('AGE_KEY_')); ctx.log.info(`Found ${ageKeys.length} age keys`);});File-mounted secrets (ctx.secrets.mountFile / exposeFile)
Section titled “File-mounted secrets (ctx.secrets.mountFile / exposeFile)”Tools that require a file path on disk (sops SOPS_AGE_KEY_FILE, kubectl KUBECONFIG, gcloud GOOGLE_APPLICATION_CREDENTIALS) get a typed step-side API: ctx.secrets.mountFile(opts) writes the concatenation of one or more existing secrets to a per-step tmpfile and returns the path; ctx.secrets.exposeFile(envVar, opts) additionally sets process.env[envVar] = path. Files are removed and env vars are unset automatically when the step completes (success, failure, or timeout) — no manual cleanup. See Mounting secrets as files for the full options table, lifecycle details, and the canonical sops example.
Local test mode secrets
Section titled “Local test mode secrets”When running kici preview, you can provide secrets locally without an orchestrator.
.kici/.secrets file
Section titled “.kici/.secrets file”Create a .kici/.secrets file in your project (auto-gitignored by kici init):
# Flat secrets (before any section)DEPLOY_TOKEN=my-deploy-tokenAPI_KEY=my-api-key
# Context-scoped secrets[production]DB_PASSWORD=prod-secretAPI_KEY=prod-key
[npm-publish]NPM_TOKEN=npm-abc123Lines before any [section] header are flat secrets. Lines within a section become context-scoped secrets. Comments start with #. Values are everything after the first = (so values can contain = characters).
CLI flags
Section titled “CLI flags”Override or supplement file-based secrets with CLI flags:
# Inject flat secrets (repeatable)kici preview push --secret DEPLOY_TOKEN=my-token --secret API_KEY=my-key
# Inject context-scoped secrets (repeatable)kici preview push --context production.DB_PASSWORD=prod-secret --context npm-publish.NPM_TOKEN=abc123Precedence: CLI flags override .kici/.secrets file values. Context secrets are auto-flattened into ctx.secrets using the same merge logic as production (last context wins).
Fixtures
Section titled “Fixtures”Test fixtures define event replicas for kici run remote. They simulate trigger events without requiring real webhooks.
fixture(id, options)
Section titled “fixture(id, options)”function fixture( id: string, options: FixtureOptions | (() => FixtureOptions | Promise<FixtureOptions>),): Fixture;Parameters:
id— unique fixture name (no whitespace). Used inkici run remote <id>.options— aFixtureOptionsobject, or an async factory function returning one.
import { fixture, push } from '@kici-dev/sdk';
export const pushMain = fixture('push-main', { event: push({ branches: ['main'] }),});FixtureOptions
Section titled “FixtureOptions”| Property | Type | Description |
|---|---|---|
event | TriggerConfig | The trigger event to simulate (required). Its filters do not set the event’s branch. |
branch | string | The event’s branch (defaults to the checked-out branch). For a PR event, the base branch (default main); for a tag event, the tag name. |
sha | string | The event’s commit SHA (defaults to the HEAD commit) |
repo | string | Override repository (defaults to git-detected) |
pr | number | For PR events, override PR number |
secrets | Record<string, string> | Secret context mappings: { localName: 'remote-context' } |
workflowName | string | Bypass trigger matching and run this workflow directly. kici run remote --workflow overrides it. |
event takes the same trigger config a workflow’s on: takes, but in a fixture it only selects the event type. A filter such as push({ branches: ['main'] }) does not make the event a push to main: the event’s branch is branch when you set it, else the branch you have checked out. To simulate a push to main from any branch, set branch: 'main'. See Branch and commit in the testing guide.
Options can also be provided as an async factory function for dynamic fixture generation.