Skip to content

Expressions and functions

Anywhere in a string value, ${{ <expression> }} evaluates a CEL expression with Slipway’s variables and functions. In if: fields the braces are optional.

CEL (Common Expression Language) is a small, safe, fast, non-Turing-complete language. The same engine evaluates pipeline expressions and policy gates, so what you learn here transfers.

env:
IMAGE_TAG: ${{ git.short_sha }}
if: git.branch == "main" && !event.pull_request.draft
name: ${{ format("deploy-{0}", matrix.region) }}
You want Write
Equality, comparison git.branch == "main", inputs.count >= 3
Boolean logic a && b, a || b, !a
Ternary git.branch == "main" ? "prod" : "dev"
Membership "hotfix" in event.pull_request.labels
String operations git.message.startsWith("fix:"), .contains(), .endsWith(), .matches("^v[0-9]+")
List and map access matrix.os, steps.build.outputs["image-digest"]
Conversions int("3"), string(1), double("1.5"), fromJSON(s)
Macros files.exists(f, f.startsWith("docs/")), files.all(...), files.filter(...), files.map(...)
  • if on jobs and steps
  • env, with, name, runs_on, matrix values, outputs, concurrency.group, environment.url
  • webhook.filter and environment policy gates (a different set of variables, see Gates)

Not allowed: secrets.* in name, run or if. Pass secrets through env. See Validation.

Expressions are parsed and type-checked at validation time against the contexts that exist at that scope. For example:

  • steps.<id> exists only inside a job, so steps.build.outputs.x in a job-level if is an error.
  • needs.<job> exists only for jobs listed in needs.
  • matrix exists only in jobs with a matrix.

A type error fails slipway validate rather than a run at 2 a.m.

# Only deploy from main or a release tag
if: git.branch == "main" || git.tag.startsWith("v")
# Skip when only docs changed
if: git.changed_files.exists(f, !f.startsWith("docs/"))
# A tag that is unique per run but readable
env:
VERSION: ${{ format("{0}-{1}", git.branch, git.short_sha) }}
# Run a cleanup step no matter what
- run: ./cleanup.sh
if: always()
# Gate on an upstream job's output
if: needs.build.outputs.changed == "true"
# Semver comparison
if: semver.gt(inputs.version, "2.0.0")

Next: Contexts, Functions, Workflow commands.