Skip to content
On this page

CEL Expressions

This page covers CEL (Common Expression Language) expressions, which give you fine-grained control over which Git events trigger your PipelineRuns. Use CEL when simple annotation matching is not flexible enough – for example, when you want to exclude a specific branch, trigger only when certain file paths change, or filter on payload fields from your Git provider.

If you use the on-cel-expression annotation in the same PipelineRun as on-event, on-target-branch, on-label, on-path-change, or on-path-change-ignore, the on-cel-expression annotation takes priority and Pipelines-as-Code ignores the other annotations.

Basic usage

The following example matches a pull_request event targeting the main branch when the source branch is called wip:

pipelinesascode.tekton.dev/on-cel-expression: |
  event == "pull_request" && target_branch == "main" && source_branch == "wip"

Available CEL variables

The following fields are available inside CEL expressions:

FieldDescription
eventpush, pull_request, or incoming.
event_typeThe event type from the webhook payload header. This value is provider-specific (for example, GitHub sends pull_request, GitLab sends Merge Request).
target_branchThe branch the event targets (for example, main in a pull request that merges into main).
source_branchThe branch the pull request originates from. On push events, this equals target_branch.
target_urlThe URL of the repository the event targets.
source_urlThe URL of the repository the pull request originates from. On push events, this equals target_url.
event_titleThe title of the event. For push, this is the commit title. For pull requests, this is the pull request title. Supported on GitHub, GitLab, and Bitbucket Cloud only.
bodyThe full webhook payload body from the Git provider. Example: body.pull_request.number retrieves the pull request number on GitHub.
headersThe full set of webhook headers from the Git provider. Example: headers['x-github-event'] retrieves the event type on GitHub.
.pathChangedA suffix function you append to a glob string to check whether matching paths changed. Supported on GitHub and GitLab only.
filesThe list of files that changed in the event (all, added, deleted, modified, and renamed). Example: files.all or files.deleted. For pull requests, this includes every file in the pull request.
Custom paramsAny custom parameters you define in the Repository CR spec.params are available as CEL variables. Example: enable_ci == "true". See Custom parameters in CEL expressions limitations below for important details.

Because CEL expressions can combine multiple conditions, they enable scenarios that simple annotations cannot handle. For example, to match a pull_request event but exclude the experimental branch:

pipelinesascode.tekton.dev/on-cel-expression: |
  event == "pull_request" && target_branch != "experimental"

You can find more information about the CEL language spec here:

https://github.com/google/cel-spec/blob/master/doc/langdef.md

Custom parameters in CEL expressions: limitations

Filtered parameters become undefined

If you use a custom parameter that has a filter in a CEL expression, and the filter condition is not met, the parameter is undefined. This causes a CEL evaluation error rather than evaluating to false.

For example, consider this Repository CR:

apiVersion: pipelinesascode.tekton.dev/v1alpha1
kind: Repository
metadata:
  name: my-repo
spec:
  url: "https://github.com/owner/repo"
  params:
    - name: docker_registry
      value: "registry.staging.example.com"
      filter: pac.event_type == "pull_request"

And this PipelineRun:

apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  name: my-pipeline
  annotations:
    pipelinesascode.tekton.dev/on-cel-expression: |
      docker_registry == "registry.staging.example.com"
spec:
  # ... pipeline spec

On a push event, the docker_registry parameter is not defined (because the filter only matches pull requests), so the CEL expression produces an error instead of false. Pipelines-as-Code does not evaluate the PipelineRun and reports an error.

To avoid undefined-parameter errors, reference custom parameters only when their filter conditions match, or use parameters without filters for CEL matching. You can test your CEL expressions against different event types with the tkn pac cel command to verify they work correctly across all scenarios.

Custom parameters cannot override built-in variables

Custom parameters you define in the Repository CR cannot override the built-in CEL variables that Pipelines-as-Code provides, such as:

  • event (or event_type)
  • target_branch
  • source_branch
  • trigger_target
  • And other default variables documented in the table above

If you define a custom parameter with the same name as a built-in CEL variable, the built-in variable takes precedence. Always choose unique names for your custom parameters to avoid conflicts.

Matching by regex

You can match any CEL field using a regular expression. For example, to trigger a PipelineRun on pull_request events where the source_branch contains the substring feat/:

pipelinesascode.tekton.dev/on-cel-expression: |
  event == "pull_request" && source_branch.matches(".*feat/.*")

