Skip to content

SDK reference: runtime

All types are exported from @kici-dev/sdk as type-only imports.

TypeDescription
WorkflowWorkflow definition returned by workflow()
WorkflowOptionsOptions for workflow() factory
JobJob definition returned by job()
JobOptionsOptions for job() factory
Step<TOutputs>Step definition returned by step()
StepOptions<T>Options for step() factory (full form with outputs)
StepRunFnSimple step function type: (ctx) => Promise<void>
BareStepFnBare step function (no options, just (ctx) => ...)
StepInputUnion of step input forms accepted by job()
OutputSchemaRecord of Zod types for step outputs
InferOutputs<T>Infer output type from output schema
ContainerConfigContainer config for job execution (image, env?)
RunsOnUnion 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.
RunsOnSelectorObject form for runsOn with labels (required) and exclude (optional) properties. Each element accepts the exact / glob / regex forms on both sides.
RunsOnAllInputUnion of runsOnAll host fan-out forms: string | RegExp | (string | RegExp)[] | { include: { all: (string | RegExp)[] }[]; exclude?: (string | RegExp)[] }. Same exact / glob / regex semantics per element. See runsOnAll.
FixtureTest fixture definition returned by fixture()
FixtureOptionsOptions for fixture() factory
RegistryPrivate npm registry declaration used in WorkflowOptions.registries
TypeDescription
TriggerTrigger definition (trigger config + source location)
TriggerConfigUnion of all 22 trigger config types
PrTriggerConfigPR trigger configuration (from pr())
PushTriggerConfigPush trigger configuration (from push())
TagTriggerConfigTag trigger configuration (from tag())
CommentTriggerConfigComment trigger configuration (from comment())
ReviewTriggerConfigReview trigger configuration (from review())
ReviewCommentTriggerConfigReview comment trigger configuration (from reviewComment())
ReleaseTriggerConfigRelease trigger configuration (from release())
DispatchTriggerConfigRepository dispatch trigger configuration (from dispatch())
CreateTriggerConfigRef creation trigger configuration (from create())
DeleteTriggerConfigRef deletion trigger configuration (from delete())
StatusTriggerConfigCommit status trigger configuration (from status())
WorkflowRunTriggerConfigWorkflow run trigger configuration (from workflowRun())
ForkTriggerConfigFork trigger configuration (from fork())
StarTriggerConfigStar trigger configuration (from star())
WatchTriggerConfigWatch trigger configuration (from watch())
WebhookTriggerConfigCatch-all webhook trigger configuration (from webhook())
KiciEventTriggerConfigCustom event trigger configuration (from kiciEvent())
WorkflowCompleteTriggerConfigWorkflow completion trigger configuration (from workflowComplete())
JobCompleteTriggerConfigJob completion trigger configuration (from jobComplete())
GenericWebhookTriggerConfigGeneric webhook trigger configuration (from genericWebhook())
ScheduleTriggerConfigSchedule trigger configuration (from schedule())
LifecycleTriggerConfigLifecycle trigger configuration (from lifecycle())
PrConfigInputConfig object for pr() factory
PushConfigInputConfig object for push() factory
BranchPattern{ type: 'glob', pattern } | { type: 'regex', pattern, flags? }
PrEventPR event string literal union (17 event types)
GenericWebhookConfigInputConfig object for genericWebhook() factory
GenericWebhookAuthUnion of generic webhook auth types (HMAC or API key)
GenericWebhookHmacAuthHMAC-SHA256 auth configuration for generic webhooks
GenericWebhookApiKeyAuthAPI key auth configuration for generic webhooks
GenericWebhookAuthMethodAuth method string literal ('hmac-sha256' | 'api-key')
TypeDescription
RuleRule definition returned by rule() / skip()
RuleCheckFn(ctx: RuleContext) => Promise<boolean> | boolean
RuleContextContext passed to rule check functions
RuleResultResult of rule evaluation (label, passed, duration)
EventPayloadDiscriminated union over event type (narrow on type for autocomplete)
RuleEvaluationResultResult of evaluateRules() (allPassed + results)
TypeDescription
MatrixUnion: StaticMatrixArray | StaticMatrixObject | DynamicMatrixFn
StaticMatrixArraystring[]
StaticMatrixObjectRecord<string, string[]>
DynamicMatrixFn(ctx) => Promise<StaticMatrixArray | StaticMatrixObject>
DynamicMatrixContextContext passed to dynamic matrix functions
MatrixValuesValues exposed to steps (value? + named dimensions)
MatrixIncludeRecord<string, string> — additional combinations
MatrixExcludeRecord<string, string> — removed combinations
TypeDescription
HookConfigHook definition returned by hook factories (onCancel(), etc.)
HookFnHook function type: (ctx: HookContext) => Promise<void>
HookInputHook input: HookFn | { run: HookFn; timeout?: number }
HookContextContext passed to hook functions
OutcomeMetadataMetadata about the outcome that triggered the hook
TypeDescription
DynamicJobFn(ctx) => Promise<Job[]>
DynamicJobContextContext for dynamic job generators
JobOrFactoryJob | DynamicJobFn
TypeDescription
StepContext<T>Context passed to step run functions
LoggerLogger interface (info, warn, error, debug)
WorkflowInfoWorkflow metadata: { name: string }
JobInfoJob metadata: { name: string, runsOn: string }
RepoInfoRepository metadata available in step context
StepSecretsAsync accessor interface for step secrets (get, expose, has)
StepSecretsTypedTyped step secrets with known key inference
KnownSecretKeysString literal union of declared secret context keys
SecretNotFoundErrorThrown when accessing a nonexistent key in secrets

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;
/** Typed inputs from dependency step outputs */
inputs: TInputs;
/** Current workflow metadata */
workflow: WorkflowInfo;
/** Current job metadata */
job: JobInfo;
/** Matrix values for current job instance (undefined without matrix) */
matrix?: MatrixValues;
/** 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 test` (remote test run) */
isTestRun: boolean;
/** The resolved deployment environment name for this job (undefined without environment) */
environment?: string;
/** Flat secrets resolved for this job. Throws SecretNotFoundError on missing key. */
secrets: StepSecrets;
/** Emit a custom event that can trigger other workflows */
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 */
jobOutputs(ref: { name: string }): Record<string, unknown>;
/** Publish a secret output value from this job (encrypted before leaving the agent) */
setSecretOutput(key: string, value: string): void;
}
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..."`;
});

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 from ctx.$, and any direct console.log / .error / .warn / .info / .debug (or other library that writes to process.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 check functions, and in the workflow concurrency.group function — captured to the workflow-level prepare log bucket for the job, alongside KiCI’s own setup narration.
  • Inside a dynamic environment / env / concurrencyGroup function on a static job — captured to the __init__ job’s synthetic step-0 log, which appears in the timeline as “Init: jobname”.
  • Inside a DynamicJobFn body and the per-generated-job environment / env / concurrencyGroup / matrix functions — 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, so await $\…“ 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).

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)

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 — append KEY=value lines. Each becomes an environment variable visible to subsequent steps, exactly like ctx.setEnv('KEY', 'value').
  • $KICI_PATH — append one directory per line. Each is prepended to PATH for subsequent steps, exactly like ctx.addPath(dir). The first directory appended ends up first on PATH.
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=value per 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.

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

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 the build platform from the run context — a step cannot spoof them. Use it to authenticate the build to an external service that trusts the platform’s OIDC issuer (for example, when generating build provenance).

