View as Markdown

Test, backtest, recent events, run history

Agent hint

Rules on this page that most often cause wasted work or wrong conclusions:

  • Work in this order: validate, backtest, test, push. forkline routine validate checks the file, backtest replays the last 7 days of stored events, test runs one event through every stage, push sends the file. No test is required before publishing.
  • Test writes nothing: no task, no run record, no event, no forge write. Safe to run as often as you like.
  • Backtest only covers the last 7 days of stored event summaries (at most 5,000 per owner). It does not simulate duplicates, concurrency, run limits or credits.
  • Event summaries hold no bodies, comment text, command arguments or file lists. A when condition or a template that reads them is “not evaluated” (shown as would_run_unverified) on a recent event. Paste the real payload to check it.
  • Reasons are sentences written by the server. Print them verbatim, never rebuild them from the stage or outcome.
  • Read an outcome with its stage: the stage says which rule decided. See Why didn’t my routine run?.
  • Run history is kept for 7 days. A run row exists for each event, routine and repository.

Four tools let you check a routine without waiting for real activity, and read what it did afterwards:

Tool Question Writes anything? Window
Recent events What did my repositories send to Forkline? no last 7 days
Test What would this routine do with this one event? no one event or payload
Backtest How often would this routine have run? no last 7 days
Run history What did this routine do, and why? no last 7 days

Recent events

Recent events lists the events Forkline received from your repositories. Open it in the web app at /routines/events (the Recent events tab next to Routines), or use the CLI:

forkline routine events --repo acme/api --event pr.opened,pr.updated --since 3d --reacted none
forkline routine event 0000000e-0000-4000-8000-000000000003

Each row shows the event name, the title (the pull request or issue title, the comment command, the failed workflow names or the push summary), the number, the repository, the actor and the time. A row also shows the reactions: which routines considered the event and what each decided (up to 5 per row, newest first, with the total). “No routine reacted” means no routine considered the event, which is how you find events that nothing handles.

Opening one event (the side panel in the web app, forkline routine event <id>) shows its facts: the values you can turn into filters (event, repository, base branch, labels, author, draft, fork, bot sender, command), each with the filter it adds and whether it is checked by default, and all its reactions (up to 50).

Filters (web select, CLI flag, API query parameter):

Filter Web CLI Values
Repository Repository select --repo owner/name repository (exact full name, case-insensitive) or repository_id
Event Event select --event E[,E] pr.opened, pr.updated, pr.merged, pr.closed, issue.opened, comment.created, ci.failed, push
Routines Routines select --reacted any|none|some any (default), none (no routine considered it), some (at least one run record references it)
Window --since Nd 1 to 7 days (default 7)
Source event (forge webhook, default), schedule, webhook, manual: the last three exist only when a routine ran
Page size Load more --limit N API page size 1 to 200 (default 50). The CLI default is 50 and --limit is at most 1,000: it follows the paging cursor

Retention. Event summaries are kept for 7 days, and each owner keeps at most 5,000: the oldest are deleted first. When that happens inside the window you are looking at, the response says older_dropped: true and the web app shows a note that older events were removed. A backtest then reports truncated: true.

Event summaries (up to 4 KiB of metadata, no bodies or comment text) are kept for 7 days, for every event of every connected repository. This happens whether or not you use routines.

A summary holds the event name, forge, repository, actor, numbers, titles, branches, labels, the comment author and command name, the CI conclusion and failed run names, and the push ref. It never holds pull request or issue bodies, comment text, command arguments, changed-file lists, CI logs or the webhook payload. See Events.

Create a routine from an event

The fastest way to a correct routine is to start from an event that should have triggered it.

In the web app

  1. Open Recent events, pick an event and read its facts under Use as filters.
  2. Check or uncheck facts. Some start checked (event, repository, base branch, and the draft, fork and bot guards when the event is not a draft, from a fork or from a bot); labels and authors start unchecked, because a label filter means “any of” and pre-checking every label makes a narrow filter that is easy to misread.
  3. Click Create a routine for events like this. The editor opens with a draft, and a Live backtest · last 7 days panel on the right that updates 600 ms after every change.
  4. Adjust, then Save draft or Publish….

