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.draftname: ${{ format("deploy-{0}", matrix.region) }}Syntax in a minute
Section titled “Syntax in a minute”| 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(...) |
Where expressions are allowed
Section titled “Where expressions are allowed”ifon jobs and stepsenv,with,name,runs_on,matrixvalues,outputs,concurrency.group,environment.urlwebhook.filterand environment policy gates (a different set of variables, see Gates)
Not allowed: secrets.* in name, run or if. Pass secrets through env. See Validation.
Type checking
Section titled “Type checking”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, sosteps.build.outputs.xin a job-levelifis an error.needs.<job>exists only for jobs listed inneeds.matrixexists only in jobs with a matrix.
A type error fails slipway validate rather than a run at 2 a.m.
Examples
Section titled “Examples”# Only deploy from main or a release tagif: git.branch == "main" || git.tag.startsWith("v")
# Skip when only docs changedif: git.changed_files.exists(f, !f.startsWith("docs/"))
# A tag that is unique per run but readableenv: 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 outputif: needs.build.outputs.changed == "true"
# Semver comparisonif: semver.gt(inputs.version, "2.0.0")Next: Contexts, Functions, Workflow commands.