View as Markdown

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: range for an empty list, too_many_events for 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/header or /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