Provenance tags. The editor tags the values the draft endpoint set. fromEvent marks a value set from a checked fact; suggested marks a value Forkline proposed without a fact (for example the draft, bots and self filters, and the starter prompt). A tag disappears when you change the field. Tags live only in the editor session: they are not stored in the file, and the YAML tab does not show them. Through the API the same information is provenance[], with a JSON pointer into the spec, a kind of fromEvent or suggested, the fact key and a note.

Guards that stay open. When the event is a draft, comes from a fork or came from a bot, that guard fact reads “yes” and starts unchecked. If you leave it unchecked, the draft sets the filter to include (tagged suggested, with the note Left open: this event is a draft, ... comes from a fork or ... came from a bot) instead of the default exclude, so the draft runs on the event it was made from. Check the fact to make the routine run only for such events (only).

Widen hint. When sibling repositories share a prefix, the editor offers “Widen to acme/api-*? 5 repositories, would have run 31 times in 7 days.” with Widen and Not now. Widen keeps the event’s repository and adds the pattern next to it. The repository count and the replay count cover the event’s repository plus everything the pattern matches, which is what applying the hint selects. A pattern that adds no repository beyond the event’s is not offered. The API returns at most 2 suggestions.

The event you started from is pinned first in the backtest list, marked “the event you started from”. If your edits make the routine skip it, the panel says Your routine no longer matches the event you started from: {reason}.

With the CLI

forkline routine new review-api-prs --from-event 0000000e-0000-4000-8000-000000000001 --fact event --fact repository --fact baseBranch
forkline routine new --from-event 0000000e-0000-4000-8000-000000000001 --widen --out routines/review.yaml
Flag Meaning
[NAME] Routine name. Without it Forkline suggests one, for example review-acme-api-prs, unique among your routines (suffix -2, -3)
--from-event <id> The recent event to start from (a UUID from forkline routine events)
--fact KEY Apply this fact. Repeat it for several. Omitted means the event’s default-checked facts. Keys: event, repository, baseBranch, branch, headBranch, pr.number, label:<name>, author, draft, fork, bots, self, command, mention, ciWorkflow:<name>
--widen Select the event’s repository and the first widen suggestion’s pattern
--out PATH Where to write. Default .forkline/routines/<name>.yaml
--force Overwrite an existing file. Without it an existing file is refused with exit code 6

An unknown fact key, or one that does not apply to the event, is a 422 at /facts/{i}. new writes a local file only; push sends it. The file starts with a comment header that records where each part came from:

# Created by `forkline routine new --from-event 0000000e-…-0001` on 2026-10-16.
# from event: /on/events/0, /repositories/include/0, /filters/branches/include/0
# suggested: /filters/draft, /filters/bots, /agent/prompt
# widen: acme/api-* (covers 2 repositories, would have run 7 times in 7 days): rerun with --widen

The paths are JSON pointers into the spec: the same pointers as in validation errors. The widen hints are also printed on stderr.

Test

A test runs one event, or one pasted payload, through every stage of a routine and tells you what would happen. It writes nothing: no task, no run record, no event, no forge write (a read-only forge lookup for the trigger policy is allowed). It checks the file you give it, so you can test unsaved edits.

Every filter is evaluated, not only until the first failure: the first failing stage is marked decisive, and you see the next blocker without running again. The prompt is rendered even when a filter fails, so you can read it.

Inputs

