View as Markdown

Filters

Agent hint

Rules on this page that most often cause invalid routines or silent skips:

  • A filter must apply to every listed event, or the routine is refused with 422 at /filters/{name}: `command` does not apply to pr.opened; every filter must apply to every listed event, so split the routine. Check the matrix. Schedule and webhook routines accept only when.
  • Defaults skip a lot. With no filters at all, a routine already skips drafts, forks, other bots and Forkline’s own activity. Set draft: include, fork: include, bots: include or self: include when you want those events.
  • include is any of, exclude is none of, and exclude wins. A list must have 1 to 50 entries, each up to 256 bytes; omit an empty list.
  • self and bots judge the actor (sender, the account that caused the event), not the author of the pull request. self is Forkline’s own bot, bots is every other bot, and both default to exclude.
  • To skip pull requests written by a bot, use when: '!pr.authorIsBot' on pr.* events (on GitLab only when the merge request is first opened, see Authors and labels); bots: exclude does not do that. To skip only Forkline’s own pushes and comments and still run for other bots, set bots: include and leave self at exclude. See the recipes.
  • A routine runs at most limits.maxRunsPerPr times for one pull request, default 3, replaced runs included. A routine on pr.updated that should run on every commit needs a higher value (at most 50); see Concurrency and limits.
  • branches means a different branch per event: the base branch for pull requests, the pushed branch for push, the run’s branch for ci.failed.
  • Branch and path globs are case-sensitive and only support *, ? and **. Labels and authors are plain names, case-insensitive, no globs.
  • comment.command.name in a template is comment.commandName in when.

Filters decide whether an event starts a run. All of them must pass. They go under filters in the routine file, and every key is optional:

filters:
  branches: { include: [main, "release/*"] }
  labels: { exclude: [no-review] }
  draft: exclude

Which filters apply to which event

Every filter you set must apply to every event you list in on.events. The table is generated from the validator.

Event branches paths labels authors draft fork bots self command when
pr.opened yes no yes yes yes yes yes yes no yes
pr.updated yes no yes yes yes yes yes yes no yes
pr.merged yes no yes yes yes yes yes yes no yes
pr.closed yes no yes yes yes yes yes yes no yes
issue.opened no no yes yes no no yes yes no yes
comment.created no no yes yes no no yes yes yes yes
ci.failed yes no no no no no no no no yes
push yes yes no yes no no yes yes no yes

When the events differ, write one routine per kind of event. The error for a misfit lists the events that do not fit:

Case Pointer Message
A filter that some listed event does not support /filters/{name} `{name}` does not apply to {events}; every filter must apply to every listed event, so split the routine
A filter other than when on a schedule or webhook routine /filters/{name} `{name}` only applies to routines started by events

All filters

Filter Value Default What it does
branches { include?: [glob], exclude?: [glob] } not set The branch of the event matches the globs, see Branches
paths { include?: [glob], exclude?: [glob] } not set At least one changed file matches, push only, see Paths
labels { include?: [name], exclude?: [name] } not set Labels of the pull request or issue
authors { include?: [name], exclude?: [name] } not set Author of the pull request, issue or comment, or the pusher
draft exclude, include or only exclude Draft pull requests
fork exclude, include or only exclude Pull requests from a fork
bots exclude, include or only exclude Events caused by other bot accounts
self exclude, include or only exclude Events caused by Forkline’s own bot
command { names?: [string], mention?: boolean } not set A slash command or a mention in a comment
when CEL expression string not set A condition over the typed event fields, see when conditions

branches, paths, labels and authors need at least one of include and exclude (set `include`, `exclude` or both).

Include and exclude

Used by branches, paths, labels and authors:

  • A value passes when it matches no exclude entry and (there is no include, or it matches at least one include entry). include is any-of, exclude is none-of, and exclude wins when both match.
  • Each list has 1 to 50 entries. An empty list is an error (must not be empty, remove it instead at /filters/{name}/include), and so is a list of more than 50 (at most 50 entries, this has 51). Each entry is 1 to 256 bytes and not blank (must not be empty, is longer than 256 bytes, pointer /filters/{name}/include/{index}).
  • labels and authors compare plain names, ignoring case, with no globs. exclude: [no-review] blocks a pull request that carries the label No-Review.
  • branches and paths use globs, which are case-sensitive.

