Skip to content
DocsAll docs
Console

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.

json
{  "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.

LimitValue
Size of the canonical definition64 KiB
Jobs in a pipeline50
Steps in a job100
Pipelines for one owner50
Versions of one pipeline100

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

KeyTypeDefaultRule
namestringnone; required[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?, at most 63 characters. Unique for the owner
environmentname or {clone: name}noneThe default environment for every job that names none. Interpolated
triggersobjectnoneWhat starts a run besides a person, see Triggers
inputsmapping of name to input{}The values a run can take, see Inputs
jobsmapping of name to jobnone; requiredAt 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.

KeyTypeRule
schedulelist of stringsFive-field cron lines in UTC, see Cron. The run has event schedule
webhookbooleantrue 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.

FieldRange
minute0-59
hour0-23
day of month1-31
month1-12
weekday0-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}.

KeyTypeDefaultRule
optionslist of stringsnoneThe only accepted values. An empty list is an error, and so is a default outside the list
defaultstringnoneUsed when the run gives no value
requiredbooleanfalseA 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.

FormMeaning
"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.

KeyTypeDefaultRule
needslist of strings[]Jobs that must finish first. Each must exist in this definition; a cycle is an error
ifstringsuccess()Evaluated just before the job starts. Bare or inside one ${{ }}. See Expressions
packageslist of strings[]Nix packages on the platform base image, attr or attr@version. Each entry can be interpolated
imagestringnoneA container image instead of the platform image. Interpolated
fromSnapshotstringnoneThe 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
saveTostringnoneA 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
environmentname or {clone: name}the pipeline'sWhere the job runs
matrixmapping of axis to list of stringsnoneThe 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
timeoutMinutesinteger601 to 1440
permissions{registry: "push", git: "read"}noneSee Permissions
envmapping 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
secretslist 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
stepslist of stepsnone; requiredAt 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

KeyValueRule
registrypush, the only valueBefore 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
gitread, the only valuegit 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
json
{  "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` .

KeyTypeDefaultRule
namestringthe first line of run (60 characters at most)What the run shows for the step
runstringnone; requiredOne shell string, run with sh -e -c, so the first failing command ends the step. Interpolated
envmapping of name to string{}Layered over the job's env. Values are interpolated, except a ${{ secrets.NAME }} reference
cwdstringthe home directoryRelative 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.

VariableValue
CItrue
KL_CI1
KL_PIPELINEThe name of the pipeline
KL_RUN_ID, KL_RUN_NUMBERThe id and the number of the run
KL_JOBThe key of the job, for example test (postgres16)
KL_REGISTRY, KL_REGISTRY_USEROnly with permissions.registry. See Registry push
HF_TOKENThe 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_TOKENOnly 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_1Set credential.helper to the Kloudlite helper and credential.useHttpPath to true
GIT_TERMINAL_PROMPT0, so git fails at once when it has no credential
KL_API_URLThe 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 ${{ }}.

NameValue
eventmanual, schedule, webhook or pipeline
run.id, run.numberThe run's id and number
inputs.NAMEA run input
matrix.NAMEThis instance's value
env.NAMEThe environment as layered so far. Readable in steps only
needs.JOB.resultsuccess, failure, skipped or cancelled, for a job this one lists in needs
webhook.body.PATHA 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:

Fieldinputsmatrixneedsenvwebhook.body
pipeline environmentdeclarednononono
job environment, image, fromSnapshot, packagesdeclaredthe job's axesnonono
job if, job envdeclaredthe job's axesthe job's needsnoyes
a step's envdeclaredthe job's axesthe job's needsyesyes
a step's run, cwddeclaredthe job's axesthe job's needsyesno

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.