Input Web (test panel) CLI API source.type
A recent event “Recent event” --event <uuid> recent_event
A pasted payload “Paste payload”: forge, event header, JSON --event <file> --forge github|gitlab|forgejo --event-header <header> payload
A repository, for a schedule “Repository (schedule)” --repository owner/name repository
A webhook body, for a custom webhook “Body (webhook)” --repository owner/name --body <file> webhook_body
  • A recent event carries only the summary. Fields that are not stored (pr.body, issue.body, comment.body, comment.command.args, push.files, CI logs) render in the prompt as ‹pr.body: not stored for recent events›, and a when that reads them is not evaluated. Paste the payload to test it fully.
  • Pasted payload. event_header is the value of the header the forge sends. GitHub: X-GitHub-Event (pull_request, issue_comment, push). GitLab: X-Gitlab-Event (Merge Request Hook, Note Hook, Pipeline Hook, Push Hook, Issue Hook). Forgejo: X-Forgejo-Event (pull_request, issue_comment, push). The forge mapping shows which of them become which event. Find the payload in the webhook delivery log of your forge. It is used for this test only and not saved. The payload’s repository must be one of your registered repositories; an unregistered one is a 422 at /source/payload (“Repository x/y is not registered in Forkline.”), and a registered repository that the routine does not select makes the repository stage filtered. A payload that is not an event routines listen to is also a 422 at /source/payload.
  • A ci.failed test from one workflow payload lists one failure. In production, one event is created for each failed run.
  • Schedule and webhook routines have no event. Use --repository (schedule) or --repository and --body (webhook).
  • Size limits. The payload or body is at most 5 MiB, otherwise 413 (“Payload is 5.4 MiB; the limit is 5 MiB.”). The request is at most 6 MiB. test shares its 30 per minute with dry runs. A test refused before it is evaluated does not count toward the 30 (Webhook and Run now limits). While Forkline is busy, a test answers 429 Forkline is busy; retry shortly with Retry-After: 5.
forkline routine test routines/review.yaml --event 0000000e-0000-4000-8000-000000000003
forkline routine test routines/review.yaml --event delivery.json --forge github --event-header pull_request
forkline routine test weekly-deps --repository acme/api
forkline routine test hook.yaml --repository acme/api --body body.json
forkline routine test review-prs --event 0000000e-0000-4000-8000-000000000003 --expect run --prompt-only
Flag Meaning
<file|ref> An existing path is a routine file; anything else is a routine name, UUID or id prefix (tests the stored spec)
--event <id|file> A UUID is a recent event; a path is a payload, and then --forge and --event-header are required (otherwise exit 2)
--forge, --event-header <header>, --delivery-id <id> Describe a payload file: the forge, the event header (pull_request, Merge Request Hook) and, optionally, the delivery id
--repository owner/name, --body FILE Schedule, or custom webhook
--prompt-only Print only the rendered prompt
--expect run|skip Exit code 10 when the result is not what you expect: run fails unless the outcome is would_run or would_run_unverified; skip fails when it is
--json The full TestRoutineResponse on stdout

An invalid routine file exits with 2 and prints the validation errors.

Stages and symbols

The result lists the stages that apply to the routine (filters you did not set are not listed), in evaluation order. Each line has a symbol:

Symbol Result Meaning
✓ pass The stage allowed the event
⊘ filtered The stage blocked the event. Neutral: a filter that blocks is not an error
✕ error The stage could not be evaluated (a template or condition error, a failed policy check)
· not_applicable or not_evaluated not_applicable: the event has no such thing. not_evaluated: the field is not stored in the summary

The decisive stage (the first one that is filtered or error) is marked. Four things are never checked in a test and are listed under not_evaluated: event_key (duplicates), concurrency (active runs), run_limit and credits.

The text of a passing stage line, by stage:

Stage Result Line
event pass {event} is one of {events}.
repository pass {repo} is selected. or {repo} is selected (matches {pattern}).
self_guard pass exclude: {sender} is not Forkline.; include: Forkline's own activity is allowed.; only: {sender} is Forkline.
bots pass exclude: {sender} is not a bot.; include: Bots are allowed.; only: {sender} is a bot.
draft pass exclude: {target} is not a draft.; include: Drafts are allowed.; only: {target} is a draft.
fork pass exclude: {target} is not from a fork.; include: Forks are allowed.; only: {target} is from a fork.
branches pass {branchKind} {branch} is allowed.
labels pass Labels {labels} pass the label filter.
authors pass {authorKind} {author} is allowed.
command pass The comment starts a line with {command}. or The comment mentions @forkline.
paths pass {m} of {n} changed files match., or Paths not checked: the forge sent no complete file list. when the list is incomplete
when pass The condition was true.
trigger_policy pass {sender} is allowed by the trigger policy ({policy}).
template pass The prompt renders ({bytes} bytes).
any stage not_applicable Not applicable: {event} has no {sender|pull request|comment|push}. and, for events Forkline derives, Not applicable: {event} is raised by Forkline and has no sender.
any stage not_evaluated {field} isn't stored for recent events, so {the condition|this filter} wasn't checked.
blocking result filtered, error the matching reason from the reason table