The yes/no filters: draft, fork, bots, self

Each takes one of three values, and the default for all four is exclude, also when filters is omitted:

Value Meaning
exclude Skip the event when the property is true. This is the default
include Ignore the property: draft or not, fork or not, bot or not
only Run only when the property is true
Filter The property Read from
draft the pull request is a draft pr.draft
fork the pull request comes from a fork pr.fork
bots the actor is a bot account, other than Forkline’s own sender.isBot
self the actor is Forkline’s own bot sender

self and bots: the actor, not the author

Both look at the actor, the sender of the event: whoever caused it by pushing, commenting, opening or merging. A pull request authored by renovate[bot] but pushed to by a person has a human actor. To filter on who wrote the pull request, use authors or when: '!pr.authorIsBot'.

Actor self: exclude, bots: exclude (defaults)
A person runs
Another bot (renovate[bot], dependabot[bot], a GitLab token bot) skipped by bots
Forkline’s own bot (forkline[bot], @forkline-bot) skipped by self
  • self identifies the bot Forkline posts as on that connector: the connector’s configured automation account, and, always, the names forkline and forkline-bot (on GitHub also forkline[bot] and forkline-bot[bot]), ignoring case. Without this filter a routine would answer its own comments and loop. Use self: include to let a routine react to Forkline’s own comments, or self: only to run only on them.
  • bots counts only other bots: Forkline’s own bot is judged by self alone. bots: only runs on bot-caused events other than Forkline’s, such as dependency updates.
  • On GitLab and Forgejo connectors set up with a person’s OAuth login and no automation account, Forkline posts as that person, so its own actions cannot be told apart from theirs.
  • ci.failed has no actor, so self and bots do not apply to it and are refused with it.

Who to skip: recipes

Goal Write
Skip pull requests authored by a bot (Renovate, Dependabot) when: '!pr.authorIsBot' on pr.* events (on GitLab only when the merge request is first opened, see Authors and labels)
Skip events caused by other bots, keep people and Forkline bots: exclude (default) and self: include
Skip only Forkline’s own activity, keep other bots bots: include and leave self: exclude (default)
Run for everybody, including Forkline’s own comments bots: include and self: include (add a loop guard such as maxRunsPerPr)
Run only for bots other than Forkline bots: only
name: review-human-prs
description: Review pull requests that were not written by a bot
on:
  events: [pr.opened, pr.updated]
repositories:
  include: all
filters:
  when: '!pr.authorIsBot'
agent:
  prompt: |
    Review pull request #{{ pr.number }} in {{ repository.fullName }}.
name: check-pushes-except-forkline
description: Look at pushes to main from people and other bots, but not from Forkline itself
on:
  events: [push]
repositories:
  include: all
filters:
  branches:
    include: [main]
  bots: include
agent:
  prompt: |
    {{ sender.login }} pushed {{ push.commitCount }} commits to {{ push.branch }} in {{ repository.fullName }}. Look for anything that needs follow-up.

The second routine runs for renovate[bot] (bots: include) and skips forkline[bot] (self stays exclude). With the default bots: exclude it would skip both.

draft and fork

They apply to the four pr.* events. draft: exclude skips draft pull requests; on GitHub and Forgejo a pull request that later leaves draft fires pr.opened again and then runs. Use draft: include to review drafts too, and fork: only to run only on pull requests from forks. GitHub and Forgejo flag a pull request as a fork when the head repository differs from the base repository; GitLab compares source and target project.

Branches

branches matches one branch, and which one depends on the event:

Event The branch it matches Field
pr.opened, pr.updated, pr.merged, pr.closed the base branch: where the pull request merges into pr.baseBranch
push the pushed branch push.branch
ci.failed the branch of the run, or the pull request’s head branch when the run has none ci.branch, else pr.headBranch

To filter pull requests by their head branch, use when: 'pr.headBranch.startsWith("feat/")'.

name: review-release-prs
description: Review pull requests that target main or a release branch
on:
  events: [pr.opened, pr.updated]
repositories:
  include: all
filters:
  branches:
    include: [main, "release/*"]
    exclude: ["release/legacy-*"]
agent:
  prompt: |
    Review pull request #{{ pr.number }} ({{ pr.title }}) into {{ pr.baseBranch }}.

Paths