Matching by path change

Path-based matching lets you run a PipelineRun only when specific files change – for example, running documentation tests only when docs change, or skipping CI when only README files are updated.

Pipelines-as-Code supports two ways to match files changed in a particular event. The .pathChanged suffix function supports glob pattern and does not support different types of “changes” (added, modified, deleted, and so on). The other option is the files. property (files.all, files.added, files.deleted, files.modified, files.renamed) which can target specific types of changed files and supports CEL expressions, for example files.all.exists(x, x.matches('renamed.go')).

To run a PipelineRun only when certain paths change, use the .pathChanged suffix function with a glob pattern. The following example matches every .md file in the docs directory:

pipelinesascode.tekton.dev/on-cel-expression: |
  event == "pull_request" && "docs/*.md".pathChanged()

The following example matches any changed file (added, modified, removed, or renamed) in the tmp directory:

    pipelinesascode.tekton.dev/on-cel-expression: |
      files.all.exists(x, x.matches('tmp/'))

This example matches any newly added file in the src or pkg directory:

    pipelinesascode.tekton.dev/on-cel-expression: |
      files.added.exists(x, x.matches('src/|pkg/'))

This example matches modified files named test.go:

    pipelinesascode.tekton.dev/on-cel-expression: |
      files.modified.exists(x, x.matches('test.go'))

Excluding non-code changes

When you want to run tests only when actual code changes occur – skipping documentation-only or config-only pull requests – you can negate a file pattern match:

pipelinesascode.tekton.dev/on-cel-expression: |
  event == "pull_request"
  && target_branch == "main"
  && !files.all.all(x, x.matches('^docs/') || x.matches('\\.md$') || x.matches('(\\.gitignore|OWNERS|PROJECT|LICENSE)$'))

This expression:

  • Matches only pull_request events targeting the main branch.
  • Skips the PipelineRun when every changed file matches one of these patterns:
    • Files in the docs/ directory (^docs/)
    • Markdown files (\\.md$)
    • Common repository metadata files (\\.gitignore, OWNERS, PROJECT, LICENSE)

The !files.all.all(x, x.matches(...)) pattern means “not every file matches these exclusion patterns.” In practice, Pipelines-as-Code triggers the PipelineRun only when at least one changed file falls outside the excluded patterns – that is, when there are meaningful code changes.

When you use regex patterns in CEL expressions, escape special characters properly. You must double the backslash (\\) within the CEL string context. Using logical OR (||) operators within the matches() function is more reliable than combining patterns with a pipe (|) character in a single regex.

Matching by event title

The following example matches all pull requests whose title starts with [DOWNSTREAM]:

pipelinesascode.tekton.dev/on-cel-expression: |
  event == "pull_request" && event_title.startsWith("[DOWNSTREAM]")

For pull_request events, event_title contains the pull request title. For push events, it contains the commit title.

Matching by body payload

⚠️

Matching PipelineRun on body payload is a Technology Preview feature only. Technology Preview features are not currently supported and might not be functionally complete. We do not recommend using them in production. These features provide early access to upcoming Pipelines-as-Code features, enabling you to test functionality and provide feedback during the development process.

The full webhook payload body from the Git provider is available in the CEL variable body. You can use this variable to filter on any field your Git provider sends.

For example, this GitHub-specific expression:

pipelinesascode.tekton.dev/on-cel-expression: |
  body.pull_request.base.ref == "main" &&
    body.pull_request.user.login == "superuser" &&
    body.action == "synchronize"

matches only when the pull request targets the main branch, the pull request author is superuser, and the action is synchronize (meaning someone pushed new commits to the pull request).

When you match on the body payload in a pull request, GitOps comments such as /retest do not work as expected. The reason is that a comment event delivers the comment payload, not the original pull request payload, so the body fields do not match your CEL expression.

To retest a pull request that uses body-payload matching, push a new SHA to force a fresh pull request event:

# assuming you are on the branch you want to retest
# and the upstream remote are set
git commit --amend --no-edit && \
  git push --force-with-lease

Alternatively, close and reopen the pull request.

Matching by request header

You can filter on webhook headers from the Git provider using the CEL variable headers. Header names are always lowercase.

For example, to verify the event is a pull_request on GitHub:

pipelinesascode.tekton.dev/on-cel-expression: |
  headers['x-github-event'] == "pull_request"