Predicted outcomes

Predicted outcome Meaning Reason
would_run Every stage passed Would start a run for {target}.
would_run_unverified Every stage that could be evaluated passed, but a when condition or filter needed a field that is not stored for recent events Would start a run for {target}, but {field} isn't stored for recent events, so {the condition|this filter} wasn't checked.
filtered A filter blocked the event (or the event or repository does not match) the decisive filter reason
condition_error The when condition could not be evaluated The condition could not be evaluated: {message}. No run was started.
template_error The prompt or the git ref could not be rendered The {prompt|git ref} could not be rendered: {message}{ (line {line})}. No run was started.
error Anything else, for example the trigger policy could not be checked The run could not be started: {message}.

The response also has the rendered_prompt (at most 64 KiB), the rendered_ref, and unverified_fields (for example ["pr.body"]).

Run now

With Run now you start a routine yourself, for real. It works for every trigger and is the counterpart of the webhook URL: connect another system with the webhook URL; start it yourself with Run now (web app, CLI or API). The same limits apply to both: concurrency, run limits, credits and the repository selection.

Only the owner can run a routine (others get 404), and only when it is published or disabled. A draft cannot be run for real, but it can be dry-run. A disabled routine runs from Run now; events, its schedule and its webhook don’t start it.

Dry run first, then Run now

In the web app, the Run drawer has two buttons. Dry run is a test of what Run now would start: it writes nothing and shows, per repository, the predicted outcome, the stages, the rendered prompt and an estimate (runs and the most credits they can cost). Run now starts the runs. The real run recomputes every check; it never trusts the dry run.

Routine Input Starts
Webhook A JSON payload of the same shape the webhook delivers, at most 5 MiB (default {}; the saved example is offered) One run per selected repository, with payload in the prompt and in when
Schedule Nothing One run per selected repository, as if the schedule fired now
Event A recent event (event_id) A replay of that stored event, for real
  • Payload from a file. In the web app, Load from file next to the payload box reads a JSON file of up to 5 MiB. A file over 256 KiB shows as a summary with Edit as text and Clear instead of filling the box.
  • Repositories. All selected repositories by default. The API and CLI can name a subset of the selection; anything else is refused with 422 repository_not_selected.
  • Filters apply. If a when or other check blocks a repository, a real run is refused as a whole with 422 filters_not_matched if any of its repositories is blocked, and nothing starts. With the default of all repositories, one blocked repository refuses the run. The CLI starts only the repositories a dry run says can run.
  • Event replay. The event must be a recent forge event (not a schedule, webhook or Run now row). Forkline keeps no bodies, comment text or file lists, so a replay is refused with 422 event_fields_not_stored when the prompt, agent.ref, when or a paths filter reads pr.body, issue.body, comment.body, comment.command.args or push.files (a paths filter needs push.files). comment.command.args counts only when the stored comment holds a command.
  • Event replay and aliases. The check follows the template however it reads the field, through an alias, pr['body'] or tojson. One case is not caught: a test on the text of a field through an alias of the whole object, such as {% set p = pr %}{% if "urgent" in p.body %}. A replay sees an empty body there, so read the field directly (pr.body). A dry run of such a replay lists the fields in not_stored, counts 0 runs, and the web app keeps Run now disabled. limits.maxRunsPerPr still applies. The trigger policy is skipped, because you are the one asking. For a comment command the run records that. If the event’s repository is no longer registered in Forkline, the answer is 422 event_repository_unavailable.
  • A fresh run each time. A replay is not deduplicated against the original delivery. Send an Idempotency-Key to make a repeated request start nothing new.
  • Active runs. If a run for the same group is active, Run now skips it with outcome skipped_active, even when the routine says onConflict: replace.
  • Limits. 10 real runs per minute per owner, for all routines together (429 with Retry-After beyond that). Dry runs count toward the 30 per 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 (422 filters_not_matched or event_fields_not_stored) counts. While Forkline is busy, a dry run answers 429 Forkline is busy; retry shortly with Retry-After: 5, like a real run. At most 200 repositories per run, as in Repository selection (422 fan_out_too_large). The payload is at most 5 MiB and the request at most 6 MiB. See Webhook and Run now limits.
  • Scope. An API token needs routines:execute.
