---
title: "Validation errors"
description: "The 422 error format, pointers and codes, and every validation error with a wrong and a fixed example."
section: "routines"
url: "https://forkline.dev/docs/routines/errors"
markdown: "https://forkline.dev/docs/routines/errors.md"
source: "src/content/docs/routines/errors.mdx"
---

# Validation errors

> [!TIP]
> **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:

```json
{
  "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](https://www.rfc-editor.org/rfc/rfc6901) 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](#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`:

```text
.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:

```yaml routine-invalid pointer=/filters/labels/inlcude code=unknown_field
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:

```yaml routine
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:

```yaml routine-invalid pointer=/agent/prompt code=missing_field
name: review-prs
description: Review pull requests
on:
  events: [pr.opened]
repositories:
  include: all
agent:
  model: anthropic/claude-sonnet-4-5
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/agent/timeoutMinutes code=wrong_type
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:

```yaml routine
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:

```yaml routine-invalid pointer=/agent/runnerSize code=enum
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/draft code=enum
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:

```yaml routine
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:

```yaml routine-invalid pointer=/on code=on_exactly_one
name: review-prs
description: Review pull requests
on:
  events: [pr.opened]
  webhook: {}
repositories:
  include: all
agent:
  prompt: Review {{ pr.title }} in {{ repository.fullName }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/on/events code=range
name: review-prs
description: Review pull requests
on:
  events: []
repositories:
  include: all
agent:
  prompt: Review {{ pr.title }} in {{ repository.fullName }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/on/events/0 code=unknown_event
name: review-prs
description: Review pull requests
on:
  events: [pr.open]
repositories:
  include: all
agent:
  prompt: Review {{ pr.title }} in {{ repository.fullName }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/on/events/0 code=reserved_event
name: review-prs
description: Review pull requests
on:
  events: [ci.succeeded]
repositories:
  include: all
agent:
  prompt: |
    Check {{ repository.fullName }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/on/events/1
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:

```yaml routine
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:

```yaml routine-invalid pointer=/on/schedule
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:

```yaml routine
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:

```yaml routine-invalid pointer=/on/schedule/cron code=cron_invalid
name: review-prs
description: Review pull requests
on:
  schedule:
    cron: "every monday"
repositories:
  include: all
agent:
  prompt: |
    Check {{ repository.fullName }} for outdated dependencies.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/on/schedule/timezone code=timezone_unknown
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:

```yaml routine
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:

```yaml routine-invalid pointer=/on/schedule/timezone
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:

```yaml routine
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:

```yaml routine-invalid pointer=/on/schedule/at
name: review-prs
description: Review pull requests
on:
  schedule:
    at: "tomorrow 9am"
repositories:
  include: all
agent:
  prompt: |
    Check {{ repository.fullName }} for outdated dependencies.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/on/webhook/secret code=unknown_field
name: review-prs
description: Review pull requests
on:
  webhook:
    secret: s3cret
repositories:
  include: all
agent:
  prompt: |
    Look into {{ payload.title }} in {{ repository.fullName }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/on/webhook/auth/preset code=unknown_preset
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:

```yaml routine
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`

```yaml routine-invalid pointer=/on/webhook/auth/preset 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`).

```yaml routine-invalid pointer=/on/webhook/auth/header code=field_not_allowed
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. ``

```yaml routine-invalid pointer=/on/webhook/auth/header code=header_reserved
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:

```yaml routine-invalid pointer=/repositories/include
name: review-prs
description: Review pull requests
on:
  events: [pr.opened]
repositories:
  include: everything
agent:
  prompt: Review {{ pr.title }} in {{ repository.fullName }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/repositories/include
name: review-prs
description: Review pull requests
on:
  events: [pr.opened]
repositories:
  include: []
agent:
  prompt: Review {{ pr.title }} in {{ repository.fullName }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/repositories/include/0
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:

```yaml routine
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:

```yaml routine-invalid pointer=/repositories/include/0/name code=selector_name_format
name: review-prs
description: Review pull requests
on:
  events: [pr.opened]
repositories:
  include:
    - name: api
agent:
  prompt: Review {{ pr.title }} in {{ repository.fullName }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/labels code=filter_not_applicable
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/draft code=filter_not_applicable
name: review-prs
description: Review pull requests
on:
  events: [pr.opened, push]
repositories:
  include: all
filters:
  draft: exclude
agent:
  prompt: |
    Check {{ repository.fullName }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/paths code=filter_not_applicable
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/bots code=filter_not_applicable
name: review-prs
description: Review pull requests
on:
  events: [ci.failed]
repositories:
  include: all
filters:
  bots: include
agent:
  prompt: |
    CI {{ ci.name }} failed.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/labels
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/labels/include
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/branches/include/0
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/command
name: review-prs
description: Review pull requests
on:
  events: [comment.created]
repositories:
  include: all
filters:
  command: {}
agent:
  prompt: |
    Handle {{ comment.body }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/command/names/0
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/command/names/1
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/when
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/when code=when_type
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/when code=when_type
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/when code=when_type
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/when code=when_type
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/when code=when_type
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/when code=when_type
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/when code=when_cost
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:

```yaml routine
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](https://forkline.dev/docs/routines/filters#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:

```yaml routine-invalid pointer=/filters/when code=when_type
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:

```yaml routine
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:

```yaml routine-invalid pointer=/filters/when code=when_type
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:

```yaml routine
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:

```yaml routine-invalid pointer=/agent/prompt
name: review-prs
description: Review pull requests
on:
  events: [pr.opened]
repositories:
  include: all
agent:
  prompt: ""
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/agent/prompt code=template_syntax
name: review-prs
description: Review pull requests
on:
  events: [pr.opened]
repositories:
  include: all
agent:
  prompt: "Review {{ pr.title }"
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/agent/prompt code=template_unknown_variable
name: review-prs
description: Review pull requests
on:
  events: [pr.opened]
repositories:
  include: all
agent:
  prompt: |
    Reply to {{ comment.body }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/agent/prompt code=template_unknown_variable
name: review-prs
description: Review pull requests
on:
  events: [pr.opened, issue.opened]
repositories:
  include: all
agent:
  prompt: |
    Look at {{ pr.title }}.
```

Fixed:

```yaml routine
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](https://forkline.dev/docs/routines/templates#filters) page lists the ones that exist.

Wrong:

```yaml routine-invalid pointer=/agent/prompt code=template_unknown_filter
name: review-prs
description: Review pull requests
on:
  events: [pr.opened]
repositories:
  include: all
agent:
  prompt: |
    Review {{ pr.title | shout }}.
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/agent/prompt code=reserved_variable
name: review-prs
description: Review pull requests
on:
  events: [comment.created]
repositories:
  include: all
agent:
  prompt: |
    History: {{ thread.comments }}
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/agent/model code=range
name: review-prs
description: Review pull requests
on:
  events: [pr.opened]
repositories:
  include: all
agent:
  prompt: Review {{ pr.title }} in {{ repository.fullName }}.
  model: ""
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/agent/ref code=range
name: review-prs
description: Review pull requests
on:
  events: [pr.opened]
repositories:
  include: all
agent:
  prompt: Review {{ pr.title }} in {{ repository.fullName }}.
  ref: ""
```

Fixed:

```yaml routine
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:

```yaml routine-invalid pointer=/agent/timeoutMinutes code=range
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:

```yaml routine
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:

```yaml routine-invalid pointer=/limits/maxRunsPerPr code=range
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:

```yaml routine
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:

```yaml routine-invalid pointer=/limits/resetOn
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:

```yaml routine
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](https://forkline.dev/docs/routines/spec#not-supported-yet) 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](https://forkline.dev/docs/routines/concurrency-and-limits#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 |
