Jobs and steps
Order the jobs of a pipeline, add conditions and a matrix, choose a runtime, and start from or save a named volume.
Last updated
On this page
Description and operation
General
A job is one unit of work in its own sandbox. Its steps run one after the other in that sandbox, so the files of one step are there for the next. Files do not pass from one job to another directly. A job can save its home as a version of a named volume, and a later job can start from it. See Home.
| Key | What it sets | See |
|---|---|---|
needs | The jobs that must end before this job starts. | Order and parallel jobs |
if | A condition that decides if the job runs. | Conditions |
matrix | One job for each combination of values. | Matrix |
packages, image | What the sandbox holds. | Runtime |
fromSnapshot, saveTo | The files that the job starts with, and the volume that it saves to. | Home |
timeoutMinutes | The time limit of the job, 1 to 1440 minutes. The default is 60. | Runtime |
env | Variables for every step of the job. | Variables |
steps | The commands of the job, at least one. | Steps |
A definition has these limits.
| Limit | Value |
|---|---|
| Size of the definition | 64 KiB |
| Jobs in a pipeline | 50 |
| Steps in a job | 100 |
| Job instances in a run, matrix included | 512 |
| Characters in a job name | 1 to 40, from a-z, 0-9 and - |
To do these tasks with an AI agent, see Agentic workflows – General.
Order and parallel jobs
The jobs of a pipeline form a graph. A job with no needs starts at once. A job with needs starts when each job that it names has ended. Jobs that do not need each other run in parallel, up to 8 jobs of a run at the same time. A needs list that makes a cycle is refused at save.
| Definition | Order |
|---|---|
a, b, with no needs | a and b run in parallel. |
b needs a | b starts when a has ended. |
c needs a and b | c starts when both have ended. |
Conditions
The platform evaluates the if of a job when the job is about to start. Without if, a job runs only when every job in its needs succeeded. An if that calls no status function adds that rule, so inputs.target == 'prod' runs only after successful needs.
| Function | True when |
|---|---|
success() | Every needed job succeeded and the run is not cancelled. This is the default. |
failure() | A needed job failed. |
always() | Always, unless the run is cancelled. Use it for a job that reports or cleans up after a failure. |
cancelled() | The run is being cancelled. |
When a run is cancelled, the platform starts no more jobs. It does not evaluate if, so always() jobs do not start.
A condition reads inputs.NAME, matrix.NAME, needs.JOB.result, event and webhook.body.PATH. The operators are !, ==, !=, &&, || and parentheses. The rules are in Expressions.
Matrix
A matrix maps each axis to a list of values. The job runs once for each combination, up to 64. The first axis varies slowest. Each instance has the key job (value, value), for example test (python311). A job that needs a matrix job waits for all its instances. needs.test.result is failure if any instance failed.
The values of an axis must be different from each other.
Runtime
A job uses one of two runtimes. With none, the job runs on the platform base image with the package git.
| Key | The sandbox holds |
|---|---|
packages | The platform base image with these nixpkgs packages, for example ["python3", "curl"]. |
image | A container image, for example python:3.12-slim. |
Each sandbox has access to the internet. A job that passes timeoutMinutes fails with timed out. Each step has the same limit by itself. The log of a job is cut after 8 MiB.
Home
The home of a job is the folder /home/kl. It is the disk of the job sandbox. By default it is empty. fromSnapshot and saveTo set what the home starts with, and where it goes when the job succeeds. Both combine with packages or image.
A reference is name, name@N or a snapshot id. go-base is the newest ready version of the volume go-base. go-base@3 is version 3. See Volumes.
fromSnapshot | saveTo | The home starts as |
|---|---|---|
| none | none | Empty. |
| A reference | none | A copy of the snapshot. |
| none | go-base | A copy of the newest version of go-base. Empty when go-base does not exist yet. |
go-base or go-base@3 | go-base | The newest version, or version 3, of go-base. |
A job with saveTo saves its home as the next version of the volume when it succeeds. A job that fails, times out or is cancelled saves nothing. The job is success only when the save is ready. When the save fails, the job fails with the reason, and the jobs that need it are skipped.
A name that exists but has no ready version fails the job. Only a name that is not made yet starts empty.
The platform finds an unpinned name when the job starts, not when the run is planned. A job that needs the saving job therefore reads the version that the saving job wrote.
Rules for the home:
saveTois a volume name. It is a literal: it has no${{ }}.- A job with
saveTohas nomatrix. fromSnapshotandsaveToname the same volume. A snapshot id must be a push of that volume.- Two jobs that save to one name must be ordered with
needs. - A name belongs to the owner of the pipeline. A team pipeline reads and writes the volumes of the team. A job never reads the volume of another owner.
- A volume that is an environment volume is not a start point or a save target.
- A save counts one
snapshotsunit of the quota of the owner. The job fails at start when the quota has no free unit. - To write
saveTo, you need the same role as to save the pipeline.
The run records the version that each job used and the version that it wrote. The home and saved fields of a job show them. To run a job again from the same version, pin it with an input: fromSnapshot: "${{ inputs.base }}".
Steps
A step has a run command, an optional name, env and cwd. The platform runs each run with sh -e -c, so the first command that fails ends the step and the job. A step starts in the home directory. cwd is a folder relative to the home. The log of the job shows ▶ and the step name before the output of each step. A step with no name shows the first line of its run.
Each step runs in its own subshell. Only files pass to the next step. A cd or an export does not.
Variables
Step env values are added to the job env, and a step value replaces a job value of the same name. A value can read ${{ inputs.NAME }}, ${{ matrix.NAME }} and ${{ needs.JOB.result }}. A step run, cwd and env can also read ${{ env.NAME }}. The fields image, packages and fromSnapshot cannot read needs. The platform also sets these variables in every step.
| 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 (python311). |
For secrets in variables, see Secrets, registries and environments.
Tasks
Run jobs in parallel and then in order
Reason for the task: Do independent work at the same time, then run one job that uses all the results.
Job set-up
- Access: you. For a team, an admin, and an active member to start the run.
- References: Pipeline definition – Jobs, Create a pipeline.
Procedure
Write this definition to
site-check.json. The jobsapianddocsrun in parallel, andsummaryruns after both.{ "name": "site-check", "jobs": { "api": { "packages": ["curl"], "steps": [{ "name": "probe", "run": "curl -fsS -o /dev/null https://api.github.com" }] }, "docs": { "packages": ["curl"], "steps": [{ "name": "probe", "run": "curl -fsS -o /dev/null https://docs.github.com" }] }, "summary": { "needs": ["api", "docs"], "if": "always()", "steps": [ { "name": "report", "run": "echo \"api=$API docs=$DOCS\"", "env": { "API": "${{ needs.api.result }}", "DOCS": "${{ needs.docs.result }}" } } ] } }}Create the pipeline from the file, as in Create a pipeline.
Start a run and wait for it to end.
run = kl.pipelines.run(owner="acme", pipeline="site-check")print(run.wait(), [(j["key"], j["state"]) for j in run.jobs])const run = await kl.pipelines.run({ owner: "acme", pipeline: "site-check" });console.log(await run.wait(), run.jobs.map((j) => [j.key, j.state]));run, err := kl.Pipelines.Run(kloudlite.RunPipeline{Owner: "acme", Pipeline: "site-check"})if err != nil { log.Fatal(err)}state, err := run.Wait(kloudlite.WaitRun{})if err != nil { log.Fatal(err)}fmt.Println(state)for _, j := range run.Jobs { fmt.Println(j.Key, j.State)}run = kl.pipelines.run(owner: "acme", pipeline: "site-check")puts run.waitrun.jobs.each { |j| puts "#{j["key"]} #{j["state"]}" }var run = kl.pipelines().run(new Kloudlite.RunPipeline().owner("acme").pipeline("site-check"));System.out.println(run.waitFor());for (var j : run.jobs()) System.out.println(j.get("key") + " " + j.get("state"));RUN=$(kloudlite pipelines run acme/site-check --json | jq -r .id)kloudlite pipelines wait-run acme site-check "$RUN"kloudlite pipelines get-run acme site-check "$RUN"
Close-out
- Make sure that
apianddocshave closestartedAtMsvalues, and thatsummarystarted after both ended. - Read the log of
summary. It shows the result of each needed job.
Run a job only on a condition
Reason for the task: Do a step of work only for some runs, for example only for the production target.
Job set-up
- Access: you. For a team, an admin.
- Conditions: you have the definition of task 001.
- References: Conditions, Inputs.
Procedure
Add the input
targetand the jobannouncetosite-check.json.{ "name": "site-check", "inputs": { "target": { "options": ["staging", "prod"], "default": "staging" } }, "jobs": { "api": { "packages": ["curl"], "steps": [{ "name": "probe", "run": "curl -fsS -o /dev/null https://api.github.com" }] }, "announce": { "needs": ["api"], "if": "inputs.target == 'prod'", "steps": [{ "name": "announce", "run": "echo \"production check passed\"" }] } }}Send the definition as the next version. See Save a new version.
Start a run with
targetset toprod.run = kl.pipelines.run(owner="acme", pipeline="site-check", inputs={"target": "prod"})print(run.wait())const run = await kl.pipelines.run({ owner: "acme", pipeline: "site-check", inputs: { target: "prod" } });console.log(await run.wait());run, err := kl.Pipelines.Run(kloudlite.RunPipeline{Owner: "acme", Pipeline: "site-check", Inputs: map[string]string{"target": "prod"}})if err != nil { log.Fatal(err)}state, err := run.Wait(kloudlite.WaitRun{})if err != nil { log.Fatal(err)}fmt.Println(state)run = kl.pipelines.run(owner: "acme", pipeline: "site-check", inputs: { "target" => "prod" })puts run.waitvar run = kl.pipelines().run(new Kloudlite.RunPipeline().owner("acme").pipeline("site-check").input("target", "prod"));System.out.println(run.waitFor());RUN=$(kloudlite pipelines run acme/site-check --input target=prod --json | jq -r .id)kloudlite pipelines wait-run acme site-check "$RUN"
Close-out
- Make sure that
announceissuccessforprod. - Start a run with no inputs. Make sure that
announceisskipped, because the default target isstaging.
Run a job for each value of a matrix
Reason for the task: Run the same work on several versions of a tool, or on several data sets, in parallel.
Job set-up
- Access: you. For a team, an admin.
- References: Matrix, Pipeline definition – Matrix.
Procedure
Write this definition to
python-check.json. The axispythonnames a nixpkgs package for each instance.{ "name": "python-check", "jobs": { "test": { "matrix": { "python": ["python311", "python312"] }, "packages": ["${{ matrix.python }}"], "steps": [{ "name": "version", "run": "python3 --version" }] } }}Create the pipeline from the file, and start a run. See Start a run and follow it.
Read the log of one instance. Use the key of the instance as the job.
print(run.logs(job="test (python312)"))console.log(await run.logs({ job: "test (python312)" }));out, err := run.Logs("test (python312)")if err != nil { log.Fatal(err)}fmt.Print(out)puts run.logs(job: "test (python312)")System.out.print(run.logs("test (python312)"));kloudlite pipelines logs acme python-check "$RUN" "test (python312)"
Close-out
- Make sure that the run lists the jobs
test (python311)andtest (python312). - Make sure that each log shows the matching Python version.
Set the runtime and the time limit of a job
Reason for the task: Run a job in a container image of your choice, and stop it when it takes too long.
Job set-up
- Access: you. For a team, an admin.
- References: Runtime.
Procedure
Give the job an
imageand atimeoutMinutes. Use only one ofpackagesandimage.{ "name": "image-check", "jobs": { "check": { "image": "python:3.12-slim", "timeoutMinutes": 30, "steps": [{ "name": "version", "run": "python -c 'import sys; print(sys.version)'" }] } }}Create the pipeline from the file, and start a run. See Start a run and follow it.
Close-out
- Read the log of
check. Make sure that it shows Python 3.12.
Save the home of a job
Reason for the task: Keep the files of a job as a version of a volume. Start a later job or a later run from them.
Job set-up
- Access: you. For a team, an admin.
- References: Home, Volumes, Pipeline definition – Jobs.
Procedure
Write this definition to
go-base.json. The jobbasesaves its home togo-base; the jobbuildstarts from it.{ "name": "go-base", "jobs": { "base": { "packages": ["go"], "saveTo": "go-base", "steps": [{ "name": "fetch", "run": "mkdir -p cache && date > cache/stamp" }] }, "build": { "needs": ["base"], "fromSnapshot": "go-base", "steps": [{ "name": "read", "run": "cat cache/stamp" }] } }}Create the pipeline from the file, and start a run. See Start a run and follow it.
Show the versions of the volume.
for v in kl.volumes.versions(owner="acme", name="go-base"): print(v["version"], v["phase"], v["saved_by"])console.log(await kl.volumes.versions({ owner: "acme", name: "go-base" }));versions, err := kl.Volumes.Versions("acme", "go-base")if err != nil { log.Fatal(err)}fmt.Println(versions)puts kl.volumes.versions(owner: "acme", name: "go-base")System.out.println(kl.volumes().versions("acme", "go-base"));kloudlite volumes versions acme/go-base
Close-out
- Make sure that each run adds a version. Make sure that
saved_bynames the jobbase. - Read the log of
build. Make sure that it shows the date thatbasewrote. - Make sure that the run shows
savedonbaseandhomeonbuildwith the same version.
Troubleshooting
| Symptom | Possible cause | Corrective action |
|---|---|---|
422 with `needs` cycle: a → b → a | Jobs need each other in a loop. | Remove one of the needs. |
422 with job summary: `needs` names `test`, which is not a job here | A needs entry names a job that does not exist. | Correct the name, or add the job. |
422 with `packages` and `image` are exclusive; a job uses one | The job sets both runtimes. | Keep one of packages and image. |
422 with the matrix makes 80 instances; the limit is 64 | The matrix has too many combinations. | Remove values, or split the job. |
422 with `matrix.python` is empty, so the job would never run | An axis has no values. | Give the axis at least one value, or remove it. |
422 with `timeoutMinutes`: 0 is outside 1-1440 | The time limit is out of range. | Use a value from 1 to 1440. |
422 with `if`: … | The condition does not parse, or reads a name that is not available. | Correct the condition. See Expressions. |
A job is skipped | A needed job did not succeed, or the if was false. | Add always() to if to run it after a failure. |
422 with job base: saveTo Go: … | The name breaks the name rule. | Use 1 to 40 characters of a-z, 0-9 and -. |
422 with job base: saveTo is a literal volume name | saveTo has ${{ }}. | Write the name. |
422 with job base: saveTo cannot be used with matrix | The job has a matrix. | Remove the matrix, or give each instance its own job. |
422 with job base: fromSnapshot and saveTo name different volumes (a, b) | A job reads one volume and saves another. | Use the same name in both keys. |
422 with jobs a and b both save to go-base and neither needs the other | The order of the two saves is not fixed. | Add needs from one job to the other. |
422 with job build: volume go-base does not exist and no job before it saves to it | fromSnapshot names a volume that does not exist, and no earlier job makes it. | Push the volume first, or add saveTo to a job that this job needs. |
422 with job build: volume go-base has no version 9 | The pinned version does not exist. | Show the versions and pin one of them. |
422 with job build: go-base is an environment volume; a job starts only from a workspace volume | The name belongs to an environment volume. | Use a workspace volume. |
422 with job build: ws-… is not a push | fromSnapshot is a sync point. | Use a snapshot id from the history, or a name. |
A job fails with fromSnapshot go-base: volume go-base does not exist | The volume was deleted, or a condition skipped the job that makes it. | Run the saving job first, or push the volume. |
A job fails with fromSnapshot go-base@9: volume go-base has no version 9 | The version was deleted after the run started. | Pin a version that exists. |
A job fails with saveTo go-base: volume go-base was created by another run while this job ran; nothing was saved | Two runs started with no go-base, and the other run saved first. | Start the run again. It now starts from the volume. |
A job fails with saveTo go-base: the save failed | The platform could not cut the snapshot. | Start the run again. If it fails again, read the error rows. |
A job fails with saveTo go-base: the save did not finish within 5 minutes | The snapshot was not ready in time. The platform deletes it. | Start the run again. |
A job fails with fromSnapshot go-base: volume go-base has no ready version; delete that volume or push to it | Every push of go-base failed, or was deleted or expired. A save never moves a name. | Delete the volume go-base, or push to it. |
A job fails with the job's sandbox does not save to go-base; nothing was saved | The sandbox does not belong to a job that saves to that name. | Start the run again. |
A job fails with the job's sandbox was saved by another job; nothing was saved | Another job already saved this sandbox. | Start the run again. |
A job fails with the job's sandbox is gone; nothing was saved | The sandbox ended before the save. | Start the run again. |
A job fails with snapshots: 20 of 20 in use; request more under Quota | The owner has no free snapshots unit for the save. | Delete a version, or request more quota. |
A job fails with timed out | The job ran longer than timeoutMinutes. | Set a larger timeoutMinutes and save a new version. |
A job fails with `probe` exited 6 | A command of the step exited with a code other than 0. | Read the log of the job and correct the command. |
A job fails with its sandbox did not start within 10 minutes | The image is large or does not exist, or no capacity is free. | Check the image name. Start the run again later. |
A job fails with could not create its sandbox: … | The owner has no quota for one more sandbox, or a setting is not valid. | Read the text after the colon. See Quota and requests. |