forkline routine run alert-triage --payload alert.json --dry-run
forkline routine run alert-triage --payload alert.json
forkline routine run weekly-deps --repository acme/api --yes
forkline routine run review-prs --event 0000000e-0000-4000-8000-000000000003

The CLI always does a dry run first, prints the table and the estimate, and asks before it starts anything; --yes skips the question. See CLI workflow. The API is POST /v1/routines/{id}/run.

Backtest

A backtest replays the stored event summaries of the last 1 to 7 days through the pure stages: event type, repository selection, filters, when and a template check. It tells you how often the routine would have run before you publish it. It does not simulate duplicates (event_key), concurrency, run limits or credits, and it only knows what the summaries hold.

forkline routine backtest routines/review.yaml
forkline routine backtest review-prs --since 3d --show skipped --limit 20
forkline routine backtest routines/review.yaml --fail-if-zero
Flag Meaning
--since Nd Window, 1d to 7d (default 7d)
--show would-run|skipped|all Which events to list (default all). The counts always cover every event
--limit N At most N listed events (default 100, at most 500)
--fail-if-zero Exit code 10 when nothing would run. Events: the sum of would_run and would_run_unverified is 0. Schedule: runs per week times repositories is 0. Webhook: no repository is selected. Use it in CI

The summary line has this form:

Would run 9 (2 unverified) · skipped 31 · ≈ 9 sessions/week · up to 540 credits/week

followed by a table with the time, the event, the repository, number and title, the outcome and the reason of each listed event.

  • Would run counts events that passed every stage the backtest evaluates. Skipped are events that matched the event types and repositories but failed a filter.
  • Unverified (would_run_unverified, shown as “Would run?”): the event would run, but a when condition or filter needs a field the summary does not hold (for example pr.body). Unverified runs count as would-run, and the line says incl. {k} unverified.
  • Estimated sessions per week = (would run + would run unverified) × 7 ÷ days covered. The window can be shorter than the one you asked for when a repository was connected recently. For a schedule it is runs per week × repositories, and the response lists the next three run times; a custom webhook shows the cost per call and replays nothing.
  • Estimated credits are a maximum: ceil(sessions per week) × timeoutMinutes × size multiplier (see Run time, size and credits).
  • Group conflicts. The response has group_conflicts (group, on_conflict and count): the number of would-run events that came less than timeoutMinutes after an earlier would-run event of the same concurrency group. With replace those would have replaced an older run, with skip they would have been skipped. The web editor shows it as a line under the tiles. It is a rough hint: it does not simulate maxActive, run limits or credits.
  • The footnote reads: “Based on stored event summaries; credits and concurrency aren’t simulated.”

In the web editor the backtest is the Live backtest · last 7 days panel: tiles for Would run, Skipped by filters and Runner sessions / week, and a list of events (All, Would run, Skipped).

Publish dialog warnings

Publish… shows the backtest, the limits and your balance, and warns when:

  • the routine would have run 0 times in the last 7 days: “This routine would not have run on any event in the last 7 days. Check its filters, or publish anyway if you expect new activity.”;
  • it would have run more than 50 times in a week, or the worst case is above half of your balance: “This routine would have run 214 times last week (up to 12,840 credits). Consider narrowing its repositories or filters.”;
  • the balance is 0: “Your balance is empty, so runs will be refused until you add credits.”;
  • Forkline’s built-in automations may run on the same events (pull request opened, CI failed, comment mention).

No test is required before publishing. The warnings are advice. From the CLI, forkline routine push --publish publishes after the files validate.

Run history

Every time an event reaches a routine that listens to it, Forkline writes a run record. The web app shows them on the routine’s Activity tab, the CLI with forkline routine runs, the API with GET /routines/{id}/runs.

