Schedules and webhooks

What starts a run: a schedule, a webhook, a person, or the task before it, and what happens when the task is already busy.

What starts a run

A task does nothing until something starts a run of it. There are four ways:

Start How
A person Start a run on the task's page, a member starting it for you in chat, or POST /v1/tasks/{id}/runs with an API token.
A schedule The task's own cadence.
A webhook Any system that can send an HTTP request. See Webhooks.
The task before it A handoff: another task names this one as a successor.

However it starts, a run is the same: the task's current version, its gates, and its spend caps. Triggers on a task's page shows all of them together: its schedule, its webhook, and the tasks that hand to it.

Schedules

A task carries its own cadence. Triggers → Add a schedule sets it:

Field What it is
How often A preset, or Custom cron…
Cron Five fields: minute, hour, day of month, month, day of week. 0 9 * * 1-5 is 09:00 on weekdays.
Timezone Any IANA timezone, such as Europe/London. UTC unless you choose another.
Run input JSON The input every firing starts with. It starts from the task's input defaults.
Run on this schedule Untick it to pause the schedule without losing it.

The presets are every 5, 15 or 30 minutes; hourly, on the hour; every 4 hours; every day at 09:00; weekdays at 09:00; Mondays at 09:00; and the 1st of each month at 09:00.

The schedule belongs to the task, not to a version: changing it publishes no new version, and a new version keeps the schedule. A cron that is not five fields, a timezone the OS does not know, or a cron that would not fire within a year is refused when you save it. So is a schedule whose run input leaves a required input empty when the task gives no default for it: the refusal names the input (input_required).

Each firing starts with the schedule's run input, and any input it leaves out takes the task's default, as Start a run does. If a new version makes an input required that the schedule does not supply, the schedule stops starting runs: each firing is skipped rather than started without the input. Triggers says which input is missing, and Blockers lists the task under task problems, until the schedule's input supplies it or the task gives it a default.

Times follow the timezone's clock, including daylight saving: a time the clocks skip does not fire that day, and a time they repeat fires twice.

Next and last firing

Triggers shows the cadence in words with its cron beside it, when it next runs, when it last fired, and the input it posts. A paused schedule says Paused. With no schedule, the task runs only when a person, a webhook or the task before it starts it.

The OS checks schedules every minute, so a firing can start up to about a minute after its time. Each firing happens once: if the OS was down through several firings, it fires once when it is back, not once for every firing it missed.

A deprecated task's schedule does not fire.

When the task is busy

A task has one run under way at a time, and so does a member. A run is under way while it is queued, running or paused by a spend cap; a run waiting at a gate for your decision does not count. What happens to a start that finds the task busy, or every member holding its role busy, depends on where it came from:

Start When busy
A schedule Skipped. The next firing tries again.
Start a run, or the API Refused, with a 409 saying whether the task (overlap) or every holder of its role (member_busy) is busy.
A webhook Waits as pending, and starts when the task and a holder of its role are free. The same body sent again while it waits returns the run already waiting.
A member in chat, a handoff, a retry Waits as pending, the same way.

A skipped or refused start is recorded as a skipped_overlap run, so you can see work arriving while the task was busy: one row for each stretch of busy starts, not one per firing. These rows are removed after 24 hours. A schedule never queues work behind a busy task, so nothing piles up.

Webhooks

Every task has a webhook. Triggers shows its URL and a curl to copy, filled with the task's input keys:

curl -X POST https://api.zerohuman.com/v1/webhooks/{enterprise}/tasks/{task} \
  -H "x-os-webhook-secret: $WEBHOOK_SECRET" \
  -H "content-type: application/json" \
  -d '{"issue": 123}'

The JSON body becomes the run's input: it must be a JSON object, and any input it leaves out or blank takes the task's default. A body that leaves a required input empty, with no default to stand in, is refused with a 400 naming it (input_required), and no run starts. The secret, its rotation, and the history of every call are on Webhooks.

After another task

A task also starts when a task before it finishes and names it as a successor. That rule lives on the task before it, not on this one: Triggers lists those tasks so you can see every way in, and you change the rule on theirs. See Handoffs.

Over the API

With an API token scoped tasks:write:

Call What it does
PUT /v1/tasks/{id}/schedule Set the schedule: { "cron": "0 9 * * 1-5", "timezone": "Europe/London", "enabled": true, "input": {} }.
DELETE /v1/tasks/{id}/schedule Remove it.
POST /v1/tasks/{id}/runs Start a run now, with its input as the body.