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
422at/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 onlywhen. - Defaults skip a lot. With no
filtersat all, a routine already skips drafts, forks, other bots and Forkline’s own activity. Setdraft: include,fork: include,bots: includeorself: includewhen you want those events. includeis any of,excludeis none of, andexcludewins. A list must have 1 to 50 entries, each up to 256 bytes; omit an empty list.selfandbotsjudge the actor (sender, the account that caused the event), not the author of the pull request.selfis Forkline’s own bot,botsis every other bot, and both default toexclude.- To skip pull requests written by a bot, use
when: '!pr.authorIsBot'onpr.*events (on GitLab only when the merge request is first opened, see Authors and labels);bots: excludedoes not do that. To skip only Forkline’s own pushes and comments and still run for other bots, setbots: includeand leaveselfatexclude. See the recipes. - A routine runs at most
limits.maxRunsPerPrtimes for one pull request, default 3, replaced runs included. A routine onpr.updatedthat should run on every commit needs a higher value (at most 50); see Concurrency and limits. branchesmeans a different branch per event: the base branch for pull requests, the pushed branch forpush, the run’s branch forci.failed.- Branch and path globs are case-sensitive and only support
*,?and**. Labels and authors are plain names, case-insensitive, no globs. comment.command.namein a template iscomment.commandNameinwhen.
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
excludeentry and (there is noinclude, or it matches at least oneincludeentry).includeis any-of,excludeis none-of, andexcludewins when both match. - Each list has 1 to 50 entries. An empty list is an error (
must not be empty, remove it insteadat/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}). labelsandauthorscompare plain names, ignoring case, with no globs.exclude: [no-review]blocks a pull request that carries the labelNo-Review.branchesandpathsuse 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 |
selfidentifies the bot Forkline posts as on that connector: the connector’s configured automation account, and, always, the namesforklineandforkline-bot(on GitHub alsoforkline[bot]andforkline-bot[bot]), ignoring case. Without this filter a routine would answer its own comments and loop. Useself: includeto let a routine react to Forkline’s own comments, orself: onlyto run only on them.botscounts only other bots: Forkline’s own bot is judged byselfalone.bots: onlyruns 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.failedhas no actor, soselfandbotsdo 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.filesTruncatedistrue(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 passedpathsthis 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@forklineor@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. Useexcludefor a negation.**inside a longer segment (a**b) acts like*. - A pattern is 1 to 256 bytes (for repository
namepatterns:the pattern is empty,the pattern is longer than 256 bytes; forbranchesandpathsentries: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.createdonly. Opening a pull request or issue, pushing and CI failures are not checked, so useauthorsif 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 loginsforklineandforkline-botcount asselffor 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_policyandallowed_usersfields 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
whenhave 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
filteredat stagetrigger_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 iserrorat stagetrigger_policyonly 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:overloaddrops events when Forkline is overloaded,fan_outlimits schedule and webhook routines to 200 repositories, andeventandrepositoryappear 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,draftandforkalways check (defaultexclude); the others run only when the filter is set. A stage that does not apply to the event (for exampleself_guardonci.failed) recordsnot_applicable; a test or backtest reportsnot_evaluatedfor awhenorpathsthat reads a field the stored event does not carry. - Each stage records what it saw in the trace: the
field(for examplepr.baseBranch), the observed value, the rule and a result ofpass,filtered,error,not_applicableornot_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. Usecomment.commandNameandcomment.commandArgs(strings, empty when the comment has no command).comment.command.nameis only valid in templates; inwhenit 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 (
prwith[pr.opened, issue.opened], orprwithcomment.created), its scalar fields can benulland its lists are empty when the root is absent. If no listed event carries it, saving fails. pr.*oncomment.createdneeds a guard. The comment’s parent is a pull request or an issue, sopris absent for a comment on an issue and a bare!pr.authorIsBotsaves but ends withcondition_erroron such a comment. Writepr.number != null && !pr.authorIsBot(false on an issue, so the routine is filtered), or list onlypr.*events when the routine is about pull requests.- Lists are bounded: 100 labels, 100
ci.failures, and 500 paths in eachpush.fileslist (push.filesTruncatedsays 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 benullonly when the other is a field that can benull(a root only some listed events carry);pr.title != nullon[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.labelsit means(!"x") in pr.labelsand is refused. - To test a possibly-absent value, compare with
nulland short-circuit:pr.title != null && pr.title.startsWith("feat").has(pr.number)istrueeven when the value isnull. - 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. PreferstartsWith()andcontains()tomatches(), 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
intest (x in payload.list), pass tosize()or a string method, or run a macro such asexists()over counts with everything under it:payload.tags.exists(t, t == "urgent")counts the wholetagslist. - 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, likehas(). On an array,incompares elements, so the whole array counts. - A constant index counts that element, plus one per element before it:
payload.items[0].idcountsid. 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. Usepayload["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 ofpayload.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.