forkline routine runs review-prs
forkline routine runs review-prs --outcome limited,errors --repo acme/api --pr 412 --since 3d --limit 100
forkline routine runs review-prs --outcome run_limit
Flag Meaning
--outcome X[,Y] An outcome group (ran, skipped, limited, errors) or exact outcomes. An unknown value is a 400. skipped includes limited
--repo owner/name Only this repository
--pr N Only this pull request or issue number
--since Nd 1d to 7d (default 7d)
--limit N Default 50, at most 1,000 (the CLI follows next_cursor)

The output starts with the counts for the --since window (7 days by default): ran, skipped, limited, errors, for example Ran 4 · Skipped 0 · Limited 0 · Errors 0 (last 7 days). The counts are not narrowed by the other flags (--outcome, --repo, --pr). In the counts skipped does not include limited, while --outcome skipped returns both.

  • One row per event, routine and repository. An event that matches 5 repositories of one routine gives 5 rows. A schedule or webhook routine that selects 6 repositories gives 6 rows for one firing; beyond 50 rows for one occurrence, the rest are grouped by outcome and stage with a count (aggregated_count, and no repository).
  • Only events a routine considered appear. An event nothing listens to has no row; see it in Recent events.
  • Trigger sources: event (a forge webhook), schedule (on.schedule), webhook (POST /webhooks/custom/{id}, on.webhook) and manual (Run now). A manual row reads “Run now by you” in Activity: it records who started it and whether it came from the web app or an API token. A replay of an event also names the event it replays.
  • Retention: run records are kept for 7 days, and at most 10,000 per routine. Deleting a routine deletes its run records.
  • Trace. Each row has a trace: one entry per stage with the field it looked at, the observed value (up to 200 characters), the rule and a result: pass, filtered, error, not_applicable or not_evaluated. not_evaluated appears only in tests and backtests.
  • A row that started a run has a task with its status. If a newer run replaced it, the outcome reads superseded and task.superseded_by_task_id names the replacing task.
  • Overload rows. dropped_overload rows are written per routine, one every 60 seconds at most, with no event: the dropped events were never parsed, so the row has no event, repository or trigger summary.

Outcomes and stages

The outcome table

Every run record has an outcome, a stage (the rule that decided), a group (the chip in the web app and the value of --outcome) and a badge.

Outcome Stage Group Badge task
task_created submit ran Ran (green, check circle) set
superseded concurrency ran Replaced (blue, refresh) set, with superseded_by_task_id
filtered any filter stage, trigger_policy skipped Skipped (gray, minus) null
deduplicated event_key skipped Duplicate (gray, copy) null
skipped_active concurrency skipped Skipped (gray, pause) null
concurrency_limited concurrency (maxActive reached) or submit (the account’s pending-run cap) limited Limit (yellow, warning) null
run_limit run_limit limited Limit null
credit_exhausted credits limited Limit null
fan_out_refused fan_out limited Limit null
dropped_overload overload limited Limit null
template_error template errors Error (red, error) null
condition_error when errors Error null
error any errors Error null
would_run submit (test, backtest) Would run (green, check circle)
would_run_unverified the unverified stage (test, backtest) Would run? (green outline, help circle)
  • superseded is derived on read: Forkline never stores it. A run stored as task_created is returned as superseded (stage concurrency) once a newer run replaced its task, and its task.superseded_by_task_id is the replacing task. The list, the counts (group ran) and the filters agree: --outcome superseded returns the replaced runs, --outcome task_created the others, and --outcome ran both. A routine’s last_run (in routine list and view) keeps the stored outcome, so it still says task_created for a replaced run.
  • dropped_overload is a real outcome in the list. When Forkline is overloaded it drops events before they are evaluated and writes one row per routine and 60 seconds (stage overload, group limited). Filter them with --outcome dropped_overload or --outcome limited.
  • concurrency_limited has two meanings, told apart by the stage. Stage concurrency: the routine reached maxActive, no task exists. Stage submit: the account’s pending-run cap was reached, no task was created.
  • In the chips, Skipped includes the limit outcomes (shown with the yellow Limit badge); Ran includes replaced runs; Errors is template_error, condition_error and error.
  • filtered is also used for a comment.created actor the trigger policy refuses (stage trigger_policy).

The stages, in evaluation order: overload, fan_out, event, repository, self_guard, bots, draft, fork, branches, labels, authors, command, paths, when, trigger_policy, template, event_key, concurrency, run_limit, credits, submit. event and repository appear only in tests and backtests. See Filters for what each stage checks.

