v2.0.0next
Reference

Inbound triggers

The schedule fields in intake.yaml and the POST /api/triggers contract for starting a chain from a cron or a webhook.

A trigger files a work item from an event instead of kraft item create. There are two kinds, a schedule in intake.yaml and an HTTP call. Both always file the item paused, exactly like manual intake: a trigger cannot start work, any more than kraft item create without --autostart can. To set one up, see Schedule or webhook work.

Schedules

A schedule is an entry in the schedules: list of intake.yaml:

config/intake.yaml
schedules:
  - cron: "0 9 * * 1,2,3,4,5"   # 9:00 UTC on weekdays
    repo: /path/to/repo
    chain: default
    title: "Weekday dependency check"
    description: "Filed by the 09:00 UTC weekday schedule"
FieldTypeDefaultMeaning
cronstringrequiredA five-field cron expression, read in UTC. See Cron syntax.
repostringrequiredPath of a repo you connected with kraft repo connect.
chainstringrequiredA chain id from config/chains/.
titlestringrequiredThe work item's title. It is one line: see Schedule titles.
descriptionstring""The work item's description.

Cron syntax

The five fields, in order:

FieldAllowed values
minute0-59
hour0-23
day of month1-31
month1-12
day of week0-7 (0 and 7 are both Sunday)

Each field takes any of these forms, or a comma-separated list of them, such as 1,2,3,4,5:

FormExampleMatches
**Every value.
An integer9That value.
A range1-5Every value from 1 to 5.
A step over everything*/15Every 15th value of the field, from its first (minutes 0, 15, 30, 45).
A step over a range10-40/10Every 10th value from 10 to 40 (10, 20, 30, 40).
A step from a start5/20Every 20th value from 5 to the field's top (minutes 5, 25, 45).

Names (MON, JAN) are not read. A day of month and a day of week must both match: 0 9 1 * 1 fires only on a 1st that is a Monday, not on every 1st and every Monday as some crons do.

A value outside its field, a backwards range, or a zero step is refused where intake.yaml is read, and the file does not load. When the file does not load says what you see.

Schedule titles

  • A title is one line. One with a line break is filed with its first line as the title and the rest at the top of the description, rather than skipped.
  • A block scalar (title: > or title: |) ends in a line break unless written >- or |-. Kraft drops that trailing break.
  • Control characters are dropped too.
  • A blank title takes the description's first line.

When a schedule fires

  • Kraft evaluates the schedule in UTC, not in the server's local time zone.
  • It checks once a minute. A minute missed while the server is down is not backfilled.
  • The poller runs for the life of the server, so a schedule you add later fires without a restart.
  • kraft admin reload rereads intake.yaml, and so does a publish from Settings › Auto-intake.

A triggers: entry left in policy.yaml still fires. The Auto-intake screen reads and writes intake.yaml only, so it does not show one: change it in the file. A cron there that Kraft cannot read at all (a word, the wrong number of fields) refuses policy.yaml. Upgrade from 1.4 says how schedules moved, and what happens to the other bad ones.

When Kraft skips a schedule

Kraft skips an entry, logs a warning naming the reason, and still fires the other entries due in the same tick, when:

  • the repo is not connected (the warning names kraft repo connect);
  • the chain id is unknown;
  • the title and the description are both blank;
  • the library or a chain is invalid;
  • its cron cannot be read (a guard: an intake.yaml holding one does not load, see When the file does not load);
  • intake refuses the item (a chain policy past the instance maximum, or an unreadable attachment).

When policy.yaml is invalid, Kraft skips the whole tick instead, so no entry fires that minute.

POST /api/triggers

The HTTP twin of a schedule, for anything that can fire a webhook and cannot wait for the next minute tick. A call like this, from the machine running Kraft:

curl -s -X POST http://127.0.0.1:8765/api/triggers \
  -H 'Content-Type: application/json' \
  -d '{"title": "Nightly dependency check", "repo": "/path/to/repo"}'

Once Kraft is bound off loopback, add -H "Authorization: Bearer $TOKEN", where $TOKEN is the trigger token and never the MCP token: see the warning below and Authentication. Schedule or webhook work has the worked example, including the call from another machine.

The call answers 201 with the new work item as JSON, filed paused. Cut to the fields that matter, the body reads:

{"id": "<work-item-id>", "title": "Nightly dependency check", "status": "paused", "...": "..."}

The full shape is in /openapi.json; see HTTP API.

The trigger token works on this route only: its holder can file paused work and nothing else. The MCP bearer token works on every route, so its holder can also start, approve and cancel work and change settings. Give CI and webhook senders the trigger token. To rotate either, see Rotate the token.

Request

The request body is JSON:

FieldTypeDefaultMeaning
titlestringrequiredThe work item's title.
repostringrequiredPath of a connected repo.
chainstringThe repo's default_chain, else defaultA chain id.
descriptionstring""The work item's description.

Authentication

Once Kraft is bound off loopback, send the trigger token in $KRAFT_HOME/run/trigger-token as Authorization: Bearer <token>. The MCP bearer token or a browser session cookie also works. While Kraft is bound to loopback, the machine running it needs no token. Who may call lists every caller.

Responses

StatusMeaning
201The item was filed. The body is the work item as JSON: id is the new item's id and status is paused.
401Kraft is bound off loopback, and the request has no valid bearer token or session.
403The caller is refused. Who may call lists the causes.
422title is missing, empty, over 500 characters, or not one line of plain text. See The title.
422repo is missing or not connected (the message names kraft repo connect), its path no longer exists, or repos.yaml, the repo's policy or its steering cannot be read or is invalid.
422chain names no chain or one that does not resolve, or chain and chain_template are both sent and name different chains (the detail names both).
422Another field has the wrong type.
502Filing failed after the checks passed. The message starts intake failed:.
503policy.yaml, the library or a chain is invalid, so Kraft refuses all new work. kraft admin doctor names the file.

The title

A 422 for title says why:

  • title cannot be empty: the title is empty or only whitespace.
  • the title is one line: it holds a line break, a trailing one included. Put the rest in description.
  • the title is plain text: it holds a tab, an escape or another control character.
Copyright © 2026