Skip to content
DocsAll docs
Console

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.

KeyWhat it setsSee
needsThe jobs that must end before this job starts.Order and parallel jobs
ifA condition that decides if the job runs.Conditions
matrixOne job for each combination of values.Matrix
packages, imageWhat the sandbox holds.Runtime
fromSnapshot, saveToThe files that the job starts with, and the volume that it saves to.Home
timeoutMinutesThe time limit of the job, 1 to 1440 minutes. The default is 60.Runtime
envVariables for every step of the job.Variables
stepsThe commands of the job, at least one.Steps

A definition has these limits.

LimitValue
Size of the definition64 KiB
Jobs in a pipeline50
Steps in a job100
Job instances in a run, matrix included512
Characters in a job name1 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.

DefinitionOrder
a, b, with no needsa and b run in parallel.
b needs ab starts when a has ended.
c needs a and bc 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.

FunctionTrue 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.

KeyThe sandbox holds
packagesThe platform base image with these nixpkgs packages, for example ["python3", "curl"].
imageA 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.

fromSnapshotsaveToThe home starts as
nonenoneEmpty.
A referencenoneA copy of the snapshot.
nonego-baseA copy of the newest version of go-base. Empty when go-base does not exist yet.
go-base or go-base@3go-baseThe 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:

  • saveTo is a volume name. It is a literal: it has no ${{ }}.
  • A job with saveTo has no matrix.
  • fromSnapshot and saveTo name 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 snapshots unit 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.

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 (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

Procedure

  1. Write this definition to site-check.json. The jobs api and docs run in parallel, and summary runs after both.

    json
    {  "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 }}" }        }      ]    }  }}
  2. Create the pipeline from the file, as in Create a pipeline.

  3. Start a run and wait for it to end.

    Python
    run = kl.pipelines.run(owner="acme", pipeline="site-check")print(run.wait(), [(j["key"], j["state"]) for j in run.jobs])
    TypeScript
    const run = await kl.pipelines.run({ owner: "acme", pipeline: "site-check" });console.log(await run.wait(), run.jobs.map((j) => [j.key, j.state]));
    Go
    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)}
    Ruby
    run = kl.pipelines.run(owner: "acme", pipeline: "site-check")puts run.waitrun.jobs.each { |j| puts "#{j["key"]} #{j["state"]}" }
    Java
    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"));
    CLI
    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

  1. Make sure that api and docs have close startedAtMs values, and that summary started after both ended.
  2. 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

Procedure

  1. Add the input target and the job announce to site-check.json.

    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\"" }]    }  }}
  2. Send the definition as the next version. See Save a new version.

  3. Start a run with target set to prod.

    Python
    run = kl.pipelines.run(owner="acme", pipeline="site-check", inputs={"target": "prod"})print(run.wait())
    TypeScript
    const run = await kl.pipelines.run({ owner: "acme", pipeline: "site-check", inputs: { target: "prod" } });console.log(await run.wait());
    Go
    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)
    Ruby
    run = kl.pipelines.run(owner: "acme", pipeline: "site-check", inputs: { "target" => "prod" })puts run.wait
    Java
    var run = kl.pipelines().run(new Kloudlite.RunPipeline().owner("acme").pipeline("site-check").input("target", "prod"));System.out.println(run.waitFor());
    CLI
    RUN=$(kloudlite pipelines run acme/site-check --input target=prod --json | jq -r .id)kloudlite pipelines wait-run acme site-check "$RUN"

Close-out

  1. Make sure that announce is success for prod.
  2. Start a run with no inputs. Make sure that announce is skipped, because the default target is staging.

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

Procedure

  1. Write this definition to python-check.json. The axis python names a nixpkgs package for each instance.

    json
    {  "name": "python-check",  "jobs": {    "test": {      "matrix": { "python": ["python311", "python312"] },      "packages": ["${{ matrix.python }}"],      "steps": [{ "name": "version", "run": "python3 --version" }]    }  }}
  2. Create the pipeline from the file, and start a run. See Start a run and follow it.

  3. Read the log of one instance. Use the key of the instance as the job.

    Python
    print(run.logs(job="test (python312)"))
    TypeScript
    console.log(await run.logs({ job: "test (python312)" }));
    Go
    out, err := run.Logs("test (python312)")if err != nil {    log.Fatal(err)}fmt.Print(out)
    Ruby
    puts run.logs(job: "test (python312)")
    Java
    System.out.print(run.logs("test (python312)"));
    CLI
    kloudlite pipelines logs acme python-check "$RUN" "test (python312)"

Close-out

  1. Make sure that the run lists the jobs test (python311) and test (python312).
  2. 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

  1. Give the job an image and a timeoutMinutes. Use only one of packages and image.

    json
    {  "name": "image-check",  "jobs": {    "check": {      "image": "python:3.12-slim",      "timeoutMinutes": 30,      "steps": [{ "name": "version", "run": "python -c 'import sys; print(sys.version)'" }]    }  }}
  2. Create the pipeline from the file, and start a run. See Start a run and follow it.

Close-out

  1. 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

Procedure

  1. Write this definition to go-base.json. The job base saves its home to go-base; the job build starts from it.

    json
    {  "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" }]    }  }}
  2. Create the pipeline from the file, and start a run. See Start a run and follow it.

  3. Show the versions of the volume.

    Python
    for v in kl.volumes.versions(owner="acme", name="go-base"):    print(v["version"], v["phase"], v["saved_by"])
    TypeScript
    console.log(await kl.volumes.versions({ owner: "acme", name: "go-base" }));
    Go
    versions, err := kl.Volumes.Versions("acme", "go-base")if err != nil {    log.Fatal(err)}fmt.Println(versions)
    Ruby
    puts kl.volumes.versions(owner: "acme", name: "go-base")
    Java
    System.out.println(kl.volumes().versions("acme", "go-base"));
    CLI
    kloudlite volumes versions acme/go-base

Close-out

  1. Make sure that each run adds a version. Make sure that saved_by names the job base.
  2. Read the log of build. Make sure that it shows the date that base wrote.
  3. Make sure that the run shows saved on base and home on build with the same version.

Troubleshooting

SymptomPossible causeCorrective action
422 with `needs` cycle: a → b → aJobs need each other in a loop.Remove one of the needs.
422 with job summary: `needs` names `test`, which is not a job hereA 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 oneThe job sets both runtimes.Keep one of packages and image.
422 with the matrix makes 80 instances; the limit is 64The matrix has too many combinations.Remove values, or split the job.
422 with `matrix.python` is empty, so the job would never runAn axis has no values.Give the axis at least one value, or remove it.
422 with `timeoutMinutes`: 0 is outside 1-1440The 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 skippedA 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 namesaveTo has ${{ }}.Write the name.
422 with job base: saveTo cannot be used with matrixThe 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 otherThe 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 itfromSnapshot 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 9The 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 volumeThe name belongs to an environment volume.Use a workspace volume.
422 with job build: ws-… is not a pushfromSnapshot 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 existThe 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 9The 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 savedTwo 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 failedThe 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 minutesThe 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 itEvery 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 savedThe 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 savedAnother job already saved this sandbox.Start the run again.
A job fails with the job's sandbox is gone; nothing was savedThe sandbox ended before the save.Start the run again.
A job fails with snapshots: 20 of 20 in use; request more under QuotaThe owner has no free snapshots unit for the save.Delete a version, or request more quota.
A job fails with timed outThe job ran longer than timeoutMinutes.Set a larger timeoutMinutes and save a new version.
A job fails with `probe` exited 6A 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 minutesThe 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.