Rendering rules for reasons

  1. A reason is one sentence (two when “No run was started.” is added). It has no “Skipped:” prefix and is at most 500 characters. Only the server writes reasons. The web app and the CLI print them verbatim.
  2. Observed values, names, branches, labels, logins, patterns, events and repositories are in backticks and are truncated to 80 characters with ….
  3. A list {list} with 1 item shows `main`; with 2 to 5 items [`main`, `release/*`]; with more than 5 the first 5 and +N more. {inList} is `main` for one item and in [`main`, `release/*`] for several.
  4. Placeholders:
    • {target}: PR #412, issue #88, the push to main (push), or acme/api (schedule, webhook, Run now).
    • {groupTarget}: PR #412, issue #88, acme/api, or this routine.
    • {repo}: acme/api. {sender}: renovate[bot].
    • {branchKind}: Base branch (pull request events), Branch (push), Head branch (ci.failed).
    • {authorKind}: Author (pull request, issue), Comment author, Pusher.
    • {forge}: GitHub, GitLab, Forgejo.
    • {time}: 14:03 UTC when on the same UTC day as the run, otherwise 2026-10-15 14:03 UTC. The text is always UTC; the web app shows these times in your time zone and keeps the UTC text as a tooltip, while the CLI and the API show it as written.
    • {n}, {max}, {count}: plain integers with thousands separators.
  5. A suffix is appended with a single space.

Reasons

Every reason template, by outcome and stage. The first rows are run records. The two rows that start with would_run are predicted outcomes (test and backtest), and the last row is a suffix.

Outcome, stage, variant Reason template
task_created, submit Started a run for {target}.
superseded, concurrency A newer run for {target} replaced this one at {time}.
filtered, self_guard, self: exclude Caused by Forkline ({sender}); this routine skips Forkline's own activity.
filtered, self_guard, self: only Not caused by Forkline; this routine only runs on Forkline's own activity.
filtered, bots, exclude {sender} is a bot; bots are skipped.
filtered, bots, only {sender} is not a bot; this routine only runs on bot events.
filtered, draft, exclude Draft PRs are skipped.
filtered, draft, only {target} is not a draft; this routine only runs on drafts.
filtered, fork, exclude {target} comes from a fork; forks are skipped.
filtered, fork, only {target} is not from a fork; this routine only runs on forks.
filtered, branches, include {branchKind} {branch} is not {inList}.
filtered, branches, exclude {branchKind} {branch} matches the exclusion {pattern}.
filtered, labels, include {target} has none of the labels {list} (has: {labels}). ({labels} is a list or no labels)
filtered, labels, exclude Label {label} is excluded. (the first excluded label present)
filtered, authors, include {authorKind} {author} is not {inList}.
filtered, authors, exclude {authorKind} {author} is excluded.
filtered, command, names The comment has no {commands} command at the start of a line. ({commands} is /fix-ci, or /a or /b)
filtered, command, mention The comment doesn't mention @forkline.
filtered, command, names and mention The comment has no {commands} command at the start of a line and doesn't mention @forkline.
filtered, paths, include None of the {n} changed files match {list}.
filtered, paths, exclude All {n} changed files match the exclusions {list}.
filtered, paths, include and exclude None of the {n} changed files match {include} outside the exclusions {exclude}.
filtered, when The condition was false: {condition}. ({condition} is the source in backticks, at most 80 characters)
filtered, trigger_policy {sender} isn't allowed to trigger routines on {repo} (trigger policy: {policy}). ({policy} is anyone, owner_only, allowlist or collaborators)
filtered, event (test, backtest) This routine runs on {events}, not {event}.
filtered, repository (test, backtest) {repo} is not in this routine's repositories.
deduplicated, event_key, forge Already handled: {forge} delivered this event again (delivery {delivery}). ({delivery} is the id in backticks, at most 12 characters plus …)
deduplicated, event_key, schedule Already handled: the run scheduled for {time} was already started.
deduplicated, event_key, webhook Already handled: a call with the same idempotency key ran before.
deduplicated, event_key, manual Already handled: a run with this idempotency key was already started.
skipped_active, concurrency A run for {groupTarget} is still active; this routine skips new events while one runs. (for Run now: …; Run now skips while one runs.)
concurrency_limited, concurrency {n} runs of this routine were already active (limit {max}). This event was not run.
concurrency_limited, submit Your account already has {n} pending runs (limit {max}), so this run was not started.
run_limit, run_limit PR #{number} already had {count} runs from this routine (limit {max} per pull request). (1 run when the count is 1)
credit_exhausted, credits Your credit balance is empty. Add credits to resume.
fan_out_refused, fan_out This {schedule|webhook} now matches {n} repositories, above the limit of 200. Narrow the repository selection.
template_error, template The {prompt|git ref} could not be rendered: {message}{ (line {line})}. No run was started.
condition_error, when The condition could not be evaluated: {message}. No run was started.
dropped_overload, overload Forkline was overloaded and dropped {n} events for this routine between {t1} and {t2}.
error, trigger_policy The trigger policy for {repo} could not be checked: {message}. No run was started.
error, any other The run could not be started: {message}.
would_run, submit Would start a run for {target}.
would_run_unverified, the unverified stage Would start a run for {target}, but {field} isn't stored for recent events, so {the condition|this filter} wasn't checked.
suffix on task_created and would_run when the paths stage passed on an incomplete list Paths not checked: the forge sent no complete file list.

