Spec reference
Agent hint
For agents writing routines. Follow these rules and the routine validates the first time:
- Write one routine per file.
- Unknown fields are rejected. A typo is an error, not an ignored setting.
- Filters must apply to every listed event. When the events need different filters, write two routines.
- Use only the template variables that every chosen event provides. See Prompt templates and variables.
- Run
forkline routine validate <file>, thenforkline routine backtest <file>, beforeforkline routine push. - Schedule and webhook routines accept no filters except
when. On a schedule routinewhensees onlyrepository; on a webhook routine it also seespayload. limits.resetOnand the events and variables in Not supported yet are rejected.filters.botsandfilters.selfjudge the actor (sender, who pushed, commented or opened), not the author of the pull request. To skip pull requests written by a bot, usewhen: '!pr.authorIsBot'(pull request events only; on GitLab only when the merge request is first opened, see Authors and labels). The defaults arebots: excludeandself: exclude: other bots and Forkline’s own bot are skipped. To skip only Forkline’s own activity and keep other bots, setbots: includeand leaveselfat its default. See self and bots.limits.maxRunsPerPrdefaults to 3 and counts every run for a pull request, replaced ones too, and never resets. A routine onpr.updatedthat must run on every commit needs a higher value (at most 50), or it stops after its third run with outcomerun_limit.ci.failedcarries a pull request only when the failed run belongs to one. If the prompt or filters need it, addwhen: 'pr.number != null'; without a pull requestpr.*is empty andmaxRunsPerPrdoes not apply.- There is no changed-files count, diff or file list for pull requests. See Not available variables.
A routine is a file that says what starts it (on), which repositories it covers (repositories), which events to ignore (filters) and what the agent does (agent). This page lists every field. The other pages explain the parts in depth: Events, Filters, Prompt templates and variables, Repository selection, Concurrency and limits and Validation errors.
The routine file
A routine file is a YAML or JSON mapping. A file that ends in .json is read as JSON, any other file as YAML. It starts with name and description, followed by the spec keys.
| Key | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | 1 to 100 characters: letters, digits, - and _. forkline routine push matches files to routines by name. A bad name is refused with 400 (see Validation errors). |
description |
string | no | Free text for people. |
on |
object | yes | What starts the routine: on |
repositories |
object | yes | repositories |
filters |
object | no | filters |
agent |
object | yes | agent |
concurrency |
object | no | concurrency. Optional; the defaults apply when it is missing. |
limits |
object | no | limits |
Every object rejects unknown fields. All keys are camelCase. Saving an invalid routine returns 422 with one message and pointer per problem, see Validation errors. In the web editor, in the API and in the pointers below, name and description are separate fields next to the spec; the pointers start at the spec.
A complete example
name: review-prs # letters, digits, - and _
description: Review pull requests that target main
on:
events: [pr.opened, pr.updated] # 1 to 8 events
repositories:
include:
- name: "acme/*" # owner/name, * and ** are wildcards
exclude:
- name: "acme/sandbox"
filters: # optional; every filter must fit every event
branches:
include: [main, "release/*"] # the pull request base branch
labels:
exclude: [no-review]
draft: exclude # exclude (default), include or only
fork: exclude
bots: exclude # the ACTOR is another bot (default), not the PR author
self: exclude # the ACTOR is Forkline's own bot (default)
when: pr.title.startsWith("WIP") == false # a typed CEL condition
agent:
prompt: |
Review pull request #{{ pr.number }} "{{ pr.title }}" in {{ repository.fullName }}.
Focus on correctness and tests. Post one summary comment.
model: anthropic/claude-sonnet-4-5
runnerSize: advanced # basic, advanced or pro
ref: "{{ pr.headBranch }}" # what the agent checks out
timeoutMinutes: 45 # 1 to 240
limits:
maxRunsPerPr: 3 # 1 to 50
on
Set exactly one of events, schedule and webhook.
| Pointer | Type | Required | Default | Constraint | Message when violated |
|---|---|---|---|---|---|
/on |
object | yes | Exactly one of events, schedule, webhook |
set exactly one of `events`, `schedule` and `webhook` |
|
/on/events |
string[] | one of three | 1 to 8 event names | list between 1 and 8 events |
|
/on/events/{i} |
string | One of the 8 event names, each at most once | Unknown event `{name}`. Did you mean `{near}`? Events: `pr.opened`, `pr.updated`, `pr.merged`, `pr.closed`, `issue.opened`, `comment.created`, `ci.failed`, `push`. (the suggestion only when one name is close), `{name}` is listed twice, `{name}` is not supported yet |
||
/on/schedule |
object | one of three | Exactly one of cron and at |
set exactly one of `cron` and `at` |
|
/on/schedule/cron |
string | one of cron, at |
Standard cron: five fields (minute hour day month weekday), or six or seven with leading seconds. The weekday is 0 or 7 for Sunday, 1 for Monday up to 6 for Saturday, or MON to SUN in any case, with ranges, lists and steps. When both the day of the month and the day of the week are restricted (neither starts with * nor is ?), the routine runs when either matches. When either day field starts with * or is ?, both must match |
`{expression}` is not a cron expression: {detail} |
|
/on/schedule/timezone |
string | no | UTC |
An IANA name such as Europe/Berlin. Only with cron |
`{timezone}` is not an IANA timezone, use a name like `Europe/Berlin`, `timezone` only applies to `cron`; `at` carries its own offset |
/on/schedule/at |
string | one of cron, at |
An RFC 3339 timestamp such as 2026-12-01T09:00:00Z. Runs once, then the routine is disabled. A time in the past is not rejected |
`{at}` is not an RFC 3339 timestamp: {detail} |
|
/on/webhook |
object | one of three | {} is allowed and means a bearer token in Authorization. Other fields: auth, idempotencyHeader, example |
Unknown field `{key}`. Allowed here: `auth`, `idempotencyHeader`, `example`. (plus “Did you mean” for a near miss) at /on/webhook/{key} |
|
/on/webhook/auth |
object | no | {} (token in Authorization) |
type is token (default) or hmac |
`{value}` is not allowed here. Use `token` or `hmac`. at /on/webhook/auth/type |
/on/webhook/auth/header |
string | no | Authorization |
Only with type: token. 1 to 64 bytes: letters, digits and ``!#$%&’*+-.^_` |
~``. Not a header the platform sets: Host, Cookie, Connection, Transfer-Encoding, X-Real-IP, Forwarded, Traceparent, Content-*, X-Forwarded-* and X-Auth-Request-*. Names are matched without regard to case |
/on/webhook/auth/preset |
string | with type: hmac |
forkline, github or sentry. Only with type: hmac |
`preset` is required with `type: hmac`. Presets: ..., Unknown preset `{name}`. Did you mean `{near}`? Presets: ..., `preset` is only for `type: hmac`. |
|
/on/webhook/idempotencyHeader |
string | no | Idempotency-Key |
Same header-name rules. Not the header that carries the credential. Names are matched without regard to case | `{name}` already carries the credential; use another header for the idempotency key. |
/on/webhook/example |
any JSON | no | A sample body for the Run drawer. At most 16 KiB, measured as compact JSON. Never read when a call arrives | The example is {size} bytes, over the 16 KiB limit. |
events
on:
events: [pr.opened, pr.updated]
The events are pr.opened, pr.updated, pr.merged, pr.closed, issue.opened, comment.created, ci.failed and push. The list of what each one means on GitHub, GitLab and Forgejo, and the fields each one carries, is on the Events page.
schedule
on:
schedule:
cron: "0 6 * * 1" # five fields, every Monday at 06:00
timezone: Europe/Berlin # IANA name, default UTC
on:
schedule:
at: "2026-12-01T09:00:00Z" # once, RFC 3339; the routine is then disabled
The weekday follows standard cron: 0 or 7 is Sunday, 1 is Monday and 6 is Saturday. Names work too (MON-FRI, SUN,SAT, case does not matter), and so do ranges, lists and steps (1-5, */2). When both the day of the month and the day of the week are restricted (neither starts with * nor is ?), the routine runs when either matches: 0 8 1 * 1 runs on the 1st of each month and on every Monday, and only once when the 1st is a Monday. When either day field starts with * (*, */2) or is ?, both must match: 0 9 */10 * 1 runs only on Mondays that are the 1st, 11th, 21st or 31st, and 0 9 1 * */2 only on a 1st that is a Sunday, Tuesday, Thursday or Saturday.
timezone only applies to cron. A cron routine fires when a scheduled time has passed since it last fired. If Forkline was unavailable, the routine fires once as soon as it is back, for the most recent time it missed; earlier missed times are not replayed. A routine never fires twice for the same scheduled time, and it is not owed times from before it was last edited or published. If a scheduled time cannot be started because of a temporary error, it is retried at the next check, about a minute later. If the routine can no longer be read as a valid routine, the scheduled time is recorded as an error run at stage fan_out and is not retried. A scheduled time is only retried while it has not been started: if a server restart lands exactly between claiming the scheduled time and starting its run, that time is skipped rather than retried, and the routine fires at its next scheduled time. A schedule runs once for each selected repository, see Repository selection.
webhook
on:
webhook:
auth: { type: hmac, preset: sentry }
A custom webhook routine runs when your system calls its URL. webhook: {} is valid and means a bearer token in Authorization. See Custom webhooks for the authentication options.
repositories
Which of your connected repositories the routine covers. The result is everything in include that is not in exclude. See Repository selection.
| Pointer | Type | Required | Default | Constraint | Message when violated |
|---|---|---|---|---|---|
/repositories |
object | yes | `repositories` is required. |
||
/repositories/include |
all or selector[] |
yes | The word all, or 1 to 50 selectors |
`{value}` is not valid here, use "all" or a list of selectors, use "all" or list at least one selector, at most 50 selectors, this has {n} |
|
/repositories/exclude |
selector[] | no | none | 1 to 50 selectors | at most 50 selectors, this has {n} |
/repositories/{include or exclude}/{i} |
selector | Exactly one of id and name |
set exactly one of `id` and `name` |
||
.../{i}/id |
string | one of id, name |
The UUID of a repository of yours | UUID parsing failed: {detail} |
|
.../{i}/name |
string | one of id, name |
owner/name, at most 256 bytes. * and ? match inside one segment, a whole segment ** matches several. Case-insensitive |
use the form `owner/name`, with `*` and `**` as wildcards, the pattern is longer than 256 bytes |
repositories:
include: all # or a list
repositories:
include:
- name: "acme/api"
- name: "acme/web-*"
- id: 3f2b8c1e-1c2d-4e5f-8a9b-0c1d2e3f4a5b
exclude:
- name: "acme/web-legacy"
For schedule and webhook routines the selection must match between 1 and 200 of your repositories when you save. See Errors that need more than the file.
filters
Optional. All filters must pass for a run to start. Filters only apply to routines started by events; a schedule or webhook routine can use only when. There when sees repository, and on a webhook routine also payload. Each filter has its own page section in Filters.
| Pointer | Type | Default | Applies to |
|---|---|---|---|
/filters/branches |
{include?, exclude?} of globs |
none | PR events (base branch), push (pushed branch), ci.failed (branch) |
/filters/paths |
{include?, exclude?} of globs |
none | push |
/filters/labels |
{include?, exclude?} of strings |
none | PR events, issue.opened, comment.created |
/filters/authors |
{include?, exclude?} of strings |
none | PR events, issue.opened, comment.created, push |
/filters/draft |
exclude, include or only |
exclude |
PR events |
/filters/fork |
exclude, include or only |
exclude |
PR events |
/filters/bots |
exclude, include or only |
exclude |
every event except ci.failed |
/filters/self |
exclude, include or only |
exclude |
every event except ci.failed |
/filters/command |
{names?, mention?} |
none | comment.created |
/filters/when |
CEL string | none | every event; on a schedule routine only repository is visible, on a webhook routine repository and payload |
Rules shared by all filters:
| Rule | Message |
|---|---|
Every filter must apply to every listed event (pointer /filters/{name}) |
`{name}` does not apply to {events}; every filter must apply to every listed event, so split the routine |
A schedule or webhook routine cannot use event filters other than when |
`{name}` only applies to routines started by events |
include and exclude lists: at least one of the two, not empty, at most 50 entries, each entry 1 to 256 bytes |
set `include`, `exclude` or both, must not be empty, remove it instead, at most 50 entries, this has {n}, must not be empty, is longer than 256 bytes |
command: names (1 to 20, each starts with a letter, has letters, digits, - and _, is at most 64 characters, and is written without the /) and mention: true, at least one of them |
set `names` or `mention: true`, list between 1 and 20 command names, a command name starts with a letter and has only letters, digits, `-` and `_`, written without the leading `/`, `{name}` is listed twice |
For the command matching semantics (line-start requirement, mention interaction), see Command.
| draft, fork, bots, self must be one of the three words | `{value}` is not allowed here. Use one of: `exclude`, `include`, `only`. |
when
when is one boolean CEL expression over the typed fields of the event, for example pr.labels.size() > 0 && !pr.draft. It is checked when you save: a field that none of the listed events carries, an unsupported function, a result that is not a boolean or an expression that is too expensive is an error at /filters/when.
| Limit | Value | Message |
|---|---|---|
| Empty expression | not allowed | must not be empty |
| Size | 4096 bytes | filter is {n} bytes, the limit is 4096 |
| Nesting | 16 levels | filter nests expressions more than 16 levels deep |
| Expression count | 512 | filter is too large, it may use at most 512 expressions |
| Estimated worst-case cost | 50,000 units | the filter's estimated worst-case cost {n} exceeds the budget of 50000 by a factor of {f}; ... |
The full list of fields and functions is in Filters.
agent
What the agent does when the routine fires.
| Pointer | Type | Required | Default | Constraint | Message when violated |
|---|---|---|---|---|---|
/agent |
object | yes | `agent` is required. |
||
/agent/prompt |
template string | yes | Not empty, at most 32 KiB (32768 bytes). Every variable must exist for every listed event | `prompt` is required., must not be empty, is {n} bytes, the limit is 32768, the template does not parse: {detail}, `{path}` isn't available for `{event}`., `{path}` isn't available for {events}; every chosen event must provide it. |
|
/agent/model |
string | no | the runner default | 1 to 200 characters. Only the length is checked | must be between 1 and 200 characters |
/agent/runnerSize |
basic, advanced or pro |
no | the runner default (basic) |
One of the three | `{value}` is not allowed here. Use one of: `basic`, `advanced`, `pro`. |
/agent/ref |
template string | no | the natural ref of the event, see below | 1 to 1024 characters. Checked like the prompt. A ref that renders empty means no ref | must be between 1 and 1024 characters |
/agent/timeoutMinutes |
integer | no | 30 | 1 to 240 | must be between 1 and 240 |
agent:
prompt: |
CI "{{ ci.name }}" failed on {{ pr.headBranch }}. Fix it and push to the PR branch.
model: anthropic/claude-sonnet-4-5
runnerSize: advanced
ref: "{{ pr.headBranch }}"
timeoutMinutes: 45
When ref is not set, the agent works on:
| Event | Default ref |
|---|---|
pr.* events |
The pull request head branch. For a pull request from a fork, the base branch, because the agent cannot push to the fork |
push |
The new commit of the push |
ci.failed |
The head branch of the pull request the run belongs to, otherwise the branch of the CI run |
| everything else | The default branch of the repository |
concurrency
concurrency is enforced for every run: events, schedule ticks, Run now and custom webhooks. It is written like this:
name: concurrency-shape
description: Every concurrency key with its default
on:
events: [pr.opened, pr.updated]
repositories:
include: all
agent:
prompt: Review pull request #{{ pr.number }}.
concurrency:
group: pr # pr, issue, repository or routine
onConflict: replace # replace or skip
maxActive: 10 # 1 to 50
| Pointer | Type | Required | Default | Constraint | Message when violated |
|---|---|---|---|---|---|
/concurrency/group |
pr, issue, repository or routine |
no | derived from the events, see below | pr needs events that all belong to a pull request (pr.*, comment.created, ci.failed). issue needs issue.opened and comment.created only. repository and routine always work |
group `{group}` needs a routine that triggers on {allowed events}, use `repository` or `routine` instead |
/concurrency/onConflict |
replace or skip |
no | replace when the group is pr or issue, skip when it is repository or routine |
One of the two | an unknown value is an enum error at this pointer |
/concurrency/maxActive |
integer | no | 10 | 1 to 50 | a value outside the range is a range error at this pointer |
The default group is pr when every event is pr.*, comment.created or ci.failed and at least one is pr.* or ci.failed. Otherwise it is issue when every event is issue.opened or comment.created, and otherwise repository. Schedule and webhook routines default to repository.
maxActive does not count tasks from the same occurrence (one event, schedule tick, Run now or custom webhook call). On replace, a run that cannot start because the owner’s pending-run cap is reached cancels nothing. Details: Concurrency and limits.
limits
limits:
maxRunsPerPr: 3
| Pointer | Type | Required | Default | Constraint | Message when violated |
|---|---|---|---|---|---|
/limits/maxRunsPerPr |
integer | no | 3 | 1 to 50 | must be between 1 and 50 |
/limits/resetOn |
reserved | not supported yet | `limits.resetOn` is not supported yet |
maxRunsPerPr is the most runs one routine starts for the same pull request, in total: replaced runs count and it never resets. It stops loops between a routine and a bot or CI. The default 3 also stops a routine on pr.updated after the pull request’s third run (outcome run_limit); raise it, up to 50, to run on every commit. Events without a pull request are not counted.
Limits at a glance
| What | Limit |
|---|---|
| Routine name | 1 to 100 characters |
Events in on.events |
8 |
Selectors in repositories.include and in .exclude |
50 each |
| Repositories of one schedule or webhook run | 1 to 200 |
| Entries in one filter list | 50, each at most 256 bytes |
Command names in filters.command.names |
20 |
filters.when |
4096 bytes, 16 levels, 512 expressions, worst-case cost 50,000, 256 bytes per matches() pattern |
Fields filters.when reads from payload |
weight 4,096, 16 levels, 1,000 items, 64 KiB of text |
| Prompt | 32 KiB (32768 bytes); the rendered prompt at most 512 KiB |
agent.model |
1 to 200 characters |
agent.ref |
1 to 1024 characters |
agent.timeoutMinutes |
1 to 240, default 30 |
limits.maxRunsPerPr |
1 to 50, default 3 |
concurrency.maxActive |
1 to 50, default 10 |
| Routines per owner | 25 published, 100 in total |
| Webhook body | 5 MiB |
| Run now and test payload | 5 MiB (request 6 MiB) |
| Run now | 10 real runs a minute per owner |
Sizes, rates and how calls behave at them: Webhook and Run now limits.
Not supported yet
These names are reserved. Forkline refuses them when you save, with the message shown, instead of ignoring them.
| Name | Pointer | Message |
|---|---|---|
limits.resetOn |
/limits/resetOn |
`limits.resetOn` is not supported yet |
Event pr.conflicted |
/on/events/{i} |
`pr.conflicted` is not supported yet |
Event ci.succeeded |
/on/events/{i} |
`ci.succeeded` is not supported yet |
Event pr.labeled |
/on/events/{i} |
`pr.labeled` is not supported yet |
Event issue.labeled |
/on/events/{i} |
`issue.labeled` is not supported yet |
Event issue.assigned |
/on/events/{i} |
`issue.assigned` is not supported yet |
Event pr.review_requested |
/on/events/{i} |
`pr.review_requested` is not supported yet |
Variable comment.prompt |
/agent/prompt or /agent/ref |
`comment.prompt` is not supported yet |
Variable comment.path |
/agent/prompt or /agent/ref |
`comment.path` is not supported yet |
Variable comment.requestedModel |
/agent/prompt or /agent/ref |
`comment.requestedModel` is not supported yet |
Variable thread.comments |
/agent/prompt or /agent/ref |
`thread.comments` is not supported yet |
Custom webhooks
A routine with on.webhook runs when another system calls its URL:
POST https://<your Forkline host>/webhooks/custom/<routine id>
The routine must be published. The call proves who it is with a secret that belongs to this routine. There is no token in the URL: access logs record URLs, so a secret there would end up in logs. The secret goes in a header.
To start a routine yourself instead, use Run now (web, CLI or API). The same limits apply to both.
on.webhook.auth
auth |
The call carries | Secret |
|---|---|---|
{} or omitted |
Authorization: Bearer <secret> |
Generated by Forkline |
{type: token, header: X-Alert-Token} |
The secret itself, as the raw value of that header | Generated by Forkline |
{type: hmac, preset: forkline} |
X-Signature-256: sha256=<hex> |
Generated by Forkline |
{type: hmac, preset: github} |
X-Hub-Signature-256: sha256=<hex> |
Pasted by you |
{type: hmac, preset: sentry} |
Sentry-Hook-Signature: <hex> |
Pasted by you |
- Token. The default. With
Authorizationthe schemeBeareris required (any letter case). With a customheaderthe value is the secret alone. - HMAC. The sender signs the raw request body with HMAC-SHA256 and sends the hex digest.
<hex>can be lower or upper case. The preset fixes the header and the prefix, soheaderis not allowed withtype: hmac. - Which preset.
forklineis for your own code.githubandsentrymatch what those senders already send: set the Forkline secret to the signing secret the sender holds (GitHub: the webhook secret; Sentry: the integration’s client secret). For GitHub, set the webhook’s Content type toapplication/json: withapplication/x-www-form-urlencoded, the body arrives as form text inpayload.raw_bodyand the event’s fields are not inpayload. Sentry sends no idempotency key: each delivery starts a call. - A wrong or missing secret answers
401:Missing credentialsorInvalid credentialsfor a token,Missing signatureorInvalid signaturefor HMAC. Nothing is started. A routine that has no secret yet answers the sameMissingmessage.
name: alert-triage
on:
webhook:
auth: { type: hmac, preset: forkline }
idempotencyHeader: X-Delivery-Id
example:
title: High error rate
level: error
repositories:
include: [{ name: "acme/api" }]
agent:
prompt: |
Investigate this alert in {{ repository.fullName }}: {{ payload.title }}
BODY='{"title":"High error rate"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.* //')
curl -X POST https://<host>/webhooks/custom/<routine id> \
-H "X-Signature-256: sha256=$SIG" \
-H "X-Delivery-Id: alert-4711" \
-d "$BODY"
With the default auth: {}, send the secret instead:
curl -X POST https://<host>/webhooks/custom/<routine id> \
-H "Authorization: Bearer $WEBHOOK_SECRET" \
-H "Idempotency-Key: alert-4711" \
-d '{"title":"High error rate"}'
The secret
- Generated (token and
forkline). Forkline creates it when you create the routine and returns it once in that response (webhook_secret). It never appears in the spec. Reading the routine later only showswebhook_secret_configured,webhook_secret_origin(generated) andwebhook_secret_updated_at. - Rotate it with
forkline routine webhook <routine> --rotate-secretorPOST /v1/routines/{id}/webhook/rotate-secret. The new secret is shown once and the old one stops working immediately. There is no grace period. - Pasted (
github,sentry). The sender owns the secret, so you give it to Forkline withforkline routine webhook <routine> --set-secret <file|->orPUT /v1/routines/{id}/webhook/secretwith{"secret": "..."}(16 to 512 bytes, surrounding whitespace trimmed). Forkline never shows it back, and a routine without it cannot be published or enabled until it is set. Rotate is refused for a pasted secret: rotate it at the sender and paste the new value. - Changing
auth. Switching between a token andforklinekeeps the secret. Moving togithuborsentry, or between them, clears it: paste the new one. Moving from a pasted secret to a token orforklinegenerates one, shown once. A routine that is already published stays published, and answers401 Missing signatureuntil you paste the secret. {}on an older routine was rewritten to{type: hmac, preset: forkline}, so existing senders keep working.
Other settings and limits
idempotencyHeader. The header that carries the retry key (defaultIdempotency-Key). Send it so a repeated delivery starts no second task. Without it, every call starts a run. The key is trimmed and at most 200 bytes; a longer key is ignored, so every call runs. Forkline keeps the key with the run, so a repeat is skipped however late it comes. Sentry sends no key. Run now does not use this setting: it always readsIdempotency-Key.example. A sample body, at most 16 KiB serialized as compact JSON. The Run drawer fills its payload box with it. A real call never reads it.- The body. Any JSON (anything else is passed as
payload.raw_body). It is available aspayloadin templates, and inwhenwithin the limits listed there. - Size. The body is at most 5 MiB. A larger body is refused with
413PAYLOAD_TOO_LARGE(The request body is larger than 5 MiB) and starts nothing. - Errors. An unexpected failure answers
500with the usual JSON envelope,{"error": "Internal server error", "code": "INTERNAL_ERROR"}; nothing is said about the cause. Retry later. - Response. An accepted call answers
200withtask_idsandsession_urls. Both are empty when nothing starts, andreasonsays why.event_idfinds the call in Recent events. An unknown routine, a draft, a disabled routine and a routine without a webhook trigger answer404. - Concurrency. The routine’s
concurrencyapplies: with the defaults, a second call while a run is active starts nothing and is recorded asskipped_active. - Rate limit. A routine accepts 600 calls a minute (bursts of 200), and one sender IP address 1,200 a minute (bursts of 400), counted per Forkline replica. A sender above either gets
429with{"success": false, "message": "Webhook rate limit exceeded; retry later"}and aRetry-Afterheader. Nothing is started: retry after the delay. - Overload. While Forkline is overloaded the endpoint answers
429with{"error":"Forkline is busy; retry shortly","code":"TOO_MANY_REQUESTS","retry_after_secs":5}and aRetry-After: 5header. Nothing was started: retry after the delay. - All limits. Webhook and Run now limits lists every size and rate in one place.
forkline routine webhook <routine> prints the URL, the authentication method, and whether a secret is configured.