---
title: "Cookbook"
description: "Complete, validated routines to copy, and the built-in automations written as routines with every difference listed."
section: "routines"
url: "https://forkline.dev/docs/routines/cookbook"
markdown: "https://forkline.dev/docs/routines/cookbook.md"
source: "src/content/docs/routines/cookbook.mdx"
---

# Cookbook

> [!TIP]
> **Agent hint**
> Copy the closest recipe, change `repositories`, then run `forkline routine validate <file>`, `forkline routine backtest <file>` and `forkline routine push <file>`.
>
> - Every recipe states the events it uses and why its filters and template variables are legal for **all** of them. Keep that property when you edit: a filter or variable that one listed event does not provide is a `422`.
> - Every recipe is a complete file (`name`, `description`, then the spec keys) and passes the validator as written.
> - `maxRunsPerPr` is **3 by default**. A routine that must run on every push to a pull request needs a higher value, as the review recipe shows.
> - `concurrency` is in the CI recipes ([Gaps](#gaps-the-recipes-cannot-close)). Without it the defaults apply: a new event for the same pull request or issue **replaces** the older run, and pushes, schedules, Run now and custom webhooks **skip** while a run of the same routine is active. See [Concurrency and limits](https://forkline.dev/docs/routines/concurrency-and-limits).
> - `maxActive` (default 10) caps the routine's active runs across all repositories; at the cap a new `/fix` elsewhere is `concurrency_limited`, not queued.
> - Reserved events (`pr.conflicted`, `ci.succeeded`, `pr.labeled`, `issue.labeled`, `issue.assigned`, `pr.review_requested`) are refused. See [Gaps](#gaps-the-recipes-cannot-close).

Each recipe has a one-line **What it costs** note. A run costs at most `timeoutMinutes × size multiplier` credits (`basic` 1, `advanced` 2, `pro` 4). Credits are charged for the time a run actually uses, so the real cost is usually lower. The runs per week are rough guesses for a mid-sized repository, not measurements: use [backtest](https://forkline.dev/docs/routines/testing-and-activity#backtest) for your own numbers. See [Run time, size and credits](https://forkline.dev/docs/routines/concurrency-and-limits#run-time-size-and-credits).

## Review pull requests

Events: `pr.opened`, `pr.updated`. Both are `pr.*` events, so `branches`, `labels`, `draft`, `fork`, `bots` and `self` all apply to both, and `pr.number`, `pr.title`, `pr.baseBranch` and `repository.fullName` exist for both.

```yaml routine
name: review-prs
description: Review every ready pull request that a person opens or pushes to
on:
  events: [pr.opened, pr.updated]
repositories:
  include:
    - name: "acme/api"
filters:
  draft: exclude
  bots: exclude
  self: exclude
  labels:
    exclude: [no-review]
agent:
  prompt: |
    Review pull request #{{ pr.number }} "{{ pr.title }}" into {{ pr.baseBranch }} in {{ repository.fullName }}.
    Look for bugs, missing tests and unclear naming. Post one summary comment.
    Do not push commits.
  runnerSize: advanced
  timeoutMinutes: 20
limits:
  maxRunsPerPr: 10
```

`draft: exclude`, `bots: exclude` and `self: exclude` are the defaults. They are written out so the intent is visible. A draft that is marked ready for review fires `pr.opened` again, so it is reviewed then. `maxRunsPerPr` is raised to 10 because the default 3 would stop reviewing a pull request after its third run: every later push is recorded as `run_limit` and starts nothing, and the count never resets. The `bots` and `self` filters judge who pushed, not who wrote the pull request; to skip pull requests written by a bot, use `when: '!pr.authorIsBot'` as in [Who to skip](https://forkline.dev/docs/routines/filters#who-to-skip-recipes).

What it costs: at most 40 credits a run (20 minutes, `advanced`). About 40 runs a week for a repository with 15 pull requests and two or three pushes each, so up to about 1,600 credits a week.

## Review only pull requests Forkline opened

Event: `pr.opened`. The filter is `self: only`, which applies to every `pr.*` event.

```yaml routine
name: review-forkline-prs
description: Review the pull requests that Forkline itself opened
on:
  events: [pr.opened]
repositories:
  include: all
filters:
  self: only
  fork: exclude
agent:
  prompt: |
    Forkline opened pull request #{{ pr.number }} "{{ pr.title }}" in {{ repository.fullName }}.
    Review it as a second pair of eyes and post one summary comment.
  runnerSize: basic
  timeoutMinutes: 15
```

`self` looks at the **actor** of the event, not at the author of the pull request. For `pr.opened` the actor is whoever opened (or reopened, or marked ready) the pull request. A pull request that Forkline opened and a person later reopens has a person as actor and is skipped. On GitLab and Forgejo connectors without an automation account, Forkline posts as a person, so `self` cannot tell its actions apart, see [`self` and `bots`](https://forkline.dev/docs/routines/filters#self-and-bots-the-actor-not-the-author).

What it costs: at most 15 credits a run (15 minutes, `basic`). About 5 runs a week if Forkline opens a few pull requests.

## Fix CI with a `ci.failures` loop

Event: `ci.failed`. It has no actor and no `draft`, `fork`, `labels`, `bots` or `self` filters; `branches` and `when` apply. The prompt reads `ci.*` and `pr.*`. A failed run that belongs to no pull request has no `pr`, so `when` requires a pull request number.

```yaml routine
name: fix-ci
description: Fix a failed CI run on a pull request and push to its branch
on:
  events: [ci.failed]
repositories:
  include:
    - name: "acme/api"
filters:
  branches:
    exclude: [main]
  when: 'pr.number != null'
agent:
  prompt: |
    CI "{{ ci.name }}" failed for pull request #{{ pr.number }} on {{ ci.branch }} ({{ ci.headSha }}).
    Failed runs:
    {% for f in ci.failures %}- {{ f.name }} ({{ f.conclusion }}): {{ f.url }}
    {% endfor %}
    Fetch the logs of each failed run with the git-api tool, find the cause,
    fix it and push the fix to {{ pr.headBranch }}. If the failure is not caused by
    the code (an outage, a flaky service), say so in a comment and do not push.
    Other workflows of the same commit may have failed too: list the failed runs of {{ ci.headSha }} with the git-api tool and fix them all in one push.
  runnerSize: advanced
  timeoutMinutes: 30
concurrency:
  group: pr
  onConflict: skip
limits:
  maxRunsPerPr: 3
```

`ci.failures` has `name`, `runId`, `url` and `conclusion` per entry and **no logs**. On GitHub and Forgejo each failed workflow sends its own event, and the entries of one event are only the failed run itself. With the default `onConflict: replace`, each later failure of the same pull request would cancel the running fix and every run would count toward `maxRunsPerPr`, so three failing workflows on one commit would use up the default 3. The recipe sets `onConflict: skip`: the first failure starts one run, the others arrive while it is active and are recorded as `skipped_active` (they start nothing and do not count), and the prompt tells the agent to fix every failed run of the commit. `maxRunsPerPr: 3` (the default 3, written out) is the loop guard: an agent whose fix fails CI again is stopped after three runs for the pull request. It never resets on its own.

What it costs: at most 60 credits a run (30 minutes, `advanced`), so at most 180 credits per pull request. About 5 to 10 runs a week for a repository with a busy CI.

## Triage new issues

Event: `issue.opened`. `labels`, `authors`, `bots` and `self` apply, and `issue.*` exists. `pr`, `draft` and `fork` do not, so none is used.

```yaml routine
name: triage-issues
description: Label and answer new issues
on:
  events: [issue.opened]
repositories:
  include: all
filters:
  labels:
    exclude: [wontfix, duplicate]
agent:
  prompt: |
    Triage issue #{{ issue.number }} "{{ issue.title }}" in {{ repository.fullName }} ({{ issue.url }}).
    Find out whether it is a bug, a feature request or a question, add the matching label,
    and ask for the missing details (version, steps, logs) in one comment.
    Do not change code.
  runnerSize: basic
  timeoutMinutes: 10
```

What it costs: at most 10 credits a run (10 minutes, `basic`). About 10 runs a week for an active repository.

## A `/fix` command in comments

Event: `comment.created`. `command` applies only to this event. The comment is on a pull request **or** an issue, so `pr` and `issue` can both be empty in the prompt; the template guards with `if`.

```yaml routine
name: fix-command
description: Run when a trusted person comments /fix
on:
  events: [comment.created]
repositories:
  include: all
filters:
  command:
    names: [fix]
agent:
  prompt: |
    {{ comment.author }} commented "/fix {{ comment.command.args }}" on {{ comment.url }}.
    {% if pr.number %}This is pull request #{{ pr.number }}. Check out its branch with git-api, make the fix and push to it.
    {% else %}This is issue #{{ issue.number }} "{{ issue.title }}". Make the fix on a new branch and open a pull request.
    {% endif %}
  runnerSize: advanced
  timeoutMinutes: 30
```

`maxRunsPerPr` (default 3) also counts command runs on a pull request, replaced ones included, so the fourth `/fix` on one pull request is `run_limit`. Raise it if people use the command often.

Names are written without the slash and match in any case. Only the first command line of a comment counts. On GitHub and Forgejo a comment on a pull request conversation has no `pr.headBranch`, so the prompt tells the agent to look the branch up. Who may issue the command is the repository [trigger policy](https://forkline.dev/docs/routines/filters#trigger-policy) (default `collaborators`), not part of the file.

What it costs: at most 60 credits a run (30 minutes, `advanced`). About 5 runs a week, one per command.

## Weekly dependency update

Trigger: `on.schedule` with `cron` and `timezone`. A schedule routine accepts only the `when` filter, and `when` sees only `repository`. The prompt reads `repository.*` and `schedule.scheduledAt`. It runs **once for each selected repository**. For one report across several repositories, select one repository and tell the agent in the prompt which others to read.

```yaml routine
name: weekly-deps
description: Update outdated dependencies every Monday morning
on:
  schedule:
    cron: "0 6 * * 1"
    timezone: Europe/Berlin
repositories:
  include:
    - name: "acme/*"
  exclude:
    - name: "acme/sandbox-*"
filters:
  when: '!repository.private'
agent:
  prompt: |
    Update outdated dependencies in {{ repository.fullName }} (scheduled for {{ schedule.scheduledAt }}).
    Group them by risk, run the tests, and open one pull request.
  runnerSize: basic
  timeoutMinutes: 30
```

What it costs: at most 30 credits a run (30 minutes, `basic`), once a week per repository: 10 matching repositories cost up to 300 credits a week.

## Sentry or any custom webhook

Trigger: `on.webhook`. Your system posts JSON to the routine URL, see [Custom webhooks](https://forkline.dev/docs/routines/spec#custom-webhooks). The body is `payload.*` in the prompt and in `when`. Name one repository: a webhook routine runs once for each selected repository.

Setup for a Sentry internal integration:

1. Save the routine below. `preset: sentry` makes Forkline check the `Sentry-Hook-Signature` header.
2. Paste the integration's **client secret** into Forkline: `forkline routine webhook sentry-alert --set-secret secret.txt` (the file holds the secret). Forkline never shows it back.
3. Publish the routine. In Sentry, set the integration's webhook URL to the routine URL (`forkline routine webhook sentry-alert` prints it) and subscribe to issue events.

The `when` skips what the agent should not look at: only newly created issues of level `error` or `fatal` start a run. Everything else is recorded as `filtered`. Sentry bodies carry the whole event, up to 5 MiB with the stack trace. That is fine: only the fields `when` reads count toward the [payload limits](https://forkline.dev/docs/routines/filters#reading-the-webhook-payload-with-payload).

```yaml routine
name: sentry-alert
description: Investigate a Sentry alert that was posted to the routine webhook
on:
  webhook:
    auth: { type: hmac, preset: sentry }
repositories:
  include:
    - name: "acme/api"
filters:
  when: 'has(payload.action) && payload.action == "created" && has(payload.data) && has(payload.data.issue) && has(payload.data.issue.level) && payload.data.issue.level in ["error", "fatal"]'
agent:
  prompt: |
    Sentry reports an issue for {{ repository.fullName }}.
    Title: {{ payload.data.issue.title | default("(no title)", true) }}
    Culprit: {{ payload.data.issue.culprit | default("unknown", true) }}
    Link: {{ payload.data.issue.web_url | default("", true) }}
    {% if payload.raw_body %}Raw body: {{ payload.raw_body }}{% endif %}
    Find the cause in the code. If you can fix it safely, open a pull request.
  runnerSize: advanced
  timeoutMinutes: 30
```

A body that is not JSON arrives as `payload.raw_body`. Sentry's deliveries carry no idempotency key Forkline reads, so each delivery runs. Use the integration's *issue* webhook, not an alert-rule action: issue alerts post a different body, and this `when` would filter every one of them. For a sender that does retry, set `on.webhook.idempotencyHeader` (default `Idempotency-Key`) so a repeated delivery starts no second run. The field names under `payload` are those of your sender: replace the Sentry ones above with what your integration posts.

What it costs: at most 60 credits a run (30 minutes, `advanced`). One run per alert; use `default(..., true)` and your sender's own filtering to keep this to about 10 a week.

## Monorepo: the `paths` filter

Event: `push`. `paths` applies to `push` only, together with `branches`, `authors`, `bots` and `self`. The prompt reads `push.*`.

```yaml routine
name: billing-service-changelog
description: Update the billing changelog when billing code changes on main
on:
  events: [push]
repositories:
  include:
    - name: "acme/platform"
filters:
  branches:
    include: [main]
  paths:
    include: ["services/billing/**"]
    exclude: ["services/billing/**/*.md"]
agent:
  prompt: |
    {{ push.commitCount }} commits landed on {{ push.branch }} ({{ push.before }}..{{ push.after }}).
    Update services/billing/CHANGELOG.md for the changes under services/billing.
  runnerSize: basic
  timeoutMinutes: 15
```

The push runs when at least one changed file matches. A push whose file list is incomplete also runs. A push to `main` while a run is active is skipped, not queued.

What it costs: at most 15 credits a run (15 minutes, `basic`). About 20 runs a week if the service changes daily.

## A `when` condition

Events: `pr.opened`, `pr.updated`. `when` reads typed fields only, and `pr.*` exists for both. It runs after the other filters.

```yaml routine
name: review-feature-prs
description: Review feature pull requests and anything labelled needs-review
on:
  events: [pr.opened, pr.updated]
repositories:
  include: all
filters:
  when: '!pr.title.startsWith("chore") && (pr.headBranch.startsWith("feat/") || "needs-review" in pr.labels)'
agent:
  prompt: |
    Review pull request #{{ pr.number }} "{{ pr.title }}" from {{ pr.headBranch }}.
  runnerSize: basic
  timeoutMinutes: 20
limits:
  maxRunsPerPr: 5
```

`when` cannot read the files of a pull request, and has no `comment.command`; use `comment.commandName` there. Prefer `startsWith()` and `contains()` to `matches()`: they cost less of the 50,000 unit budget. See [`when` conditions](https://forkline.dev/docs/routines/filters#when-conditions).

What it costs: at most 20 credits a run (20 minutes, `basic`). About 20 runs a week, since the condition skips many pull requests.

## Built-in automations written as routines

Forkline's built-in automations (CI auto-fix, auto PR review, bot mentions and mention commands, merge-conflict auto-fix) are settings, not routines. A routine can approximate three of them and cannot reproduce them exactly. Each section gives the closest routine and a table of **every** difference. The routines below are starting points: none reaches parity.

> [!WARNING]
> **Turn the built-in off, or both run**
> A built-in and a routine that react to the same event **both run** and start two tasks. Forkline does not merge them. When you move a feature to a routine, **turn the built-in off** for the repository first: [CI auto-fix](https://forkline.dev/docs/automation/ci-auto-fix), [auto PR review](https://forkline.dev/docs/automation/auto-pr-review), [merge-conflict auto-fix](https://forkline.dev/docs/automation/merge-conflicts) (each page says where, under the repository **Automation** tab). Bot mentions have no switch, see [Bot mentions](https://forkline.dev/docs/automation/bot-mentions); narrow the routine with `repositories` and `filters` so that it covers events the built-in does not. See also [Routines vs built-in automations](https://forkline.dev/docs/routines#routines-vs-built-in-automations).

### CI auto-fix

Built-in: [CI auto-fix](https://forkline.dev/docs/automation/ci-auto-fix). Closest routine: the [fix CI recipe](#fix-ci-with-a-cifailures-loop), with the failing runs listed in the prompt and the agent fetching logs itself.

```yaml routine
name: ci-auto-fix
description: Closest routine to the CI auto-fix built-in
on:
  events: [ci.failed]
repositories:
  include: all
filters:
  when: 'pr.number != null'
agent:
  prompt: |
    CI "{{ ci.name }}" failed for pull request #{{ pr.number }} on commit {{ ci.headSha }}.
    {% for f in ci.failures %}- {{ f.name }} ({{ f.conclusion }}): {{ f.url }}
    {% endfor %}
    Fetch the failing job logs with the git-api tool, fix the cause and push
    directly to {{ pr.headBranch }}.
    Other workflows of the same commit may have failed too: list the failed runs of {{ ci.headSha }} with the git-api tool and fix them all in one push.
  runnerSize: basic
  timeoutMinutes: 30
concurrency:
  group: pr
  onConflict: skip
limits:
  maxRunsPerPr: 3
```

Where this routine differs from the built-in:

| Area | Built-in CI auto-fix | This routine |
|------|----------------------|--------------|
| Failure logs | Fetched by Forkline (failed jobs, up to 50 KiB per failure) and put in the prompt | **No logs** in `ci.failures` (`name`, `runId`, `url`, `conclusion` only). The agent fetches them itself, which costs runner time |
| Several failures on one commit | A 5 minute collection window (+1 minute per extra failure, at most 10 minutes) gives **one task** with all failures | One event **per failed run or pipeline**. With the default `replace`, each later failure of the same pull request cancels the running fix, and every run counts toward `maxRunsPerPr`: three failing workflows on one commit use up the default 3. The recipe sets `onConflict: skip`, so the first failure starts one run and the others, while it is active, are `skipped_active` (they start nothing and do not count); the prompt tells the agent to fix every failed run of the commit |
| Dedupe per head SHA | One collection per repository and commit | None per commit. A forge redelivery of the same event is deduplicated, a second failed workflow of the same commit is not |
| Attempt limit | Setting per repository or account (default 3), counts consecutive attempts, **shared by all of a repository's CI failures** | `maxRunsPerPr` (default 3), counts every run of this routine for the pull request including replaced ones. One routine per distinct limit |
| Reset and resume | `/reset` or `/reset-ci-auto-fix` zeroes the counter and re-checks CI. A human push pauses it (GitHub, Forgejo) | No `/reset`, no `resetOn`, no pause on a human push. At the limit, runs are recorded as `run_limit` and **no comment** is posted |
| Cancel on success | A later passing run cancels pending fixes of the same check | `ci.succeeded` is reserved. Nothing is cancelled when CI passes again; a newer `ci.failed` for the pull request replaces the older run by default |
| CI sources | GitHub `workflow_run`, `check_run` and `status`; GitLab pipelines; Forgejo `action_run_failure`, `check_run` and Gitea `workflow_run`; external CI that reports a check or status | `workflow_run` (GitHub, Forgejo), `pipeline` (GitLab) and `action_run_failure` only. **External CI that reports only `check_run` or `status` is not seen** |
| `timed_out` | Ignored | Included: `ci.failed` fires for failed and timed-out runs |
| Fork pull requests | Skipped | `fork` does not apply to `ci.failed` and `pr.fork` is always `false` there. Add a `when` on the head branch if you need to avoid forks |
| Bots | No bot check: CI failures on Forkline's own commits are processed | `self` and `bots` do not apply to `ci.failed`; `maxRunsPerPr` is the only guard |
| Progress and result comments | Posts a started, success and failure comment, and a paused comment at the limit | No comments. The agent posts what its prompt asks for |
| Task priority | CI fixes outrank reviews | Routine runs have the default priority, not the CI-fix priority |
| Settings inheritance | A toggle per repository that overrides an account default | `repositories` selects repositories by name or id. A per-repository toggle becomes `exclude` entries, and a different limit becomes another routine |
| Forges | Per-forge log fetching | One routine for all forges, but the agent needs the forge tooling to fetch logs |

### Auto PR review

Built-in: [Auto PR review](https://forkline.dev/docs/automation/auto-pr-review). The built-in default scope is Forkline-authored pull requests only; the **all pull requests** scope is the first routine below, the default scope is the second.

```yaml routine
name: auto-pr-review-all
description: Closest routine to auto PR review with scope all pull requests
on:
  events: [pr.opened]
repositories:
  include: all
filters:
  draft: exclude
  fork: exclude
  labels:
    exclude: [no-auto-review]
agent:
  prompt: |
    Review pull request #{{ pr.number }} "{{ pr.title }}" by {{ pr.author }}
    ({{ pr.headBranch }} into {{ pr.baseBranch }}) in {{ repository.fullName }}.
    Provide your feedback as review comments. Do not make any code changes.
  runnerSize: basic
  timeoutMinutes: 20
```

```yaml routine
name: auto-pr-review-forkline-only
description: Closest routine to auto PR review with the default scope
on:
  events: [pr.opened]
repositories:
  include: all
filters:
  self: only
  draft: exclude
  fork: exclude
  labels:
    exclude: [no-auto-review]
agent:
  prompt: |
    Review pull request #{{ pr.number }} "{{ pr.title }}" ({{ pr.headBranch }} into {{ pr.baseBranch }}).
    Provide your feedback as review comments. Do not make any code changes.
  runnerSize: basic
  timeoutMinutes: 20
```

Where these routines differ from the built-in:

| Area | Built-in auto PR review | These routines |
|------|-------------------------|----------------|
| Trigger | Opened only | `pr.opened` also fires when a pull request is **reopened** or a draft is **marked ready for review** (GitHub, Forgejo), so a draft that becomes ready is now reviewed, and a reopened pull request is reviewed again |
| Scope `forkline_only` | Forkline-authored pull requests: the **author** is the bot | `self: only` judges the **actor**, so it matches only while the bot opens, reopens or readies the pull request. A person who reopens a Forkline pull request is skipped. On GitLab and Forgejo connectors without an automation account it matches nothing |
| Own bot rule | Scope `all` reviews Forkline's pull requests too | `self` defaults to `exclude`, so scope all must keep the default and **does not review Forkline's own pull requests**. Use `self: include` to cover both |
| Other bots | Reviews them | `bots` defaults to `exclude` and judges the **actor**: events caused by `renovate[bot]` and similar (opening or pushing to a pull request) are skipped, while a person's push to a bot's pull request still runs. Use `bots: include` to review bot activity, or `when: '!pr.authorIsBot'` to skip pull requests written by a bot (on GitLab only when the merge request is first opened, see [Filters](https://forkline.dev/docs/routines/filters#authors-and-labels)) |
| Draft and fork | Skipped | Same (`draft: exclude`, `fork: exclude`) |
| Skip label | `no-auto-review` | `labels.exclude: [no-auto-review]` as written, a label you must set yourself |
| Duplicate reviews | Skipped while a review of the pull request is pending or running | A new event for the pull request **replaces** the running one by default (`pr` group), and `maxRunsPerPr` (default 3) counts the runs |
| Action `commit` | Prompt asks for fixes on the PR branch | Write it in the prompt; the default ref is the pull request head branch (the base branch for forks) |
| Custom prompt per repository or account | Setting with fallback | Part of the file: one routine per distinct prompt, each with its own `repositories` |
| Repository toggle | Per repository, with an account default | `repositories` include and exclude |
| Trigger policy | Not checked | Not checked either: `opened` events are not subject to the trigger policy. Use `authors` to restrict who opens the pull request |
| Task priority | Reviews have a fixed priority | Default priority |
| Result | Posted as review comments by Forkline | Posted by the agent as the prompt asks |

### Bot mentions and issue response

Built-in: [Bot mentions](https://forkline.dev/docs/automation/bot-mentions). Comments that mention `@forkline` start a pull request review task on a pull request and an issue response task on an issue. A routine can answer mentions in comments with the `command.mention` filter.

```yaml routine
name: forkline-mention
description: Closest routine to bot mentions in comments
on:
  events: [comment.created]
repositories:
  include: all
filters:
  command:
    mention: true
agent:
  prompt: |
    {{ comment.author }} mentioned you on {{ comment.url }}:

    {{ comment.body }}

    {% if pr.number %}This is pull request #{{ pr.number }}.{% else %}This is issue #{{ issue.number }} "{{ issue.title }}".{% endif %}
    Read the conversation on the forge, then answer in a comment or make the change that was asked for.
  runnerSize: basic
  timeoutMinutes: 30
```

Where this routine differs from the built-in:

| Area | Built-in mentions | This routine |
|------|-------------------|--------------|
| Prompt | Only the text after the mention, up to the next mention; `/model` removed | The **whole** comment as `comment.body`, mention included. No `comment.prompt` |
| Several mentions in one comment | One task per mention | One run per comment |
| Mention in a pull request or issue **description** | Starts a task when opened | **Not supported**: `command` filters comments only |
| Label, assignment and review-request triggers (`forkline`, `ai-task`, `forkline-review` labels, bot assigned or asked to review) | Start tasks | **Not supported**: `pr.labeled`, `issue.labeled`, `issue.assigned` and `pr.review_requested` are reserved. Those webhook actions are dropped |
| Inline review comments | Passed with file path, line and diff hunk | Event fires, but without path, line or diff hunk |
| Conversation history | Fetched and attached to the task | Not attached; the agent reads the thread itself |
| Model selection | `/model <id>` and `model:` or `forkline:` labels, with a disambiguation comment | None. `agent.model` is fixed in the file |
| Task type | `pr_review`, `issue_response` or `autolabel`, chosen from the text | One routine, one prompt; branch in the template with `if pr.number` |
| Who may trigger | Sender trigger policy (default `collaborators`) for mentions, descriptions, labels and assignments | The same policy applies to `comment.created`, not to the other triggers, which a routine cannot express anyway |
| Bot-authored mentions | Create work on purpose | `self: exclude` (default) skips Forkline's own comments. `self: include` allows them (comments from a plain `forkline` account that is not the connector's automation account are still subject to the trigger policy), with `maxRunsPerPr` (default 3) as the only loop guard; there is no content-based loop check |
| Fork and draft | Not checked | `fork` and `draft` do not apply to `comment.created`; for comments on a pull request `pr.fork` and `pr.draft` are `false` |
| Reactions and replies | Reacts to the comment and replies to it | Neither: the routine run has no link to the comment beyond `comment.id` and `comment.url` in the prompt |
| Several users with the same repository URL | Picks the registering user's repository record | `repositories` selects the repositories of the routine owner only |
| Switch | No toggle | A routine is on or off per routine |

### Mention commands

Built-in: the commands in a mention, `/review`, `/autolabel`, `/model` and `/reset`. `/review` and `/autolabel` can be written as slash commands with `command.names`. `/model` and `/reset` cannot.

```yaml routine
name: review-and-label-commands
description: Closest routine to the review and autolabel mention commands
on:
  events: [comment.created]
repositories:
  include: all
filters:
  command:
    names: [review, autolabel]
agent:
  prompt: |
    {{ comment.author }} wrote "/{{ comment.command.name }} {{ comment.command.args }}" on {{ comment.url }}.
    {% if comment.command.name == "autolabel" %}Add the labels that fit to {% if pr.number %}pull request #{{ pr.number }}{% else %}issue #{{ issue.number }}{% endif %}. Do not change code.
    {% else %}Review {% if pr.number %}pull request #{{ pr.number }} and post review comments{% else %}the linked change{% endif %}. Do not change code.
    {% endif %}
  runnerSize: basic
  timeoutMinutes: 20
```

Where this routine differs from the built-in:

| Area | Built-in commands | This routine |
|------|-------------------|--------------|
| Command syntax | Recognised in the text of a mention | `command.names` matches the **first line that starts with** `/name` only. A mention does not need the slash, and `mention: true` is a separate condition that matches without any command |
| `/autolabel` natural language | "please label this" also forces the autolabel task | Not recognised. Only the exact command |
| `/model <id>` | Overrides the model for that task | Not available: no per-event model |
| `/reset` and `/reset-ci-auto-fix` | A platform action, **not** a task: zeroes the CI fix counter and re-checks CI | **Not expressible.** A routine run costs credits and cannot touch the built-in counter or `maxRunsPerPr` |
| Who may trigger | Trigger policy | Same policy for `comment.created` |
| Case | Command names are case-insensitive | Same |
| Bot-authored commands | Allowed | Skipped unless `self: include` |
| Reactions, history, inline context | Reaction on the comment, history attached | Not available, see the mention table above |

### Merge conflicts

Built-in: [Merge conflict auto-fix](https://forkline.dev/docs/automation/merge-conflicts). **A routine cannot detect conflicts.** The event that would carry it, `pr.conflicted`, is reserved and rejected at save. The `pr` fields have no mergeability flag, and there is no event for a push to the base branch of a pull request. The nearest thing is a routine that runs on every push to the default branch and asks the agent to find the open pull requests that no longer merge.

```yaml routine
name: merge-conflict-sweep
description: After a push to main, find open pull requests with conflicts and resolve them
on:
  events: [push]
repositories:
  include: all
filters:
  branches:
    include: [main]
agent:
  prompt: |
    {{ push.commitCount }} commits landed on {{ push.branch }} in {{ repository.fullName }}.
    Use git-api to list the open pull requests that target {{ push.branch }} and
    cannot be merged because of conflicts. For each, merge origin/{{ push.branch }}
    into its head branch (do not rebase), resolve the conflicts, run the tests and push.
    Skip pull requests from forks and say what you skipped.
  runnerSize: advanced
  timeoutMinutes: 45
```

Where this routine differs from the built-in:

| Area | Built-in merge-conflict auto-fix | This routine |
|------|----------------------------------|--------------|
| Detection | Forkline checks mergeability on pull request open, update and reopen, on commit statuses, and when the base branch is pushed; it re-checks unknown mergeability after 60 seconds, up to 5 times | **None.** The run starts on every push to the branch, whether or not any pull request conflicts, and the agent does the checking |
| One task per pull request | A task per conflicting pull request, with its own head branch | **One run per push**, covering all pull requests. `push` carries no pull request, so `pr.*` is not available and `limits.maxRunsPerPr` (default 3) cannot protect a single pull request |
| Attempt budget | Per pull request **and head commit**: a new push resets it, runs that never started do not count; setting default 3 | None per pull request or commit. Cap the cost with `timeoutMinutes` and the `skip` default (a push while a run is active is skipped) |
| Cost | Only when a conflict exists | A run for every push to `main`, conflict or not |
| Forks | Skipped | Asked for in the prompt only |
| Drafts | Not excluded | Not looked at in the file, the agent decides |
| GitLab | **Not supported** | The routine runs on GitLab pushes, but the agent must find merge requests itself |
| Priority, status comments | Fixed priority, started and resolved comments | Default priority, no comments unless the prompt asks |
| Conflicting files | The task knows the pull request, branches and SHAs | The agent has to find them |

## Gaps the recipes cannot close

These cannot be written as a routine today. Do not try them: the validator refuses them or the routine would not do what it says.

| Wanted | Why not | Closest |
|--------|---------|---------|
| React to a merge conflict (`pr.conflicted`) | The event is reserved and refused at `/on/events/{i}` with `` `pr.conflicted` is not supported yet `` | [Merge conflicts](#merge-conflicts) sweep on `push` |
| Stop when CI passes (`ci.succeeded`) | Reserved | A newer `ci.failed` replaces the older run by default |
| React to a label, an assignment or a review request | `pr.labeled`, `issue.labeled`, `issue.assigned`, `pr.review_requested` are reserved, and those webhook actions are dropped | A `when` on `pr.labels` for `pr.updated` runs on every push, not when the label is added |
| Command in a pull request or issue description | `command` applies to `comment.created` only | None |
| CI logs in the prompt | `ci.failures` has no logs | The agent fetches logs with git-api |
| One run for all failures of a commit | One event per failed run | `onConflict: skip` and a prompt that asks the agent to fix every failed run of the commit. No collection window |
| Trigger policy for events other than comments | The policy checks `comment.created` only | `authors` restricts who opens or pushes |
| Reset or pause the run limit | No `resetOn`, no `/reset` | Raise `maxRunsPerPr` (default 3, at most 50) |
| Cap parallel runs or change the replace behaviour | Not a gap: use `concurrency` | See [Concurrency and limits](https://forkline.dev/docs/routines/concurrency-and-limits) |

A routine that must not be replaced mid-run sets `onConflict: skip`, as both CI recipes above do. A second event for the same pull request while a run is active is then skipped instead of replacing the run. See [Concurrency and limits](https://forkline.dev/docs/routines/concurrency-and-limits).