The predicted outcomes filtered, condition_error, template_error and error use the same templates as the run records of that outcome and stage.

Why didn’t my routine run?

Open the routine’s Activity tab (or forkline routine runs <routine>), find the event, and read the outcome and stage. If there is no row at all, the event never reached the routine: see the first line.

Symptom Outcome and stage What to do
No row, and the event is not in Recent events none The forge did not send a webhook for it, or the webhook is not set up. Check the repository’s webhook delivery log. Actions Forkline does not map (labeling, assignment, review requests, cancelled CI) are dropped silently, see Events
No row, but the event is in Recent events with “no routine reacted” none The routine does not list the event name in on.events, does not select the repository, or is a draft or disabled. Run forkline routine test <routine> --event <id>: stage event or repository is filtered
filtered at self_guard, bots, draft or fork filtered The default filters skip Forkline’s own activity, bots, drafts and forks. Set the filter to include or only
filtered at branches, labels, authors, command, paths filtered The event does not match the filter: read the reason and fix the filter or the event
filtered at when filtered The condition was false
filtered at trigger_policy filtered The person who commented is not allowed by the repository’s trigger policy
condition_error at when condition_error The when expression failed at run time. Test with the payload to see the message
template_error at template template_error The prompt or agent.ref could not be rendered. Test the event and read the line number
deduplicated at event_key deduplicated The forge delivered the same event twice, or an Idempotency-Key was reused
skipped_active at concurrency skipped_active A run for the same group is still active and onConflict is skip. Applies to events, schedules, Run now and custom webhooks. Run now always skips, whatever onConflict says. For a Run now, wait for the active run. For events, schedules and webhooks, replace also works
Run shows “Replaced” superseded at concurrency A newer event for the same pull request replaced it. Set onConflict: skip to keep it
concurrency_limited at concurrency concurrency_limited The routine reached maxActive. The event is not queued. Raise maxActive or run it again later
concurrency_limited at submit concurrency_limited Your account has too many pending runs: no task was created, and with replace the older run was not cancelled. Wait for earlier runs to start or finish
run_limit at run_limit run_limit The pull request reached limits.maxRunsPerPr (default 3), counting replaced runs. Raise the limit or open a new pull request
credit_exhausted at credits credit_exhausted Your balance is empty. Add credits
fan_out_refused at fan_out fan_out_refused A schedule or webhook now matches more than 200 repositories. Narrow the selection
dropped_overload at overload dropped_overload Forkline was overloaded. Nothing to fix in the routine
error at any stage error See the message.
A schedule did not fire row with source schedule, or none Check forkline routine backtest <routine> for the next three run times, and that the routine is published and enabled
Backtest says 0 but you expected runs none Window is 7 days of stored summaries. A filter that reads a field that is not stored (pr.body) shows as unverified, not as 0. Check filtered_by_stage with --json