const publish = job('publish', {
steps: [
step('mint', async (ctx) => {
const { token, expiresIn } = await ctx.kici.oidc.token({ audience: 'sigstore' });
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 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 platform credentials — the request is relayed through the orchestrator, which mints the token on the step’s behalf.
  • Only available inside a running job step; calling it outside one (for example, during local execution) rejects with a clear error.

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 the platform — not from the step — so it cannot be spoofed, signs it, 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 a platform-minted identity token, so it is offline-verifiable against the platform’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-attestation can retrieve it.
  • The returned { storageKey, subjectDigest, bundleMediaType } identifies the stored bundle.
  • Only available inside a running job step; calling it outside one (for example, during local execution) rejects with a clear error.

See the build provenance guide for the end-to-end attest → verify → view journey, including how to verify a bundle with kici verify-attestation.

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.

Each job picks its secret environment via the environment option on job(). The orchestrator resolves the environment’s scoped-secret store at dispatch time, evaluates access rules, and sends the decrypted secrets to the agent:

const deploy = job('deploy', {
runsOn: 'linux',
environment: 'production',
steps: [
/* ... */
],
});
export default workflow('deploy', {
on: push({ branches: 'main' }),
jobs: [deploy],
});

environment accepts either a static string or an async function (event) => string | Promise<string> for dynamic resolution at trigger-evaluation time. The resolved environment’s secrets are flattened into ctx.secrets.

ctx.secrets provides flat access to the secrets resolved for the job’s environment.

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.

import { workflow, job, step, push } from '@kici-dev/sdk';
const deploy = job('deploy', {
runsOn: 'linux',
environment: '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],
});
  • 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.

When running kici test, you can provide secrets locally without an orchestrator.

Create a .kici/.secrets file in your project (auto-gitignored by kici init):

# Flat secrets (before any section)
DEPLOY_TOKEN=my-deploy-token
API_KEY=my-api-key
# Context-scoped secrets
[production]
DB_PASSWORD=prod-secret
API_KEY=prod-key
[npm-publish]
NPM_TOKEN=npm-abc123

Lines 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).

Override or supplement file-based secrets with CLI flags:

Terminal window
# Inject flat secrets (repeatable)
kici test push --secret DEPLOY_TOKEN=my-token --secret API_KEY=my-key
# Inject context-scoped secrets (repeatable)
kici test push --context production.DB_PASSWORD=prod-secret --context npm-publish.NPM_TOKEN=abc123

Precedence: 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).

Test fixtures define event replicas for kici run remote. They simulate trigger events without requiring real webhooks.

function fixture(
id: string,
options: FixtureOptions | (() => FixtureOptions | Promise<FixtureOptions>),
): Fixture;

Parameters:

  • id — unique fixture name (no whitespace). Used in kici run remote <id>.
  • options — a FixtureOptions object, or an async factory function returning one.
import { fixture, push } from '@kici-dev/sdk';
export const pushMain = fixture('push-main', {
event: push({ branches: ['main'] }),
});
PropertyTypeDescription
eventTriggerConfigThe trigger event to simulate (required)
branchstringOverride branch name (defaults to git-detected)
shastringOverride commit SHA (defaults to HEAD)
repostringOverride repository (defaults to git-detected)
prnumberFor PR events, override PR number
secretsRecord<string, string>Secret context mappings: { localName: 'remote-context' }
workflowNamestringBypass trigger matching and run this workflow directly

Options can also be provided as an async factory function for dynamic fixture generation.