Pipeline definition
Describes the JSON that defines a pipeline, with every key, trigger and expression.
Last updated
On this page
A complete example
This definition has all three triggers, two inputs, a matrix, a cloned environment, and jobs that clone repositories with permissions.git: read.
{ "name": "ci", "environment": { "clone": "staging" }, "triggers": { "schedule": ["0 3 * * 1-5"], "webhook": true, "after": { "pipeline": "build", "result": "success" } }, "inputs": { "target": { "options": ["staging", "prod"], "default": "staging" }, "note": { "default": "" } }, "jobs": { "lint": { "packages": ["git", "[email protected]", "[email protected]"], "permissions": { "git": "read" }, "steps": [ { "run": "git clone --branch main https://github.com/acme/payments workspace/payments" }, { "run": "cargo clippy --workspace", "cwd": "workspace/payments" } ] }, "test": { "needs": ["lint"], "if": "always() && needs.lint.result == 'success'", "packages": ["git", "[email protected]", "[email protected]"], "matrix": { "db": ["postgres16", "postgres17"] }, "timeoutMinutes": 30, "permissions": { "registry": "push", "git": "read" }, "secrets": ["DEPLOY_TOKEN"], "env": { "DB": "${{ matrix.db }}" }, "steps": [ { "run": "git clone https://github.com/acme/payments workspace/payments" }, { "run": "git clone https://github.com/acme/shared-config config" }, { "name": "test", "run": "cargo test --workspace", "cwd": "workspace/payments", "env": { "CI": "1", "TARGET": "${{ inputs.target }}", "NOTE": "${{ webhook.body.note }}" } } ] } }}Rules for the whole document
A definition is camelCase JSON. Every object refuses an unknown key: a misspelled need or timeout is an error that names the key. A save checks the whole definition and reports every problem at once, each with its job and step.
| Limit | Value |
|---|---|
| Size of the canonical definition | 64 KiB |
| Jobs in a pipeline | 50 |
| Steps in a job | 100 |
| Pipelines for one owner | 50 |
| Versions of one pipeline | 100 |
The platform stores the definition in a canonical form. It keeps the order of jobs, inputs and matrix as you wrote them, sorts other maps and leaves out every default. Two spellings of one definition give the same hash, so a save of an equal definition makes no new version.
Pipeline keys
| Key | Type | Default | Rule |
|---|---|---|---|
name | string | none; required | [a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?, at most 63 characters. Unique for the owner |
environment | name or {clone: name} | none | The default environment for every job that names none. Interpolated |
triggers | object | none | What starts a run besides a person, see Triggers |
inputs | mapping of name to input | {} | The values a run can take, see Inputs |
jobs | mapping of name to job | none; required | At least one job. Listed in the order written |
Triggers
A person can always start a run with the API, the CLI or an SDK, so there is no manual key. The keys of triggers add three more ways to start a run. A run from any of them uses the newest version at the time it starts.
| Key | Type | Rule |
|---|---|---|
schedule | list of strings | Five-field cron lines in UTC, see Cron. The run has event schedule |
webhook | boolean | true gives the pipeline a secret URL. A call to it starts a run with event webhook. The secret is not part of the definition |
after | {pipeline, result} | Starts a run when a run of another pipeline of the same owner ends. result is success, failure or cancelled; absent means any end. The run has event pipeline |
A run from a trigger takes the default of every input. A definition with a trigger and a required input that has no default is refused, because no person is there to give a value.
A pipeline cannot follow itself. A chain of after triggers stops at a depth of 5, and the finishing run records the refusal. The target of after must exist when you save.
Webhook
The first save with "webhook": true makes the secret and returns the URL once. A save with "webhook": false deletes the secret. To replace the secret, rotate it, and the old URL stops at once. The call needs no token, so keep the URL secret.
A JSON body of at most 256 KiB is available as webhook.body. A body that is not JSON runs with no body. A pipeline takes 30 webhook calls in one minute. See the webhook route.
Cron
Five fields, minute hour day-of-month month weekday, in UTC. Each field is a comma list of *, N, A-B, any of those with /STEP, and N/STEP for N to the field's maximum.
| Field | Range |
|---|---|
| minute | 0-59 |
| hour | 0-23 |
| day of month | 1-31 |
| month | 1-12 |
| weekday | 0-7, where both 0 and 7 are Sunday |
There are no month or day names, no @daily macros, no seconds field and no L, W or #. When both day of month and weekday are restricted, a time matches if either does: 0 0 1 * 1 is the 1st and every Monday. A tick that the platform misses while it is down is dropped, not replayed.
Inputs
Each input is {options, default, required}. Every value is text: a 3 or true that you send is the text you wrote. Names follow [A-Za-z_][A-Za-z0-9_-]{0,39}.
| Key | Type | Default | Rule |
|---|---|---|---|
options | list of strings | none | The only accepted values. An empty list is an error, and so is a default outside the list |
default | string | none | Used when the run gives no value |
required | boolean | false | A run that gives no value and has no default is refused |
A run can give only inputs the pipeline declares. A declared input with no value and no default reads as the empty string. Inside the pipeline, read inputs as ${{ inputs.name }}.
Environments
environment is a name, or {clone: name}, on the pipeline and on any job. A job's own value overrides a run request's, which overrides the pipeline's.
| Form | Meaning |
|---|---|
"staging" | The shared environment. Every job uses the one that exists |
{"clone": "staging"} | A copy made for this run |
A clone is made once per run for each distinct clone target, named ci-{run}-{n}, and deleted when the run ends. Jobs with the same target share the one clone. Both forms can carry ${{ }}. Neither form can be empty. {clone: ...} takes no other key. The jobs of a run reach their environment through a hidden session the run creates, see Sessions.
Jobs
Each job is one sandbox. Its steps run in it in order, as one script. A failing step stops the job.
| Key | Type | Default | Rule |
|---|---|---|---|
needs | list of strings | [] | Jobs that must finish first. Each must exist in this definition; a cycle is an error |
if | string | success() | Evaluated just before the job starts. Bare or inside one ${{ }}. See Expressions |
packages | list of strings | [] | Nix packages on the platform base image, attr or attr@version. Each entry can be interpolated |
image | string | none | A container image instead of the platform image. Interpolated |
fromSnapshot | string | none | The home of the job starts as a copy of this reference: a volume name, name@N or a snapshot id. Interpolated. Combines with packages or image |
saveTo | string | none | A volume name. When the job succeeds, its home is saved as the next version of the volume. A literal, not allowed with matrix. See Save the home |
environment | name or {clone: name} | the pipeline's | Where the job runs |
matrix | mapping of axis to list of strings | none | The job runs once per combination. Axis names: [A-Za-z_][A-Za-z0-9_-]{0,39}. An empty axis, a value listed twice, or more than 64 combinations is an error |
timeoutMinutes | integer | 60 | 1 to 1440 |
permissions | {registry: "push", git: "read"} | none | See Permissions |
env | mapping of name to string | {} | Names: [A-Za-z_][A-Za-z0-9_]*. Values are interpolated, except a value that is exactly ${{ secrets.NAME }}, which is a secret and is not interpolated |
secrets | list of names | [] | Secrets of the pipeline owner (the team, for a team) for every step: secrets: ["TOKEN"] is env: { "TOKEN": "${{ secrets.TOKEN }}" }. Each secret must exist when you save |
steps | list of steps | none; required | At least one |
packages and image are exclusive: a job uses one. With none of them a job gets the platform base image with the package git. Job names follow [a-z0-9][a-z0-9-]{0,39}.
One more key is accepted and validated and does nothing yet: artifacts (list of paths).
Save the home
saveTo names a volume of the pipeline owner. fromSnapshot and saveTo name the same volume when both are set. A job with saveTo and no fromSnapshot starts from the newest version of the volume, or empty when the volume does not exist yet. An unpinned name is read when the job starts.
Two jobs that save to one name need an order in needs. A save counts one snapshots unit of the quota. A job that fails saves nothing. The run shows home and saved for each job. See Home and the save checks.
Permissions
| Key | Value | Rule |
|---|---|---|
registry | push, the only value | Before the first step, the platform runs docker login for each container registry of the pipeline owner. KL_REGISTRY holds the first registry and its namespace, and KL_REGISTRY_USER its user. The image of the job needs docker |
git | read, the only value | git clone and git fetch over HTTPS on github.com and gitlab.com get a short-lived, read-only credential from a connector of the pipeline owner. See Code in a job |
{ "name": "web-tests", "jobs": { "test": { "permissions": { "git": "read" }, "steps": [ { "run": "git clone https://github.com/acme/web" }, { "run": "make test", "cwd": "web" } ] } }}A job with git: read can read every repository the GitHub App is installed on, a webhook run included. A GitLab credential reads only the one project that the connector picked. A job env that sets GIT_CONFIG_COUNT replaces the platform's credential helper.
Matrix
The first axis varies slowest, each axis in the order written. {db: [a, b], os: [x, y]} makes (a, x) (a, y) (b, x) (b, y). An instance is named test (a, x), and a job with no matrix is named by its job name alone. needs.test.result reads one result for the whole job: failure if any instance failed, else cancelled if any was cancelled, else success if any succeeded, else skipped.
Steps
A step is one shell command. run is required: a step without it is refused with a step needs `run` .
| Key | Type | Default | Rule |
|---|---|---|---|
name | string | the first line of run (60 characters at most) | What the run shows for the step |
run | string | none; required | One shell string, run with sh -e -c, so the first failing command ends the step. Interpolated |
env | mapping of name to string | {} | Layered over the job's env. Values are interpolated, except a ${{ secrets.NAME }} reference |
cwd | string | the home directory | Relative to the home (~), confined there. Interpolated |
Variables that the platform sets
Every step also gets these variables. A job or step env entry with the same name replaces it.
| Variable | Value |
|---|---|
CI | true |
KL_CI | 1 |
KL_PIPELINE | The name of the pipeline |
KL_RUN_ID, KL_RUN_NUMBER | The id and the number of the run |
KL_JOB | The key of the job, for example test (postgres16) |
KL_REGISTRY, KL_REGISTRY_USER | Only with permissions.registry. See Registry push |
HF_TOKEN | The HF_TOKEN secret of the owner, when the owner has one and the job does not set the name. See Hugging Face token |
KL_GIT_TOKEN | Only with permissions.git. The token that the git credential helper shows to Kloudlite. The log shows it as *** |
GIT_CONFIG_COUNT, GIT_CONFIG_KEY_0, GIT_CONFIG_VALUE_0, GIT_CONFIG_KEY_1, GIT_CONFIG_VALUE_1 | Set credential.helper to the Kloudlite helper and credential.useHttpPath to true |
GIT_TERMINAL_PROMPT | 0, so git fails at once when it has no credential |
KL_API_URL | The address of the Kloudlite API that the helper calls |
Expressions
${{ ... }} interpolates into the fields marked interpolated above. if takes the expression bare or wholly inside one ${{ }}.
| Name | Value |
|---|---|
event | manual, schedule, webhook or pipeline |
run.id, run.number | The run's id and number |
inputs.NAME | A run input |
matrix.NAME | This instance's value |
env.NAME | The environment as layered so far. Readable in steps only |
needs.JOB.result | success, failure, skipped or cancelled, for a job this one lists in needs |
webhook.body.PATH | A field of the JSON body of the webhook call, with dots for nesting. A missing field is the empty string |
The body of a webhook call is text that the sender controls. For that reason it reaches a script only through an environment variable. Read it in env and use "$NAME" in run. A run, cwd, image, packages or environment that reads webhook.body is an error at save. A run or cwd that reads ${{ env.NAME }} is also an error when NAME gets its value from webhook.body.
Where each name is available:
| Field | inputs | matrix | needs | env | webhook.body |
|---|---|---|---|---|---|
pipeline environment | declared | no | no | no | no |
job environment, image, fromSnapshot, packages | declared | the job's axes | no | no | no |
job if, job env | declared | the job's axes | the job's needs | no | yes |
a step's env | declared | the job's axes | the job's needs | yes | yes |
a step's run, cwd | declared | the job's axes | the job's needs | yes | no |
Reading a name that does not exist, such as inputs.nope, is an error. A save finds it where it can, and otherwise the job fails.
Literals are 'single' strings ('' for a quote inside), "double" strings with no escapes, numbers, true and false. Operators, tightest first: !, == and !=, &&, ||, and parentheses. Every value is text. == is case-sensitive, except that a number compares numerically with text that parses as one, and true or false compares with that text. Truthy is true, a non-empty string or a non-zero number.
The functions success(), failure(), always() and cancelled() work in if only. An if that calls none of them is success() && (the condition), so a job whose needs failed does not run unless its if says so, for example with always(). There is no indexing, no string function and no arithmetic; a step has a shell for those.