Validation errors
Agent hint
Rules that fix most invalid routines:
- Fix an error by its
pointer, not by guessing from the text. The pointer is a JSON pointer into the routine, for example/filters/labels/inlcude. An empty pointer is the whole document. - Structural mistakes (unknown field, missing field, wrong type, unknown value of a fixed set) are reported one at a time. Fix it, validate again, and the next one appears.
- Every filter must apply to every listed event, and every template variable must exist for every listed event. When the events need different filters or variables, split the routine in two.
- Names marked “not supported yet” are reserved. Remove them; they are not ignored.
- Run
forkline routine validate <file>after every edit. It exits with code 2 when the file is invalid.
Forkline validates a routine when you save it, in the web editor, with forkline routine validate and with POST /v1/routines/validate. All of them use the same rules. This page lists every error, grouped by the part of the routine it points at. Each error has a wrong and a fixed example. Examples marked as routines are complete files that are checked against the real validator when the documentation is built.
Error format
A routine that is not valid is refused with status 422 and this body:
{
"error": "Invalid routine spec: /filters/labels/inlcude: Unknown field `inlcude`. Allowed here: `include`, `exclude`. Did you mean `include`?",
"code": "UNPROCESSABLE_ENTITY",
"errors": [
{
"pointer": "/filters/labels/inlcude",
"message": "Unknown field `inlcude`. Allowed here: `include`, `exclude`. Did you mean `include`?",
"code": "unknown_field"
}
]
}
| Field | Meaning |
|---|---|
error |
One line that names the first issue and, when there are more, how many (for example (and 2 more)). Read errors instead of parsing this line. |
code |
Always UNPROCESSABLE_ENTITY for a validation failure. |
errors[].pointer |
Always present. A JSON pointer into the routine in its camelCase form: /on/events/1 is the second event. A key that contains / or ~ is escaped as ~1 and ~0. An empty string means the whole document, which only happens when the routine is not an object. A missing key is reported at the key itself (a missing on is /on). |
errors[].message |
A sentence for people. The wording can be refined between releases. Match on the pointer, and on code when it is present, never on the text. |
errors[].code |
Optional. A short stable name such as unknown_field, listed in Error codes. Some errors have none. |
The messages in this page are the ones the validator produces today. Values that depend on your input are shown for the example, or as {placeholders}.
How the CLI prints issues
forkline routine validate, create, update and push print one line per issue, in the form {file}:{line}: {message} ({pointer}), and exit with code 2:
.forkline/routines/review-prs.yaml:9: Unknown field `inlcude`. Allowed here: `include`, `exclude`. Did you mean `include`? (/filters/labels/inlcude)
{line} is the line of the offending value in the file. push validates every file first and writes nothing when one of them is invalid. With --json (or -o json) the CLI prints the errors array as it came from the API.
Error codes
A code is optional: an error either carries one of the codes below or none. This is the complete list of the codes the API sends, and the “Pointer” column shows where they appear. Errors that are not listed here have no code, and the validator can add new ones, so treat an unknown code as “no code”.
| Code | When | Pointer |
|---|---|---|
unknown_field |
A key the object does not have | the key |
missing_field |
A required key is missing | the missing key |
wrong_type |
A value of the wrong type | the value |
enum |
A value outside a fixed set | the value |
range |
A number or a length outside its range (agent.model, agent.ref, agent.timeoutMinutes, limits.maxRunsPerPr, concurrency.maxActive), or an on.events list that is empty. Also a routine name that is empty or over 100 characters, on the validate endpoint |
the value |
on_exactly_one |
Not exactly one of events, schedule, webhook |
/on |
unknown_event |
An event name that does not exist (the message names the closest event when one is near) | /on/events/{i} |
reserved_event |
An event reserved for a future release | /on/events/{i} |
too_many_events |
More than 8 events | /on/events |
filter_not_applicable |
A filter that does not apply to a listed event (paths with an event other than push, bots or self on ci.failed, command on an event other than comment.created, and so on), or to a routine that is not started by events |
/filters/{name} |
invalid_glob |
A repository name pattern that is empty or over 256 bytes | /repositories/.../name |
selector_name_format |
A repository name that is not owner/name |
/repositories/.../name |
list_too_long |
A list over its maximum length (50 repository selectors, 50 entries in a filter list) | the list |
fan_out_too_large |
A schedule or webhook routine that matches more than 200 repositories, at save or on Run now | /repositories |
no_repositories |
No repository matches the selection of an event routine (a warning on a validate response) | /repositories |
cron_invalid |
Not a cron expression | /on/schedule/cron |
timezone_unknown |
Not an IANA timezone | /on/schedule/timezone |
template_syntax |
The template does not parse | /agent/prompt, /agent/ref |
template_unknown_filter |
A filter or test that the template engine does not have ({{ pr.title | shout }}) |
/agent/prompt, /agent/ref |
template_unknown_variable |
A variable that a listed event does not provide | /agent/prompt, /agent/ref |
reserved_variable |
A variable reserved for a future release | /agent/prompt, /agent/ref |
template_too_long |
The prompt is over 32 KiB | /agent/prompt |
when_type |
The when condition does not parse or type-check |
/filters/when |
when_cost |
The when condition is too expensive to run on every event |
/filters/when |
group_needs_events |
concurrency.group does not fit the listed events |
/concurrency/group |
header_name_invalid |
A webhook header name that is empty, over 64 bytes or not a valid header name | /on/webhook/auth/header, /on/webhook/idempotencyHeader |
header_reserved |
A webhook header the platform sets, or an idempotency header equal to the credential header | /on/webhook/auth/header, /on/webhook/idempotencyHeader |
field_not_allowed |
header with type: hmac, or preset with type: token |
/on/webhook/auth/header, /on/webhook/auth/preset |
preset_required |
type: hmac without a preset |
/on/webhook/auth/preset |
unknown_preset |
An HMAC preset that does not exist (the message names the closest) | /on/webhook/auth/preset |
example_too_large |
on.webhook.example is over 16 KiB |
/on/webhook/example |
not_a_repository_event |
POST /routines/drafts/from-event with an event that is a schedule, webhook or Run now occurrence |
/event_id |
unknown_fact |
POST /routines/drafts/from-event with a fact key the event does not have |
/facts/{i} |
Errors in the next section show the code they carry, or “none defined”.
Errors by area
The examples use the same few lines of context so that the difference stays visible. Every wrong example fails at exactly the pointer shown.
Document shape
Unknown field
- Pointer:
/filters/labels/inlcude - Message:
Unknown field `inlcude`. Allowed here: `include`, `exclude`. Did you mean `include`? - Code:
unknown_field
The pointer ends with the unknown key itself. The message names it, lists the allowed keys and, when the key is close to one of them (a typo of one or two letters), suggests it with Did you mean.
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
labels:
inlcude: [bug]
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
labels:
include: [bug]
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Missing required field
- Pointer:
/agent/prompt - Message:
`prompt` is required. - Code:
missing_field
Required fields are on, repositories, agent and agent.prompt; include under repositories.
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
model: anthropic/claude-sonnet-4-5
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
model: anthropic/claude-sonnet-4-5
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Wrong type
- Pointer:
/agent/timeoutMinutes - Message:
Expected a whole number, found a string. - Code:
wrong_type
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
timeoutMinutes: 30m
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
timeoutMinutes: 30
Unknown value of a fixed set
- Pointer:
/agent/runnerSize - Message:
`huge` is not allowed here. Use one of: `basic`, `advanced`, `pro`. - Code:
enum
The same error comes from filters.draft, fork, bots and self (allowed: exclude, include, only).
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
runnerSize: huge
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
runnerSize: advanced
Unknown filter mode
- Pointer:
/filters/draft - Message:
`maybe` is not allowed here. Use one of: `exclude`, `include`, `only`. - Code:
enum
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
draft: maybe
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
draft: only
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
on: what starts the routine
No trigger or more than one
- Pointer:
/on - Message:
set exactly one of `events`, `schedule` and `webhook` - Code:
on_exactly_one
An empty on: {} gives the same error.
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
webhook: {}
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
on.events
No events, or more than 8
- Pointer:
/on/events - Message:
list between 1 and 8 events - Code:
rangefor an empty list,too_many_eventsfor more than 8
Wrong:
name: review-prs
description: Review pull requests
on:
events: []
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened, pr.updated]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Unknown event
- Pointer:
/on/events/0 - Message:
Unknown event `pr.open`. Did you mean `pr.opened`? Events: `pr.opened`, `pr.updated`, `pr.merged`, `pr.closed`, `issue.opened`, `comment.created`, `ci.failed`, `push`. - Code:
unknown_event
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.open]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Reserved event
- Pointer:
/on/events/0 - Message:
`ci.succeeded` is not supported yet - Code:
reserved_event
Wrong:
name: review-prs
description: Review pull requests
on:
events: [ci.succeeded]
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [ci.failed]
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }}.
Event listed twice
- Pointer:
/on/events/1 - Message:
`pr.opened` is listed twice - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened, pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened, pr.updated]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
on.schedule
Both or neither of cron and at
- Pointer:
/on/schedule - Message:
set exactly one of `cron` and `at` - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
schedule:
cron: "0 6 * * 1"
at: "2026-12-01T09:00:00Z"
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
Fixed:
name: review-prs
description: Review pull requests
on:
schedule:
cron: "0 6 * * 1"
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
Invalid cron expression
- Pointer:
/on/schedule/cron - Message:
`every monday` is not a cron expression: {parser detail} - Code:
cron_invalid
Wrong:
name: review-prs
description: Review pull requests
on:
schedule:
cron: "every monday"
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
Fixed:
name: review-prs
description: Review pull requests
on:
schedule:
cron: "0 6 * * 1"
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
Unknown timezone
- Pointer:
/on/schedule/timezone - Message:
`Berlin` is not an IANA timezone, use a name like `Europe/Berlin` - Code:
timezone_unknown
Wrong:
name: review-prs
description: Review pull requests
on:
schedule:
cron: "0 6 * * 1"
timezone: Berlin
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
Fixed:
name: review-prs
description: Review pull requests
on:
schedule:
cron: "0 6 * * 1"
timezone: Europe/Berlin
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
timezone together with at
- Pointer:
/on/schedule/timezone - Message:
`timezone` only applies to `cron`; `at` carries its own offset - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
schedule:
at: "2026-12-01T09:00:00Z"
timezone: Europe/Berlin
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
Fixed:
name: review-prs
description: Review pull requests
on:
schedule:
at: "2026-12-01T09:00:00+01:00"
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
at is not an RFC 3339 timestamp
- Pointer:
/on/schedule/at - Message:
`tomorrow 9am` is not an RFC 3339 timestamp: {parser detail} - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
schedule:
at: "tomorrow 9am"
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
Fixed:
name: review-prs
description: Review pull requests
on:
schedule:
at: "2026-12-01T09:00:00Z"
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
on.webhook
Setting that does not exist on webhook
- Pointer:
/on/webhook/secret - Message:
Unknown field `secret`. Allowed here: `auth`, `idempotencyHeader`, `example`. - Code:
unknown_field
The secret is created by Forkline, or pasted with PUT /v1/routines/{id}/webhook/secret. It is never written in the file.
Wrong:
name: review-prs
description: Review pull requests
on:
webhook:
secret: s3cret
repositories:
include: all
agent:
prompt: |
Look into {{ payload.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
webhook: {}
repositories:
include: all
agent:
prompt: |
Look into {{ payload.title }} in {{ repository.fullName }}.
Unknown HMAC preset
- Pointer:
/on/webhook/auth/preset - Message:
Unknown preset `sentri`. Did you mean `sentry`? Presets: `forkline`, `github`, `sentry`. - Code:
unknown_preset
Wrong:
name: sentry-alert
description: Investigate a Sentry alert
on:
webhook:
auth: { type: hmac, preset: sentri }
repositories:
include: all
agent:
prompt: |
Look into {{ payload.title }} in {{ repository.fullName }}.
Fixed:
name: sentry-alert
description: Investigate a Sentry alert
on:
webhook:
auth: { type: hmac, preset: sentry }
repositories:
include: all
agent:
prompt: |
Look into {{ payload.title }} in {{ repository.fullName }}.
HMAC without a preset
- Pointer:
/on/webhook/auth/preset - Message:
`preset` is required with `type: hmac`. Presets: `forkline`, `github`, `sentry`. - Code:
preset_required
name: signed-alert
description: Investigate a signed alert
on:
webhook:
auth: { type: hmac }
repositories:
include: all
agent:
prompt: |
Look into {{ payload.title }} in {{ repository.fullName }}.
Fix it by adding preset: forkline (or github, sentry).
Header on an HMAC preset
- Pointer:
/on/webhook/auth/header - Message:
`header` is only for `type: token`; an HMAC preset fixes its header. - Code:
field_not_allowed
The same code is used for preset with type: token (message `preset` is only for `type: hmac`., pointer /on/webhook/auth/preset).
name: signed-alert
description: Investigate a signed alert
on:
webhook:
auth: { type: hmac, preset: forkline, header: X-Signature }
repositories:
include: all
agent:
prompt: |
Look into {{ payload.title }} in {{ repository.fullName }}.
Reserved or invalid header name
- Pointer:
/on/webhook/auth/headeror/on/webhook/idempotencyHeader - Message:
`Host` is set by the platform and cannot be used here.(header_reserved), or`{name}` is not a valid header name: use letters, digits and!#$%&’*+-.^_|~`` and at most 64 bytes. `` (header_name_invalid`) - Also
header_reserved:`{name}` already carries the credential; use another header for the idempotency key.
name: token-alert
description: Investigate an alert
on:
webhook:
auth: { type: token, header: Host }
repositories:
include: all
agent:
prompt: |
Look into {{ payload.title }} in {{ repository.fullName }}.
repositories
include is a string other than all
- Pointer:
/repositories/include - Message:
`everything` is not valid here, use "all" or a list of selectors - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: everything
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Empty include list
- Pointer:
/repositories/include - Message:
use "all" or list at least one selector - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: []
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include:
- name: acme/api
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Selector with both or neither of id and name
- Pointer:
/repositories/include/0 - Message:
set exactly one of `id` and `name` - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include:
- id: 3f2b8c1e-1c2d-4e5f-8a9b-0c1d2e3f4a5b
name: acme/api
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include:
- name: acme/api
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Name without owner/
- Pointer:
/repositories/include/0/name - Message:
use the form `owner/name`, with `*` and `**` as wildcards - Code:
selector_name_format
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include:
- name: api
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include:
- name: acme/api
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
filters
Event filter on a schedule or webhook routine
- Pointer:
/filters/labels - Message:
`labels` only applies to routines started by events - Code:
filter_not_applicable
Wrong:
name: review-prs
description: Review pull requests
on:
schedule:
cron: "0 6 * * 1"
repositories:
include: all
filters:
labels:
include: [deps]
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
Fixed:
name: review-prs
description: Review pull requests
on:
schedule:
cron: "0 6 * * 1"
repositories:
include: all
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
Filter that does not apply to a listed event
- Pointer:
/filters/draft - Message:
`draft` does not apply to push; every filter must apply to every listed event, so split the routine - Code:
filter_not_applicable
Split the routine in two when the events need different filters.
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened, push]
repositories:
include: all
filters:
draft: exclude
agent:
prompt: |
Check {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
draft: exclude
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
paths on a pull request event
- Pointer:
/filters/paths - Message:
`paths` does not apply to pr.opened; every filter must apply to every listed event, so split the routine - Code:
filter_not_applicable
paths works with push only.
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
paths:
include: ["src/**"]
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [push]
repositories:
include: all
filters:
paths:
include: ["src/**"]
agent:
prompt: |
Look at {{ push.branch }}.
bots or self on ci.failed
- Pointer:
/filters/bots - Message:
`bots` does not apply to ci.failed; every filter must apply to every listed event, so split the routine - Code:
filter_not_applicable
ci.failed is raised by Forkline and has no sender.
Wrong:
name: review-prs
description: Review pull requests
on:
events: [ci.failed]
repositories:
include: all
filters:
bots: include
agent:
prompt: |
CI {{ ci.name }} failed.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [ci.failed]
repositories:
include: all
agent:
prompt: |
CI {{ ci.name }} failed.
include and exclude both missing
- Pointer:
/filters/labels - Message:
set `include`, `exclude` or both - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
labels: {}
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
labels:
include: [needs-review]
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Empty list
- Pointer:
/filters/labels/include - Message:
must not be empty, remove it instead - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
labels:
include: []
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
labels:
include: [needs-review]
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Empty entry
- Pointer:
/filters/branches/include/0 - Message:
must not be empty - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
branches:
include: [""]
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
branches:
include: [main]
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
filters.command
command without names or mention
- Pointer:
/filters/command - Message:
set `names` or `mention: true` - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [comment.created]
repositories:
include: all
filters:
command: {}
agent:
prompt: |
Handle {{ comment.body }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [comment.created]
repositories:
include: all
filters:
command:
names: [fix-ci]
agent:
prompt: |
Handle {{ comment.body }}.
Invalid command name
- Pointer:
/filters/command/names/0 - Message:
a command name starts with a letter and has only letters, digits, `-` and `_`, written without the leading `/` - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [comment.created]
repositories:
include: all
filters:
command:
names: ["/fix-ci"]
agent:
prompt: |
Handle {{ comment.body }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [comment.created]
repositories:
include: all
filters:
command:
names: [fix-ci]
agent:
prompt: |
Handle {{ comment.body }}.
Command listed twice
- Pointer:
/filters/command/names/1 - Message:
`Fix-CI` is listed twice - Code: none defined
Names are compared case-insensitively.
Wrong:
name: review-prs
description: Review pull requests
on:
events: [comment.created]
repositories:
include: all
filters:
command:
names: [fix-ci, Fix-CI]
agent:
prompt: |
Handle {{ comment.body }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [comment.created]
repositories:
include: all
filters:
command:
names: [fix-ci, review]
agent:
prompt: |
Handle {{ comment.body }}.
filters.when
Empty condition
- Pointer:
/filters/when - Message:
must not be empty - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: ""
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: pr.labels.size() > 0
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Condition does not parse
- Pointer:
/filters/when - Message:
invalid expression `pr.labels.size() >`: {parser detail} - Code:
when_type
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: "pr.labels.size() >"
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: "pr.labels.size() > 0"
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Unknown field in the condition
- Pointer:
/filters/when - Message:
`pr.lables` is not a field, available fields are `pr.number`, `pr.title`, `pr.body`, `pr.author`, `pr.authorIsBot`, `pr.baseBranch`, `pr.headBranch`, `pr.headSha`, `pr.labels`, `pr.draft`, `pr.fork`, `pr.url` - Code:
when_type
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: pr.lables.size() > 0
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: pr.labels.size() > 0
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Field that no listed event carries
- Pointer:
/filters/when - Message:
`issue` is not available: the routine's events (pr.opened) never carry it - Code:
when_type
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: issue.title.contains("bug")
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: pr.title.contains("bug")
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Root that is not a variable
- Pointer:
/filters/when - Message:
`event` is not a variable, use `repository`, `sender`, `pr`, `issue`, `comment`, `ci` or `push` (the ones your events carry) - Code:
when_type
event, receivedAt, schedule and payload are not available in when.
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: event.name == "pr.opened"
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: pr.draft == false
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Function that is not supported
- Pointer:
/filters/when - Message:
`.lowerAscii()` is not a supported function, or takes other arguments; filters may use size(), contains(), startsWith(), endsWith(), matches(), string(), int(), dyn(), has() and the macros all(), exists(), exists_one(), map() and filter() - Code:
when_type
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: pr.title.lowerAscii() == "wip"
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: pr.title.startsWith("WIP")
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Condition is not a boolean
- Pointer:
/filters/when - Message:
a filter must evaluate to a boolean, but this one is string - Code:
when_type
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: pr.title
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: pr.title != ""
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Condition is too expensive
- Pointer:
/filters/when - Message:
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 by the size of the list they run over (up to the declared limits), so move expensive work out of them, avoid comparing or concatenating whole lists, and keep regular expressions small - Code:
when_cost
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: >-
pr.labels.all(a, pr.labels.all(b, pr.labels.all(c, pr.labels.all(d, pr.body.contains(a + b + c + d)))))
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: pr.labels.exists(l, l.startsWith("team-"))
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
matches() pattern is too long
- Pointer:
/filters/when - Message:
the regular expression is {n} bytes, the limit is 256; use a shorter pattern - Code:
when_type
A pattern is compiled once, when the routine is saved, and priced by the states it builds (see the cost note under Operators and functions). Its length is capped separately at 256 bytes, so no filter can make the control plane parse and price an arbitrarily large expression. Keep the list of alternatives to the ones you actually have, or move the detailed check into the prompt and let the agent read the title.
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: 'pr.title.matches("^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|release|hotfix|spike|wip|deps|devdeps|security|i18n|l10n|a11y|db|sql|api|ui|ux|infra|tooling|templates|examples|migrations|probe)([(][a-z0-9._/-]{1,40}[)])?!?: [A-Z][A-Z0-9]+-[0-9]+ ?[a-z].{1,200}$")'
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
filters:
when: 'pr.title.matches("^(feat|fix|docs|refactor|perf|test|build|ci|chore|revert|security)([(][a-z0-9._/-]{1,40}[)])?!?: [A-Z][A-Z0-9]+-[0-9]+")'
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
when on a schedule or webhook routine reads an event
- Pointer:
/filters/when - Message:
`pr` is not available: this routine is not started by an event, only `repository` is - Code:
when_type
Only repository exists there.
Wrong:
name: review-prs
description: Review pull requests
on:
schedule:
cron: "0 6 * * 1"
repositories:
include: all
filters:
when: pr.draft
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
Fixed:
name: review-prs
description: Review pull requests
on:
schedule:
cron: "0 6 * * 1"
repositories:
include: all
filters:
when: repository.private == false
agent:
prompt: |
Check {{ repository.fullName }} for outdated dependencies.
agent
Empty prompt
- Pointer:
/agent/prompt - Message:
must not be empty - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: ""
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
Template does not parse
- Pointer:
/agent/prompt - Message:
the template does not parse: {template parser detail} - Code:
template_syntax
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: "Review {{ pr.title }"
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: "Review {{ pr.title }}"
Variable the event does not provide
- Pointer:
/agent/prompt - Message:
`comment.body` isn't available for `pr.opened`. - Code:
template_unknown_variable
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: |
Reply to {{ comment.body }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: |
Review {{ pr.title }}.
Variable that only some of the listed events provide
- Pointer:
/agent/prompt - Message:
`pr.title` isn't available for `issue.opened`; every chosen event must provide it. - Code:
template_unknown_variable
Every variable must exist for every listed event.
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened, issue.opened]
repositories:
include: all
agent:
prompt: |
Look at {{ pr.title }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened, issue.opened]
repositories:
include: all
agent:
prompt: |
Look at {{ repository.fullName }}.
Unknown filter
- Pointer:
/agent/prompt - Message:
`shout` is an unknown template filter - Code:
template_unknown_filter
A filter or is test that the template engine does not have is refused at save, so the run cannot fail on it later. The filters page lists the ones that exist.
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: |
Review {{ pr.title | shout }}.
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: |
Review {{ pr.title | upper }}.
Reserved variable
- Pointer:
/agent/prompt - Message:
`thread.comments` is not supported yet - Code:
reserved_variable
Wrong:
name: review-prs
description: Review pull requests
on:
events: [comment.created]
repositories:
include: all
agent:
prompt: |
History: {{ thread.comments }}
Fixed:
name: review-prs
description: Review pull requests
on:
events: [comment.created]
repositories:
include: all
agent:
prompt: |
Comment: {{ comment.body }}
Empty model
- Pointer:
/agent/model - Message:
must be between 1 and 200 characters - Code:
range
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
model: ""
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
model: anthropic/claude-sonnet-4-5
Empty ref
- Pointer:
/agent/ref - Message:
must be between 1 and 1024 characters - Code:
range
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
ref: ""
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
ref: "{{ pr.headBranch }}"
timeoutMinutes out of range
- Pointer:
/agent/timeoutMinutes - Message:
must be between 1 and 240 - Code:
range
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
timeoutMinutes: 300
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
timeoutMinutes: 60
limits
maxRunsPerPr out of range
- Pointer:
/limits/maxRunsPerPr - Message:
must be between 1 and 50 - Code:
range
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
limits:
maxRunsPerPr: 0
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
limits:
maxRunsPerPr: 3
Reserved resetOn
- Pointer:
/limits/resetOn - Message:
`limits.resetOn` is not supported yet - Code: none defined
Wrong:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
limits:
maxRunsPerPr: 3
resetOn: success
Fixed:
name: review-prs
description: Review pull requests
on:
events: [pr.opened]
repositories:
include: all
agent:
prompt: Review {{ pr.title }} in {{ repository.fullName }}.
limits:
maxRunsPerPr: 3
Size limits
These errors come from lists and texts that are too long. The examples would need many lines, so they are described here instead of shown. Each one has the pointer and message shown.
| Pointer | Message | Limit |
|---|---|---|
/on/events |
list between 1 and 8 events |
1 to 8 events |
/repositories/include, /repositories/exclude |
at most 50 selectors, this has 51 |
50 selectors per list (code list_too_long) |
/repositories/include/{i}/name |
the pattern is longer than 256 bytes |
256 bytes per name pattern |
/filters/{name}/include, /filters/{name}/exclude |
at most 50 entries, this has 51 |
50 entries per list, for branches, paths, labels and authors (code list_too_long) |
/filters/{name}/include/{i} |
is longer than 256 bytes |
256 bytes per entry |
/filters/command/names |
list between 1 and 20 command names |
1 to 20 names |
/agent/prompt |
is 32769 bytes, the limit is 32768 |
32 KiB (code template_too_long) |
/agent/model |
must be between 1 and 200 characters |
1 to 200 characters |
/agent/ref |
must be between 1 and 1024 characters |
1 to 1024 characters |
/filters/when |
filter is 4804 bytes, the limit is 4096 |
4096 bytes (code when_type) |
/filters/when |
filter nests expressions more than 16 levels deep |
16 levels (code when_type) |
/filters/when |
filter is too large, it may use at most 512 expressions |
512 expressions (code when_type) |
/filters/when |
the regular expression is 260 bytes, the limit is 256; use a shorter pattern |
256 bytes per matches() pattern (code when_type) |
Not supported yet
Some names are reserved for features that are not available yet. Forkline refuses them instead of ignoring them.
| Pointer | Message |
|---|---|
/limits/resetOn |
`limits.resetOn` is not supported yet |
/on/events/{i} |
`{name}` is not supported yet for pr.conflicted, ci.succeeded, pr.labeled, issue.labeled, issue.assigned and pr.review_requested |
/agent/prompt, /agent/ref |
`{variable}` is not supported yet for comment.prompt, comment.path, comment.requestedModel and thread.comments |
The spec reference explains each of them.
Errors that need more than the file
The validator alone cannot reproduce these, so they have no fenced example. They come from the API, the CLI or the web editor.
| Where | Status and pointer | Message or shape | Code |
|---|---|---|---|
Create or update, name |
400, no pointer |
Routine name cannot be empty, Routine name cannot exceed 100 characters or Use only letters, numbers, dashes (-) and underscores (_), with no spaces.. The validate endpoint reports the same problem as an issue at /name, with the code range for an empty or too long name and none for the characters. |
none |
| Save, schedule or webhook routine that matches no repository of yours | 422, /repositories |
the selection matches none of your repositories, so the routine would never run. validate returns the same 422. |
none |
| Save, schedule or webhook routine that matches more than 200 repositories | 422, /repositories |
the selection matches {count} repositories, over the limit of 200; narrow it with `include` and `exclude` |
fan_out_too_large |
Save, concurrency.group that does not fit the events |
422, /concurrency/group |
group `pr` needs a routine that triggers on only pull request events, `comment.created` and `ci.failed`, use `repository` or `routine` instead |
group_needs_events |
| Create or publish beyond the account limits | 409 |
25 published and 100 routines in total per owner. Re-enabling a disabled routine also counts against the 25 published | none |
| Create or update with a name you already use | 409 |
A routine named `{name}` already exists. |
CONFLICT |
| Request body over the endpoint limit | 413 |
{"error": "The request body is larger than 256 KiB", "code": "PAYLOAD_TOO_LARGE"} (the number is the limit of the endpoint). Limits: validate and backtest bodies 256 KiB, drafts from an event 16 KiB, test body 6 MiB with the event payload at most 5 MiB (Payload is 5.4 MiB; the limit is 5 MiB. for the payload). |
PAYLOAD_TOO_LARGE |
| Custom webhook body over 5 MiB | 413 |
{"error": "The request body is larger than 5 MiB", "code": "PAYLOAD_TOO_LARGE"}. Nothing is started. |
PAYLOAD_TOO_LARGE |
| Custom webhook that fails unexpectedly | 500 |
{"error": "Internal server error", "code": "INTERNAL_ERROR"}. The cause is not shown. Nothing is started; retry later. |
INTERNAL_ERROR |
| Test with a payload that is not an event routines listen to | 422, /source/payload |
`pull_request` action `labeled` is not an event routines listen to. |
none |
| Test with a payload of a repository you have not registered | 422, /source/payload |
Repository `x/y` is not registered in Forkline. |
none |
| Test with the wrong combination of spec and source | 400 |
A spec sent to /routines/{id}/test, or a source that does not fit the trigger |
none |
| Validate, test or backtest above the rate limit | 429 |
{"error": ..., "code": "TOO_MANY_REQUESTS", "retry_after_secs": n} and a Retry-After header. Limits per owner and minute: validate 120, test 30, backtest 60. The CLI exits with code 9. |
none |
| Run now (a real run or a dry run), a test or a custom webhook while Forkline is busy | 429 |
{"error": "Forkline is busy; retry shortly", "code": "TOO_MANY_REQUESTS", "retry_after_secs": 5} and Retry-After: 5. No run is started. |
none |
| Run now above 10 real runs a minute | 429 |
{"error": ..., "code": "TOO_MANY_REQUESTS", "retry_after_secs": n} and a Retry-After header. The CLI exits with code 9. Dry runs count toward the 30 a minute shared with test. A request refused before it is evaluated does not count (Webhook and Run now limits); a real run that its filters refuse counts. The busy answer is in the row above. |
none |
| Run now on a draft | 409 |
The message starts with ROUTINE_DRAFT:. Publish first. A dry run of a draft works. |
CONFLICT |
Run now with a body over 6 MiB, or a payload over 5 MiB |
413 |
PAYLOAD_TOO_LARGE. Nothing is started. |
PAYLOAD_TOO_LARGE |
| Run now with fields that do not fit the trigger | 400 |
payload on a schedule routine, payload or repositories on an event routine (`payload` and `repositories` are not allowed: this routine runs on forge events; send an `event_id`), an unknown field in the body (Invalid request body: unknown field ...), event_id on a webhook or schedule routine, no event_id on an event routine, event_id together with repositories or payload, an empty repositories, or an event that is not a forge event |
none |
Run now with an event_id that does not exist |
404 |
Not found | none |
| Run now with a repository outside the selection | 422, /repositories/{i} |
The repository is not one the routine selects | repository_not_selected |
| Run now that a check blocks | 422, /repositories/{i} (/repositories when none was listed, /event_id for an event) |
One issue per blocked repository, with the decisive stage and the reason. Nothing is started | filters_not_matched |
| Run now replay that needs a field Forkline does not keep | 422, /event_id |
The message lists the fields (pr.body, issue.body, comment.body, comment.command.args, push.files) |
event_fields_not_stored |
| Run now replay of an event whose repository is no longer registered in Forkline | 422, /event_id |
The repository is not registered any more | event_repository_unavailable |
| Run now on more than 200 repositories | 422, /repositories |
Narrow the selection | fan_out_too_large |
| Webhook call with a wrong or missing credential | 401 |
Missing credentials or Invalid credentials (token), Missing signature or Invalid signature (HMAC). Nothing is started |
UNAUTHORIZED |
| Webhook calls above the per-IP or per-routine rate limit | 429 |
{"success": false, "message": "Webhook rate limit exceeded; retry later"} and a Retry-After header. Nothing is started |
none |
PUT /routines/{id}/webhook/secret on a routine with a generated secret, or a secret under 16 or over 512 bytes |
400 |
The message says what to do: rotate instead, or use a secret of 16 to 512 bytes | none |
| Publish or enable a routine whose pasted secret is not set yet | 400 |
The message asks you to paste the vendor’s secret | none |
| Rotate the secret of a routine with a pasted secret | 400 |
Rotate it at the sender and paste the new value | none |