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. |