paths applies to push only. The file list of a push is every added, modified and removed path.

  • The push passes when at least one changed file passes the include/exclude test. With only exclude, the push runs unless every changed file is excluded.
  • Paths are relative to the repository root, without a leading /, and use / as separator. Globs are case-sensitive.
  • An incomplete file list passes. When the forge sent no file list, or push.filesTruncated is true (the forge listed fewer commits than were pushed, or the push has more than 3,000 paths, each path at most 512 bytes), Forkline cannot rule the push out and runs the routine. A run that passed paths this way says so in its reason.
name: docs-on-push
description: Update the docs when source files change on main
on:
  events: [push]
repositories:
  include: all
filters:
  branches:
    include: [main]
  paths:
    include: ["src/**", "**/*.proto"]
    exclude: ["src/**/*_test.go"]
agent:
  prompt: |
    {{ push.commitCount }} commits were pushed to {{ push.branch }}.
    Bring the docs in line with the changes in {{ push.after }}.

Authors and labels

Filter Compared with Notes
authors on pr.* pr.author The author of the pull request, not the person who pushed or merged
authors on issue.opened issue.author
authors on comment.created comment.author
authors on push sender.login The pusher
labels on pr.* pr.labels include passes when any label is listed, exclude when none is
labels on issue.opened issue.labels
labels on comment.created the labels of the pull request when the comment is on one, otherwise of the issue

GitLab names a merge request’s author only when it is first opened. On GitLab, a reopened merge request (pr.opened), pr.updated, pr.merged and pr.closed have an empty pr.author and pr.authorIsBot is false: authors.include never matches them, authors.exclude always passes, and !pr.authorIsBot does not skip bot merge requests. On GitLab, also test the bot’s branch prefix: when: '!pr.authorIsBot && !pr.headBranch.startsWith("renovate/")'.

name: triage-bugs
description: Look at new issues labelled bug from outside the team
on:
  events: [issue.opened]
repositories:
  include: all
filters:
  labels:
    include: [bug, regression]
    exclude: [wontfix]
  authors:
    exclude: [alice, bob]
agent:
  prompt: |
    Triage issue #{{ issue.number }}: {{ issue.title }}

Command

command applies to comment.created. It passes when either condition holds (they combine as OR):

  • names: the comment’s slash command is one of the names. 1 to 20 names, each starting with a letter, then letters, digits, - or _, up to 64 characters, written without the leading /, each listed once. Names are case-insensitive: names: [fix-ci] matches /Fix-CI.
  • mention: true: the comment mentions @forkline or @forkline-bot.

Set names, mention: true or both (set `names` or `mention: true`).

What counts as a command: the first line of the comment that starts, after optional spaces, with / and a letter, followed by letters, digits, - or _, and then a space or the end of the line. Only that first command line is considered, so a comment /retry followed by /fix-ci runs as retry. Lines such as /usr/bin or /1 go are not commands. Everything after the name on that line, trimmed, is comment.command.args (at most 1,024 bytes) in templates and comment.commandArgs in when. A mention counts anywhere in the comment, as a whole word, in any case; a@forkline.dev and @forklinex do not count.

Because the slash must start the line, a comment such as @forkline /fix-ci does not parse as the command fix-ci — the line begins with @forkline, not /, so no command is extracted. With names: [fix-ci] alone the filter would not pass. Set mention: true as well: the mention path (comment.mentionsBot) is independent of the command parse, so @forkline /fix-ci passes via the mention even though the command name is not recognised. The two conditions are OR: a comment that starts with /fix-ci and has no mention passes via names; a comment that mentions the bot with no slash command passes via mention: true; a comment that does both passes via either path.

command only filters comments. Who may issue the command is decided by the trigger policy. A command in the description of a new pull request or issue is not supported.

name: fix-on-command
description: Fix CI when a trusted person comments /fix-ci or mentions Forkline
on:
  events: [comment.created]
repositories:
  include: all
filters:
  command:
    names: [fix-ci, retry]
    mention: true
  labels:
    exclude: [do-not-touch]
agent:
  prompt: |
    {{ comment.author }} wrote on {{ comment.url }}:
    {{ comment.body }}
    Command: {{ comment.command.name }} {{ comment.command.args }}

Glob syntax

branches and paths entries, and repository names in selectors, are globs. The rules are exactly these:

