[{"data":1,"prerenderedAt":35},["ShallowReactive",2],{"$f2rv561rokv2xe":3},{"href":4,"title":5,"description":6,"kind":7,"mark":7,"planned":8,"contributors":9,"provenance":7,"html":10,"headings":11},"\u002Fdocs\u002Fwork\u002Fschedules","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.",null,false,[],"\u003Ch2 id=\"what-starts-a-run\">What starts a run\u003C\u002Fh2>\n\u003Cp>A task does nothing until something starts a run of it. There are four ways:\u003C\u002Fp>\n\u003Cdiv class=\"prose__table\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Start\u003C\u002Fth>\n\u003Cth>How\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>A person\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Start a run\u003C\u002Fstrong> on the task's page, a member starting it for you in \u003Ca href=\"\u002Fdocs\u002Fwork\u002Fchat\">chat\u003C\u002Fa>, or \u003Ccode>POST \u002Fv1\u002Ftasks\u002F{id}\u002Fruns\u003C\u002Fcode> with an \u003Ca href=\"\u002Fdocs\u002Fapi\u002Fauthentication\">API token\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>A schedule\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>The task's own cadence.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>A webhook\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Any system that can send an HTTP request. See \u003Ca href=\"\u002Fdocs\u002Fapi\u002Fwebhooks\">Webhooks\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>The task before it\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>A \u003Ca href=\"\u002Fdocs\u002Fwork\u002Fhandoffs\">handoff\u003C\u002Fa>: another task names this one as a successor.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Cp>However it starts, a run is the same: the task's current version, its gates, and its spend caps. \u003Cstrong>Triggers\u003C\u002Fstrong> on a\ntask's page shows all of them together: its schedule, its webhook, and the tasks that hand to it.\u003C\u002Fp>\n\u003Ch2 id=\"schedules\">Schedules\u003C\u002Fh2>\n\u003Cp>A task carries its own cadence. \u003Cstrong>Triggers → Add a schedule\u003C\u002Fstrong> sets it:\u003C\u002Fp>\n\u003Cdiv class=\"prose__table\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Field\u003C\u002Fth>\n\u003Cth>What it is\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>How often\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>A preset, or \u003Cstrong>Custom cron…\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Cron\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Five fields: minute, hour, day of month, month, day of week. \u003Ccode>0 9 * * 1-5\u003C\u002Fcode> is 09:00 on weekdays.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Timezone\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Any IANA timezone, such as \u003Ccode>Europe\u002FLondon\u003C\u002Fcode>. UTC unless you choose another.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Run input JSON\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>The input every firing starts with. It starts from the task's input defaults.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Run on this schedule\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Untick it to pause the schedule without losing it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Cp>The presets are every 5, 15 or 30 minutes; hourly, on the hour; every 4 hours; every day at 09:00; weekdays at\n09:00; Mondays at 09:00; and the 1st of each month at 09:00.\u003C\u002Fp>\n\u003Cp>The schedule belongs to the task, not to a version: changing it publishes no new version, and a new version keeps the\nschedule. A cron that is not five fields, a timezone the OS does not know, or a cron that would not fire within a year\nis refused when you save it. So is a schedule whose run input leaves a required input empty when the task gives no\ndefault for it: the refusal names the input (\u003Ccode>input_required\u003C\u002Fcode>).\u003C\u002Fp>\n\u003Cp>Each firing starts with the schedule's run input, and any input it leaves out takes the task's default, as \u003Cstrong>Start a\nrun\u003C\u002Fstrong> does. If a new version makes an input required that the schedule does not supply, the schedule stops starting\nruns: each firing is skipped rather than started without the input. \u003Cstrong>Triggers\u003C\u002Fstrong> says which input is missing, and\n\u003Cstrong>Blockers\u003C\u002Fstrong> lists the task under task problems, until the schedule's input supplies it or the task gives it a\ndefault.\u003C\u002Fp>\n\u003Cp>Times follow the timezone's clock, including daylight saving: a time the clocks skip does not fire that day, and a\ntime they repeat fires twice.\u003C\u002Fp>\n\u003Ch3 id=\"next-and-last-firing\">Next and last firing\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>Triggers\u003C\u002Fstrong> shows the cadence in words with its cron beside it, when it \u003Cstrong>next\u003C\u002Fstrong> runs, when it \u003Cstrong>last\u003C\u002Fstrong> fired, and the\ninput it posts. A paused schedule says \u003Cstrong>Paused\u003C\u002Fstrong>. With no schedule, the task runs only when a person, a webhook or\nthe task before it starts it.\u003C\u002Fp>\n\u003Cp>The OS checks schedules every minute, so a firing can start up to about a minute after its time. Each firing happens\nonce: if the OS was down through several firings, it fires once when it is back, not once for every firing it missed.\u003C\u002Fp>\n\u003Cp>A deprecated task's schedule does not fire.\u003C\u002Fp>\n\u003Ch2 id=\"when-the-task-is-busy\">When the task is busy\u003C\u002Fh2>\n\u003Cp>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\npaused by a spend cap; a run waiting at a gate for your decision does not count. What happens to a start that finds\nthe task busy, or every member holding its role busy, depends on where it came from:\u003C\u002Fp>\n\u003Cdiv class=\"prose__table\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Start\u003C\u002Fth>\n\u003Cth>When busy\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>A schedule\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Skipped.\u003C\u002Fstrong> The next firing tries again.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Start a run\u003C\u002Fstrong>, or the API\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Refused\u003C\u002Fstrong>, with a \u003Ccode>409\u003C\u002Fcode> saying whether the task (\u003Ccode>overlap\u003C\u002Fcode>) or every holder of its role (\u003Ccode>member_busy\u003C\u002Fcode>) is busy.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A webhook\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Waits\u003C\u002Fstrong> as \u003Ccode>pending\u003C\u002Fcode>, 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.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A member in chat, a handoff, a retry\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Waits\u003C\u002Fstrong> as \u003Ccode>pending\u003C\u002Fcode>, the same way.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Cp>A skipped or refused start is recorded as a \u003Ccode>skipped_overlap\u003C\u002Fcode> run, so you can see work arriving while the task was\nbusy: one row for each stretch of busy starts, not one per firing. These rows are removed after 24 hours. A schedule\nnever queues work behind a busy task, so nothing piles up.\u003C\u002Fp>\n\u003Ch2 id=\"webhooks\">Webhooks\u003C\u002Fh2>\n\u003Cp>Every task has a webhook. \u003Cstrong>Triggers\u003C\u002Fstrong> shows its URL and a \u003Ccode>curl\u003C\u002Fcode> to copy, filled with the task's input keys:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\">curl -X POST https:\u002F\u002Fapi.zerohuman.com\u002Fv1\u002Fwebhooks\u002F{enterprise}\u002Ftasks\u002F{task} \\\n  -H &quot;x-os-webhook-secret: $WEBHOOK_SECRET&quot; \\\n  -H &quot;content-type: application\u002Fjson&quot; \\\n  -d '{&quot;issue&quot;: 123}'\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>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\ndefault. A body that leaves a required input empty, with no default to stand in, is refused with a \u003Ccode>400\u003C\u002Fcode> naming it\n(\u003Ccode>input_required\u003C\u002Fcode>), and no run starts. The secret, its rotation, and the history of every call are on\n\u003Ca href=\"\u002Fdocs\u002Fapi\u002Fwebhooks\">Webhooks\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"after-another-task\">After another task\u003C\u002Fh2>\n\u003Cp>A task also starts when a task before it finishes and names it as a successor. That rule lives on the task before it,\nnot on this one: \u003Cstrong>Triggers\u003C\u002Fstrong> lists those tasks so you can see every way in, and you change the rule on theirs. See\n\u003Ca href=\"\u002Fdocs\u002Fwork\u002Fhandoffs\">Handoffs\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"over-the-api\">Over the API\u003C\u002Fh2>\n\u003Cp>With an \u003Ca href=\"\u002Fdocs\u002Fapi\u002Fauthentication\">API token\u003C\u002Fa> scoped \u003Ccode>tasks:write\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cdiv class=\"prose__table\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Call\u003C\u002Fth>\n\u003Cth>What it does\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>PUT \u002Fv1\u002Ftasks\u002F{id}\u002Fschedule\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Set the schedule: \u003Ccode>{ &quot;cron&quot;: &quot;0 9 * * 1-5&quot;, &quot;timezone&quot;: &quot;Europe\u002FLondon&quot;, &quot;enabled&quot;: true, &quot;input&quot;: {} }\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>DELETE \u002Fv1\u002Ftasks\u002F{id}\u002Fschedule\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Remove it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>POST \u002Fv1\u002Ftasks\u002F{id}\u002Fruns\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Start a run now, with its input as the body.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n",[12,16,19,23,26,29,32],{"id":13,"text":14,"level":15,"planned":8},"what-starts-a-run","What starts a run",2,{"id":17,"text":18,"level":15,"planned":8},"schedules","Schedules",{"id":20,"text":21,"level":22,"planned":8},"next-and-last-firing","Next and last firing",3,{"id":24,"text":25,"level":15,"planned":8},"when-the-task-is-busy","When the task is busy",{"id":27,"text":28,"level":15,"planned":8},"webhooks","Webhooks",{"id":30,"text":31,"level":15,"planned":8},"after-another-task","After another task",{"id":33,"text":34,"level":15,"planned":8},"over-the-api","Over the API",1791124519887]