Pattern part Matches
* any run of characters (including none) within one segment: it never crosses a /
? exactly one character within one segment
** as a whole segment any number of segments, including none
anything else itself, literally
  • Segments are separated by /. A pattern must match the whole value.
  • Globs are case-sensitive for branches and paths, and case-insensitive for repository names.
  • There are no character classes ([abc]), no braces ({a,b}), no ! negation and no escaping. Use exclude for a negation. ** inside a longer segment (a**b) acts like *.
  • A pattern is 1 to 256 bytes (for repository name patterns: the pattern is empty, the pattern is longer than 256 bytes; for branches and paths entries: must not be empty, is longer than 256 bytes).
Pattern Matches Does not match
main main Main, main2
release/* release/1.0 release/1.0/rc, release
release/** release/1.0/rc, release hotfix/1
* main feat/x
v?.0 v1.0 v10.0
src/** src/a/b.rs, src lib/a.rs
src/**/*.rs src/c.rs, src/a/b/c.rs src/c.toml, lib/c.rs
**/Cargo.toml Cargo.toml, crates/x/Cargo.toml Cargo.lock

Trigger policy

Forkline checks who may start a run for events that a person triggers as a command. The trigger policy is that check.

  • Which events. comment.created only. Opening a pull request or issue, pushing and CI failures are not checked, so use authors if you need to restrict them.
  • Which actors. Every actor except the connector’s own automation identity on your repository: its configured automation account, and on GitHub the app bot (forkline[bot], forkline-bot[bot]). The plain logins forkline and forkline-bot count as self for the filters, but a comment from them is still checked against the policy.
  • Where it is set. It is a setting of each repository, not of the routine, and it does not appear in the routine file. Change it under Repositories, then the repository’s settings, then Trigger Policy, or with the trigger_policy and allowed_users fields of the repository API. See Trigger policies.
  • Values.
Policy Who may start a run
collaborators (default) The owner of the connector, or a person with write access to the repository (checked on the forge for the person who wrote the comment)
owner_only Only the owner of the connector (matched by git username)
allowlist Only the usernames in the repository’s allowed users, ignoring case
anyone Everybody who can comment
  • When it runs. After every filter and when have passed and before the prompt is rendered, so a denied person never costs a render or a task. The same policy applies to Forkline’s built-in automations.
  • A denied actor gives outcome filtered at stage trigger_policy, with a reason such as `mallory` isn't allowed to trigger routines on `acme/api` (trigger policy: `collaborators`). If the forge cannot be asked whether the person has write access (it answers with an error or is unreachable), the person is denied the same way. The outcome is error at stage trigger_policy only when the repository or its connector cannot be loaded, and no run is started.

Evaluation order

For each event and each routine that lists it, Forkline runs these stages in order. The first stage that blocks ends the run for that routine and repository, and its name is the stage of the outcome you see in Testing and activity. (A test evaluates every filter and when, even after one blocks, and reports all of them. It does not evaluate overload, fan_out, event_key, concurrency, run_limit, credits or submit; a backtest also skips trigger_policy.)

overload → fan_out → event → repository → self_guard → bots → draft → fork → branches → labels → authors → command → paths → when → trigger_policy → template → event_key → concurrency → run_limit → credits → submit

Stage Reads Blocks with
self_guard filters.self and the actor filtered
bots filters.bots and sender.isBot filtered
draft filters.draft and pr.draft filtered
fork filters.fork and pr.fork filtered
branches filters.branches, see Branches filtered
labels filters.labels filtered
authors filters.authors filtered
command filters.command filtered
paths filters.paths filtered
when filters.when filtered when false; condition_error when it cannot be evaluated
trigger_policy the repository’s trigger policy (comment.created only) filtered; error when the check fails
template agent.prompt, agent.ref template_error
event_key the delivery of the event deduplicated: the forge delivered it again
concurrency concurrency (see Concurrency and limits) skipped_active, concurrency_limited; an older run is superseded
run_limit limits.maxRunsPerPr run_limit
credits your credit balance credit_exhausted
submit the owner’s pending-run cap concurrency_limited when the account has too many pending runs (otherwise the run is created: task_created)
  • The first four stages (overload, fan_out, event, repository) are not filters: overload drops events when Forkline is overloaded, fan_out limits schedule and webhook routines to 200 repositories, and event and repository appear only in tests and backtests, which check that the event and repository match the routine.
  • The filter stages that your routine does not set still run with their defaults: self_guard, bots, draft and fork always check (default exclude); the others run only when the filter is set. A stage that does not apply to the event (for example self_guard on ci.failed) records not_applicable; a test or backtest reports not_evaluated for a when or paths that reads a field the stored event does not carry.
  • Each stage records what it saw in the trace: the field (for example pr.baseBranch), the observed value, the rule and a result of pass, filtered, error, not_applicable or not_evaluated.

when conditions

when is one CEL expression that returns a boolean. It reads the typed fields of the event, and on a webhook routine also the request body as payload. It runs after the other filters. false ends the run with outcome filtered at stage when.

filters:
  when: 'pr.title.startsWith("feat") || "needs-review" in pr.labels'

Fields

The roots and fields are the ones on the Events page, with the same camelCase names. Roots are repository, sender, pr, issue, comment, ci and push. There is no event, receivedAt or schedule in when. payload exists only on a webhook routine, see Webhook routines.

Root Fields
repository fullName, owner, name, url, defaultBranch, private
sender login, isBot
pr number, title, body, author, authorIsBot, baseBranch, headBranch, headSha, labels, draft, fork, url
issue number, title, body, author, labels, url
comment id, body, author, url, commandName, commandArgs, mentionsBot
ci name, runId, url, conclusion, headSha, branch, failures (each with name, runId, url, conclusion)
push ref, branch, before, after, forced, commitCount, filesTruncated, files.added, files.modified, files.removed
  • The command is flat in when. Use comment.commandName and comment.commandArgs (strings, empty when the comment has no command). comment.command.name is only valid in templates; in when it is refused as `comment.command` is not a field, available fields are ....
  • A root exists only for events that carry it. If every listed event carries a root, its fields have plain types. If only some do (pr with [pr.opened, issue.opened], or pr with comment.created), its scalar fields can be null and its lists are empty when the root is absent. If no listed event carries it, saving fails.
  • pr.* on comment.created needs a guard. The comment’s parent is a pull request or an issue, so pr is absent for a comment on an issue and a bare !pr.authorIsBot saves but ends with condition_error on such a comment. Write pr.number != null && !pr.authorIsBot (false on an issue, so the routine is filtered), or list only pr.* events when the routine is about pull requests.
  • Lists are bounded: 100 labels, 100 ci.failures, and 500 paths in each push.files list (push.filesTruncated says when there were more).

Operators and functions

Operators: ==, !=, <, <=, >, >=, &&, ||, !, + (numbers, strings and lists), -, *, /, %, unary -, indexing, in, and cond ? a : b. Allowed functions:

Function Example
size() size(push.files.added) > 0, pr.labels.size() == 0
contains() issue.title.contains("crash")
startsWith(), endsWith() pr.headBranch.startsWith("feat/"), push.files.added.exists(f, f.endsWith(".sql"))
matches() `pr.title.matches(“^(feat
string(), int(), dyn() string(pr.number) == "412"
has() has(push.files)
all(), exists(), exists_one(), map(), filter() pr.labels.exists(l, l.startsWith("team/"))

Anything else (lowerAscii(), replace(), optional access ?.) is refused.

  • Both sides of == must have the same type. A side may be null only when the other is a field that can be null (a root only some listed events carry); pr.title != null on [pr.opened] is refused. &&, ||, ! and ?: need booleans. The whole expression must be a boolean.
  • Negate membership with parentheses: !("x" in pr.labels). Written !"x" in pr.labels it means (!"x") in pr.labels and is refused.
  • To test a possibly-absent value, compare with null and short-circuit: pr.title != null && pr.title.startsWith("feat"). has(pr.number) is true even when the value is null.
  • Limits: 4,096 bytes, 16 levels of nesting, 512 expression nodes, and 256 bytes for any one matches() pattern.
  • Cost budget: 50,000 units (about 12 nanoseconds each, so well under a millisecond), estimated at save time for the worst case under the size bounds above. exists() and the other macros multiply the cost of their body by the size of the list, and a regular expression has a cost of its own. Prefer startsWith() and contains() to matches(), and do not compare or concatenate whole lists.

Errors

At save time the validator type-checks when and returns 422 at /filters/when:

Mistake Message
Empty string must not be empty
Syntax error invalid expression `pr.title ==`: {detail}
A root no listed event carries `pr` is not available: the routine's events (push) never carry it
On a schedule or webhook routine `pr` is not available: this routine is not started by an event, only `repository` is (a webhook routine also has payload)
Unknown root `foo` is not a variable, use `repository`, `sender`, `pr`, `issue`, `comment`, `ci` or `push` (the ones your events carry), or `payload` in a webhook routine
payload on an event or schedule routine `payload` is not available: only a webhook routine has a request body. Event and schedule routines can read `repository` and the roots their events carry
Unknown field `comment.command` is not a field, available fields are `comment.id`, `comment.body`, ...
Not a boolean a filter must evaluate to a boolean, but this one is int
Mixed types `==` compares values of the same type, not string and int
Function not allowed `.lowerAscii()` is not a supported function, or takes other arguments; filters may use size(), contains(), ...
Too long filter is {n} bytes, the limit is 4096
Too deep filter nests expressions more than 16 levels deep
Too many nodes filter is too large, it may use at most 512 expressions
matches() pattern too long the regular expression is {n} bytes, the limit is 256; use a shorter pattern
Over the cost budget the filter's estimated worst-case cost {n} exceeds the budget of 50000 by a factor of {f}; macros such as exists() multiply the cost of their body ...

At run time a when that cannot be evaluated, for example because it calls a method on a null field (pr.title.startsWith("x") on a comment that is on an issue), ends with outcome condition_error at stage when and the message it reads a field that is empty for this event, so `{source}` can't be checked. Test the field first, for example `pr.number != null && …` (or, for any other failure, `{source}` could not be evaluated for this event.). In a webhook routine, a condition that reads a payload field the body does not have says so, for example the payload has no `level` field. Guard optional fields with `has(payload.level)`., and any other failure reads `{source}` could not be evaluated against this payload.. The message names the missing key. It also names a guard only when that is certain: the key must appear exactly once in the whole filter, as a plain read such as payload.a.b (field names and constant indexes only, starting at payload), and the filter must not use an index that is not a string or an integer constant, such as payload.m[payload.k] or payload.m[true]. The guard is has(payload.a.b) for plain names, or "user-agent" in payload for a key that is not a plain name. If the key appears more than once, or is selected on a macro variable (such as i.name in exists(i, ...)), or on the result of a conditional, a list or map literal, or a function call, the message does not name a guard. Otherwise it says to guard with has(), or with in for a key that is not a plain name. has() on a body that is not an object (an array, or data: null) is a condition_error, not false. No run is started, and other routines are not affected. A result that is not a boolean is also a condition_error.

Schedule and webhook routines

On a schedule routine when can read repository only. A webhook routine can also read payload, see below. Forkline evaluates when once per selected repository at stage when. A repository for which it is false is skipped, so when: '!repository.private' runs a scheduled routine on public repositories only.

Reading the webhook payload with payload

On a webhook routine, payload is the JSON body of the call, so a routine can ignore alerts that do not matter. Run now with a payload (see Testing and activity) evaluates it the same way.

filters:
  when: 'payload.level == "error" && payload.environment in ["prod", "production"]'

Other shapes: payload.tags.exists(t, t == "urgent"), payload.count > 3 (a JSON number compares with whole numbers whether or not it has a decimal point; arithmetic such as % needs a whole number). Guard fields a sender may leave out with has(): has(payload.level) && payload.level == "error". A body that is not JSON arrives as payload.raw_body. A JSON body that is an array is payload itself: read its elements with payload[0], and payload.x on it is a condition_error. when cannot read a body that is a JSON string, number or null. Read it in the prompt instead.

Forkline cannot check payload field names when you save, because the body is only known at run time. A typo shows up as a condition_error on a run, or on a dry run with an example body.

To keep a condition fast, Forkline checks the fields when reads against these limits before it evaluates the condition. The rest of the body is not counted, so a large body such as a Sentry alert with a full stack trace can be filtered on payload.level.

Limit on the fields when reads Value
Weight of all read fields together: one per value and per key, plus one per 64 bytes of text 4,096
Nesting depth, counted from payload 16
Items in any one array or object 1,000
Text of strings and keys, all read fields together 64 KiB

What counts as read:

  • A field you compare, use as the list of an in test (x in payload.list), pass to size() or a string method, or run a macro such as exists() over counts with everything under it: payload.tags.exists(t, t == "urgent") counts the whole tags list.
  • A field you only test with has() counts as one value: has(payload.data.event.exception) does not count the exception.
  • A key you test with in ("user-agent" in payload) counts as one value, like has(). On an array, in compares elements, so the whole array counts.
  • A constant index counts that element, plus one per element before it: payload.items[0].id counts id. An index of 1,000 or more into a list that has that element is over the item limit: payload exceeds filter limits: `payload.items[1500]` is past the 1000-item limit. Use payload["user-agent"] for keys that are not plain names, and test such a key with "user-agent" in payload (has() takes only plain names).
  • A computed index counts the whole value it indexes: payload.byEnv[payload.env] counts all of payload.byEnv.
  • A field read twice counts once.

When the fields read go over a limit, the run ends with outcome condition_error at stage when, the reason names the field, and nothing runs. For example, payload.data.event.exception.values.exists(v, v.type == "TypeError") reads the whole values list of a Sentry event, and a list with many frames gives payload exceeds filter limits: `payload.data.event.exception.values` takes the fields `when` reads past 4096 values and keys (long text counts as several). Read a smaller field (the event’s level or title rather than its frames), or make the decision in the prompt, where templates see the whole body. A when that does not read payload is not affected.

name: flaky-test-hunt
description: Look into pull requests that touch tests and are not from bots
on:
  events: [pr.opened, pr.updated]
repositories:
  include: all
filters:
  draft: include
  bots: exclude
  when: 'pr.labels.exists(l, l == "tests") || pr.title.contains("flaky")'
agent:
  prompt: |
    Check pull request #{{ pr.number }} for flaky tests.
name: review-forkline-prs
description: Review the pull requests that Forkline itself opened
on:
  events: [pr.opened]
repositories:
  include: all
filters:
  self: only
  fork: exclude
agent:
  prompt: |
    Forkline opened pull request #{{ pr.number }}. Review it.
name: dependency-bots
description: Look at pull requests opened by dependency bots
on:
  events: [pr.opened]
repositories:
  include: all
filters:
  bots: only
  authors:
    include: ["renovate[bot]", "dependabot[bot]"]
agent:
  prompt: |
    Check the changelog of the dependency bump in pull request #{{ pr.number }}.
name: schedule-public-only
description: Weekly audit of public repositories
on:
  schedule:
    cron: "0 8 * * 1"
repositories:
  include: all
filters:
  when: '!repository.private && repository.name.startsWith("api-")'
agent:
  prompt: |
    Audit {{ repository.fullName }}.

Examples that fail the one rule

A filter that does not apply to every listed event fails at its own pointer, whichever event is the misfit.

name: command-on-prs
description: command only applies to comment.created
on:
  events: [pr.opened, comment.created]
repositories:
  include: all
filters:
  command:
    names: [review]
agent:
  prompt: Review it.

Fix: write two routines, one for pr.opened and one for comment.created.

name: paths-on-prs
description: paths only applies to push
on:
  events: [push, pr.updated]
repositories:
  include: all
filters:
  paths:
    include: ["src/**"]
agent:
  prompt: Check the change.
name: self-on-ci
description: ci.failed has no actor
on:
  events: [ci.failed]
repositories:
  include: all
filters:
  self: exclude
agent:
  prompt: Fix {{ ci.name }}.
name: draft-on-issues
description: draft only applies to pull request events
on:
  events: [pr.opened, issue.opened]
repositories:
  include: all
filters:
  draft: include
agent:
  prompt: Look at it.
name: empty-label-list
description: an empty list is an error, remove it instead
on:
  events: [pr.opened]
repositories:
  include: all
filters:
  labels:
    include: []
agent:
  prompt: Review it.
name: pr-field-on-push
description: no listed event carries pr
on:
  events: [push]
repositories:
  include: all
filters:
  when: 'pr.title.startsWith("feat")'
agent:
  prompt: Check the push.
name: branches-on-schedule
description: a schedule routine accepts only when
on:
  schedule:
    cron: "0 6 * * 1"
repositories:
  include: all
filters:
  branches:
    include: [main]
agent:
  prompt: Audit it.