[{"data":1,"prerenderedAt":38},["ShallowReactive",2],{"$fygq0ofekajsv":3},{"href":4,"title":5,"description":6,"kind":7,"mark":7,"planned":8,"contributors":9,"provenance":7,"html":10,"headings":11},"\u002Fdocs\u002Fapi\u002Ftasks","Tasks over the API","Create a task from code, change it without overwriting anyone else's change, start it with input, and read how the run went.",null,false,[],"\u003Cp>What a task is, and what each part of it does, is on \u003Ca href=\"\u002Fdocs\u002Fwork\u002Ftasks\">Tasks\u003C\u002Fa>. This page is the same task as JSON.\nCreating, changing and starting a task needs a token scoped \u003Ccode>tasks:write\u003C\u002Fcode>; reading its runs needs \u003Ccode>runs:read\u003C\u002Fcode>. See\n\u003Ca href=\"\u002Fdocs\u002Fapi\u002Fauthentication\">Authentication\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"a-task-as-json\">A task as JSON\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;name&quot;: &quot;Triage new issues&quot;,\n  &quot;teamId&quot;: &quot;6f1d2c3e-8a4b-4c1e-9f0a-2b7d5e1c9a40&quot;,\n  &quot;assigneeRole&quot;: &quot;support-engineer&quot;,\n  &quot;goal&quot;: &quot;Every new issue is labelled and answered within a day.&quot;,\n  &quot;metric&quot;: { &quot;text&quot;: &quot;Share of new issues labelled within 24 hours&quot; },\n  &quot;guardrails&quot;: [&quot;Never close an issue.&quot;, &quot;Never promise a release date.&quot;],\n  &quot;skillMd&quot;: &quot;# Triage an issue\\n\\n1. Read the issue.\\n2. Label it bug, feature or question.\\n3. Reply once: thank the author and say what happens next.\\n&quot;,\n  &quot;tools&quot;: [&quot;github.issue_read&quot;, &quot;github.issue_write&quot;, &quot;github.add_issue_comment&quot;],\n  &quot;gates&quot;: [{ &quot;tool&quot;: &quot;github.add_issue_comment&quot;, &quot;owner&quot;: &quot;ceo&quot;, &quot;approval&quot;: &quot;specific&quot; }],\n  &quot;inputs&quot;: [\n    { &quot;key&quot;: &quot;repo&quot;, &quot;label&quot;: &quot;Repository&quot;, &quot;type&quot;: &quot;string&quot;, &quot;required&quot;: true },\n    { &quot;key&quot;: &quot;issue&quot;, &quot;label&quot;: &quot;Issue number&quot;, &quot;type&quot;: &quot;number&quot;, &quot;required&quot;: true }\n  ],\n  &quot;spend&quot;: { &quot;moneyUsdPerRun&quot;: 0.5, &quot;maxModelTier&quot;: &quot;mid&quot; }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Only \u003Ccode>name\u003C\u002Fcode> is required. Everything else has a default, but a task with no \u003Ccode>assigneeRole\u003C\u002Fcode> has nobody to run it, and\ncannot start a run.\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>\u003Ccode>name\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Required. 1 to 200 characters.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>slug\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Unique in your enterprise; webhooks and other tasks' successors find the task by it. Made from \u003Ccode>name\u003C\u002Fcode> when you leave it out. Either way it is cleaned: lower case, accents dropped, and every run of characters other than letters and digits turned into one \u003Ccode>-\u003C\u002Fcode>. At most 80 characters.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>teamId\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The team it belongs to.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>assigneeRole\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The slug of a role: each run goes to a member who holds that role on the task's team and is free, the one who has waited longest first. When the team has no role by that slug, an enterprise-wide role (one with no team) of that slug is used. A task cannot be given to one member. See \u003Ca href=\"\u002Fdocs\u002Fcompany\u002Froles\">Roles\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>kind\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>llm\u003C\u002Fcode>, the default, runs a model. \u003Ccode>deterministic\u003C\u002Fcode> runs none, and so may list no tools.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>status\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>active\u003C\u002Fcode>, the default, or \u003Ccode>deprecated\u003C\u002Fcode>. A deprecated task starts no new runs, from the API, its webhook, a schedule or the portal, and answers \u003Ccode>task_not_runnable\u003C\u002Fcode>. Runs already under way finish.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>overlapPolicy\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>skip\u003C\u002Fcode>, the default: a run waits while its member is busy with another. \u003Ccode>os\u003C\u002Fcode>: runs beside the member's other work, for a short check on a schedule that should never queue.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>serializeOn\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The input keys that split the task's work into lanes, such as \u003Ccode>[&quot;repo&quot;, &quot;issue&quot;]\u003C\u002Fcode>. Runs in different lanes run at once; two runs in the same lane never do. Left out or \u003Ccode>null\u003C\u002Fcode>, the task has one lane: one run at a time. At most 8 keys of letters, digits, \u003Ccode>_\u003C\u002Fcode>, \u003Ccode>.\u003C\u002Fcode> and \u003Ccode>-\u003C\u002Fcode>. See \u003Ca href=\"\u002Fdocs\u002Fwork\u002Ftasks#runs-at-once\">Runs at once\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>goal\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>One sentence: what the task is for.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>metric\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ &quot;text&quot;: &quot;…&quot; }\u003C\u002Fcode>: how you would know it works.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>guardrails\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Rules the model must not break, one string each.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>skillMd\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The skill, in Markdown: how to do the job. A heading with the slug until you write one.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>tools\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Every tool a run may call. See \u003Ca href=\"\u002Fdocs\u002Fcontrol\u002Fgates#tool-scopes\">Gates and tool scopes\u003C\u002Fa>, and \u003Ca href=\"\u002Fdocs\u002Ftools\u002Fbuilt-in\">Built-in tools\u003C\u002Fa> for the OS's own.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>gates\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The calls that wait for you: \u003Ccode>tool\u003C\u002Fcode>, \u003Ccode>owner\u003C\u002Fcode> and \u003Ccode>approval\u003C\u002Fcode>. See \u003Ca href=\"\u002Fdocs\u002Fcontrol\u002Fgates\">Gates and tool scopes\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>inputs\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The fields a run starts with: \u003Ccode>key\u003C\u002Fcode>, \u003Ccode>label\u003C\u002Fcode>, \u003Ccode>type\u003C\u002Fcode> (\u003Ccode>string\u003C\u002Fcode> or \u003Ccode>number\u003C\u002Fcode>), and optionally \u003Ccode>required\u003C\u002Fcode>, \u003Ccode>default\u003C\u002Fcode> and \u003Ccode>placeholder\u003C\u002Fcode>. The portal's \u003Cstrong>Start a run\u003C\u002Fstrong> form is built from them.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>spend\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>moneyUsdPerRun\u003C\u002Fcode> in US dollars, and a highest \u003Ccode>maxModelTier\u003C\u002Fcode> (\u003Ccode>economical\u003C\u002Fcode>, \u003Ccode>mid\u003C\u002Fcode> or \u003Ccode>frontier\u003C\u002Fcode>) or an exact \u003Ccode>model\u003C\u002Fcode>. See \u003Ca href=\"\u002Fdocs\u002Fcontrol\u002Fspend\">Spend and models\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>workspace\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>For a task that works in a repository: its branch, and which files it may write. See \u003Ca href=\"\u002Fdocs\u002Ftools\u002Fworkspaces\">Workspaces\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>successors\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>What runs after it: each a \u003Ccode>slug\u003C\u002Fcode>, with optionally \u003Ccode>on\u003C\u002Fcode>, \u003Ccode>when\u003C\u002Fcode>, \u003Ccode>delayMinutes\u003C\u002Fcode>, \u003Ccode>maxRunsPerIssue\u003C\u002Fcode> and \u003Ccode>resumeConversation\u003C\u002Fcode>. See \u003Ca href=\"\u002Fdocs\u002Fwork\u002Fhandoffs\">Handoffs\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Ch2 id=\"create-a-task\">Create a task\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-bash\">curl -X POST https:\u002F\u002Fapi.zerohuman.com\u002Fv1\u002Ftasks \\\n  -H &quot;Authorization: Bearer zhos_…&quot; \\\n  -H &quot;content-type: application\u002Fjson&quot; \\\n  -d @triage-new-issues.json\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>It answers \u003Ccode>201\u003C\u002Fcode> with the task, as \u003Ccode>GET \u002Fv1\u002Ftasks\u002F{id}\u003C\u002Fcode> reads 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>\u003Ccode>id\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The task's id. Every other call names the task by it.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>slug\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The slug it was given: \u003Ccode>triage-new-issues\u003C\u002Fcode> here.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>currentVersionId\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Its current version's id. Send it as \u003Ccode>baseVersionId\u003C\u002Fcode> when you change the task.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>currentVersion\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The current version's definition.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>skillMd\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The current skill.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>versions\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Every version, newest first, with how many runs used each.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>definitionIssues\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>What the \u003Ca href=\"#the-checks\">checks\u003C\u002Fa> found that did not stop it saving.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>webhook\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The URL that starts it from another system, and the header your webhook secret goes in. See \u003Ca href=\"\u002Fdocs\u002Fapi\u002Fwebhooks\">Webhooks\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>canEditDefinition\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>false\u003C\u002Fcode> for a clone, whose definition belongs to the original's owner, and for the OS's own tasks, which are the same in every enterprise and change only with a release of the OS.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Cdiv class=\"prose__table\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Status\u003C\u002Fth>\n\u003Cth>Code\u003C\u002Fth>\n\u003Cth>Why\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>invalid_name\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>name\u003C\u002Fcode> is empty, or longer than 200 characters.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>invalid_slug\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The slug is empty once cleaned, or longer than 80 characters.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>invalid_task_definition\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The definition fails one of \u003Ca href=\"#the-checks\">the checks\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>llm_model_not_in_catalogue\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>spend.model\u003C\u002Fcode> is not a model your connected provider offers. \u003Ccode>GET \u002Fv1\u002Fllm\u002Fmodels\u003C\u002Fcode> lists them.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>assignee_member_removed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The body names a member in \u003Ccode>assigneeMemberId\u003C\u002Fcode>. A task is given to a role: send \u003Ccode>assigneeRole\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>serialize_on_invalid\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>serializeOn\u003C\u002Fcode> is not \u003Ccode>null\u003C\u002Fcode> or a list of up to 8 input keys.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>404\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>team_not_found\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>teamId\u003C\u002Fcode> is not in your enterprise.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>409\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>task_slug_taken\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Your enterprise already has a task with that slug.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Ch3 id=\"a-task-that-merges\">A task that merges\u003C\u002Fh3>\n\u003Cp>A task that can merge a pull request must declare a gate on the merge, and hold \u003Ccode>os.set_gate_summary\u003C\u002Fcode> so its run\ncan tell you what you are approving. Send all three in the same call:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;name&quot;: &quot;Merge approved pull requests&quot;,\n  &quot;assigneeRole&quot;: &quot;release-engineer&quot;,\n  &quot;tools&quot;: [&quot;github.pull_request_read&quot;, &quot;github.merge_pull_request&quot;, &quot;os.set_gate_summary&quot;],\n  &quot;gates&quot;: [{ &quot;tool&quot;: &quot;github.merge_pull_request&quot;, &quot;owner&quot;: &quot;ceo&quot;, &quot;approval&quot;: &quot;specific&quot; }]\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Without the gate, the call is refused with \u003Ccode>gate_required_for_tool\u003C\u002Fcode>. That holds however \u003Ccode>tools\u003C\u002Fcode> reaches the merge:\nby its name, or through \u003Ccode>github\u003C\u002Fcode> or \u003Ccode>github.*\u003C\u002Fcode>, which hold every GitHub tool. The gate must name\n\u003Ccode>github.merge_pull_request\u003C\u002Fcode> itself: a gate on \u003Ccode>github\u003C\u002Fcode> or \u003Ccode>github.*\u003C\u002Fcode> does not count. If a task without that gate\nreaches a run anyway, the run ends at the merge call with \u003Ccode>gate_required_for_tool\u003C\u002Fcode>, and nothing is merged.\u003C\u002Fp>\n\u003Cp>The merge is on this rule because your GitHub connection marks it must-gate. Every connection carries such a list\n(\u003Ccode>mustGate\u003C\u002Fcode> on \u003Ccode>GET \u002Fv1\u002Ftools\u002Fbindings\u003C\u002Fcode>), a GitHub connection starts with the merge on it, and only the CEO can change\nit. Create, save and a new version are all checked against the lists of every connection your enterprise has. See\n\u003Ca href=\"\u002Fdocs\u002Fcontrol\u002Fgates#merging-waits-for-you\">Gates\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"the-checks\">The checks\u003C\u002Fh2>\n\u003Cp>Creating a task and saving a version both check the definition against your enterprise's other tasks. An error\nrefuses the call with a \u003Ccode>400\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;error&quot;: &quot;invalid_task_definition&quot;,\n  &quot;message&quot;: &quot;A deterministic task has no model to call its tools, so every run fails. Use kind \\&quot;llm\\&quot;.&quot;,\n  &quot;issues&quot;: [\n    {\n      &quot;severity&quot;: &quot;error&quot;,\n      &quot;code&quot;: &quot;deterministic_with_tools&quot;,\n      &quot;message&quot;: &quot;A deterministic task has no model to call its tools, so every run fails. Use kind \\&quot;llm\\&quot;.&quot;\n    }\n  ]\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>message\u003C\u002Fcode> joins every error's sentence, and \u003Ccode>issues\u003C\u002Fcode> lists them one by one. A warning does not refuse the call: it\nis listed in the task's \u003Ccode>definitionIssues\u003C\u002Fcode> (read the task after a save to see them) and, for an active task, on\n\u003Ccode>GET \u002Fv1\u002Fblockers\u003C\u002Fcode> under \u003Ccode>taskProblems\u003C\u002Fcode>, until you fix it.\u003C\u002Fp>\n\u003Cdiv class=\"prose__table\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Code\u003C\u002Fth>\n\u003Cth>Severity\u003C\u002Fth>\n\u003Cth>When\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>gate_required_for_tool\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>error\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>tools\u003C\u002Fcode> can reach \u003Ccode>github.merge_pull_request\u003C\u002Fcode>, by its name or through \u003Ccode>github\u003C\u002Fcode> or \u003Ccode>github.*\u003C\u002Fcode>, and \u003Ccode>gates\u003C\u002Fcode> declares no gate naming it; or \u003Ccode>tools\u003C\u002Fcode> lists \u003Ccode>os.mail_send\u003C\u002Fcode> and \u003Ccode>gates\u003C\u002Fcode> declares no gate on it; or \u003Ccode>tools\u003C\u002Fcode> lists \u003Ccode>os.write_plan\u003C\u002Fcode> and \u003Ccode>gates\u003C\u002Fcode> declares no gate on \u003Ccode>os.ask_ceo\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>gate_summary_tool_required\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>error\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>tools\u003C\u002Fcode> can reach \u003Ccode>github.merge_pull_request\u003C\u002Fcode>, by its name or through \u003Ccode>github\u003C\u002Fcode> or \u003Ccode>github.*\u003C\u002Fcode>, but does not list \u003Ccode>os.set_gate_summary\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>deterministic_with_tools\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>error\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>kind\u003C\u002Fcode> is \u003Ccode>deterministic\u003C\u002Fcode> and \u003Ccode>tools\u003C\u002Fcode> is not empty.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>successor_when_unknown\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>error\u003C\u002Ftd>\n\u003Ctd>A successor's \u003Ccode>when\u003C\u002Fcode> is not \u003Ccode>has_issue\u003C\u002Fcode>, \u003Ccode>has_pr\u003C\u002Fcode> or \u003Ccode>no_pr\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>successor_cap_invalid\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>error\u003C\u002Ftd>\n\u003Ctd>A successor's \u003Ccode>maxRunsPerIssue\u003C\u002Fcode> is not a whole number of at least 1.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>successor_delay_invalid\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>error\u003C\u002Ftd>\n\u003Ctd>A successor's \u003Ccode>delayMinutes\u003C\u002Fcode> is not more than 0 and at most 1440.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>unconditional_loop\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>error\u003C\u002Ftd>\n\u003Ctd>Successors on \u003Ccode>terminal\u003C\u002Fcode> lead back to this task, so it would restart for ever.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>successor_unknown_slug\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>warning\u003C\u002Ftd>\n\u003Ctd>A successor names a task your enterprise does not have, so the work stops there.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>successor_on_unknown\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>warning\u003C\u002Ftd>\n\u003Ctd>A successor waits for an outcome the OS does not know. Usually a typo.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Ch2 id=\"change-a-task\">Change a task\u003C\u002Fh2>\n\u003Cp>\u003Ccode>POST \u002Fv1\u002Ftasks\u002F{id}\u002Fversions\u003C\u002Fcode> publishes the task's next version. Send only what changes: every field you leave\nout keeps its value from the current version, \u003Ccode>skillMd\u003C\u002Fcode> included. To clear \u003Ccode>inputs\u003C\u002Fcode>, \u003Ccode>workspace\u003C\u002Fcode> or \u003Ccode>successors\u003C\u002Fcode>,\nsend \u003Ccode>null\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Send \u003Ccode>baseVersionId\u003C\u002Fcode> too: the \u003Ccode>currentVersionId\u003C\u002Fcode> you read before you made your change.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\">curl -X POST https:\u002F\u002Fapi.zerohuman.com\u002Fv1\u002Ftasks\u002F$TASK_ID\u002Fversions \\\n  -H &quot;Authorization: Bearer zhos_…&quot; \\\n  -H &quot;content-type: application\u002Fjson&quot; \\\n  -d '{\n    &quot;baseVersionId&quot;: &quot;0b7c4a52-3d1e-4f6a-8c2b-9e5d7a1f3c60&quot;,\n    &quot;guardrails&quot;: [&quot;Never close an issue.&quot;, &quot;Never promise a release date.&quot;, &quot;Reply in English.&quot;]\n  }'\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>It answers \u003Ccode>201\u003C\u002Fcode> with the new version: its \u003Ccode>id\u003C\u002Fcode>, its \u003Ccode>version\u003C\u002Fcode> number, and its definition. That \u003Ccode>id\u003C\u002Fcode> is your next\n\u003Ccode>baseVersionId\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>If someone published a version after the one you read (a person, a script, or a self-improvement change you\napproved), your save is refused, so it cannot put the older definition back over theirs:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{ &quot;message&quot;: &quot;task_version_conflict&quot;, &quot;error&quot;: &quot;Conflict&quot;, &quot;statusCode&quot;: 409 }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Read the task again, make your change to what is there now, and save against the new \u003Ccode>currentVersionId\u003C\u002Fcode>. A save\nwithout \u003Ccode>baseVersionId\u003C\u002Fcode> is not checked this way: it publishes over whatever is current.\u003C\u002Fp>\n\u003Cp>The same call takes the task's own fields: \u003Ccode>name\u003C\u002Fcode>, \u003Ccode>slug\u003C\u002Fcode>, \u003Ccode>teamId\u003C\u002Fcode>, \u003Ccode>assigneeRole\u003C\u002Fcode>, \u003Ccode>serializeOn\u003C\u002Fcode>, \u003Ccode>status\u003C\u002Fcode> and \u003Ccode>kind\u003C\u002Fcode>. These are\nnot part of a version. They change the task at once, and the version history does not record them, though the call\nstill publishes a new version. A new \u003Ccode>slug\u003C\u002Fcode> changes the task's webhook URL, and successors that name the old slug no\nlonger find it. The save does not check that a new slug is free, so choose one no other task has.\u003C\u002Fp>\n\u003Cdiv class=\"prose__table\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Status\u003C\u002Fth>\n\u003Cth>Code\u003C\u002Fth>\n\u003Cth>Why\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>invalid_task_definition\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The new version fails one of \u003Ca href=\"#the-checks\">the checks\u003C\u002Fa>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>assignee_member_removed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The body names a member in \u003Ccode>assigneeMemberId\u003C\u002Fcode>. A task is given to a role: send \u003Ccode>assigneeRole\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>serialize_on_invalid\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>serializeOn\u003C\u002Fcode> is not \u003Ccode>null\u003C\u002Fcode> or a list of up to 8 input keys.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>skillMd_required\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The skill would be empty.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>llm_model_not_in_catalogue\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>spend.model\u003C\u002Fcode> is not a model your connected provider offers.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>403\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>task_definition_read_only\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The task is a clone: its definition belongs to the original's owner. Copy it to make your own.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>404\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>task_not_found\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No task with that id in your enterprise.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>409\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>task_version_conflict\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Someone published a newer version than your \u003Ccode>baseVersionId\u003C\u002Fcode>.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>409\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>os_task_immutable\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>It is one of the OS's own tasks: the same in every enterprise, changed only with a release of the OS, never through the API.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Cp>Saving a version declines any self-improvement recommendation still waiting on the task, since it was written\nagainst the version before. See \u003Ca href=\"\u002Fdocs\u002Fcontrol\u002Fself-improvement\">Self-improvement\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch3 id=\"versions\">Versions\u003C\u002Fh3>\n\u003Cp>\u003Ccode>GET \u002Fv1\u002Ftasks\u002F{id}\u002Fversions\u003C\u002Fcode> lists every version, newest first. \u003Ccode>GET \u002Fv1\u002Ftasks\u002F{id}\u002Fversions\u002F{versionId}\u003C\u002Fcode> reads\none as it was, with its \u003Ccode>skillMd\u003C\u002Fcode>, how many runs used it, and whether it is \u003Ccode>current\u003C\u002Fcode>. A version never changes\nonce saved. To go back to an earlier one, read it and publish its fields as a new version.\u003C\u002Fp>\n\u003Ch2 id=\"start-a-run\">Start a run\u003C\u002Fh2>\n\u003Cp>\u003Ccode>POST \u002Fv1\u002Ftasks\u002F{id}\u002Fruns\u003C\u002Fcode> starts a run on the task's current version. The body is the run's input, a JSON object:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\">curl -X POST https:\u002F\u002Fapi.zerohuman.com\u002Fv1\u002Ftasks\u002F$TASK_ID\u002Fruns \\\n  -H &quot;Authorization: Bearer zhos_…&quot; \\\n  -H &quot;content-type: application\u002Fjson&quot; \\\n  -d '{&quot;repo&quot;: &quot;acme\u002Fweb&quot;, &quot;issue&quot;: 128}'\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>It answers \u003Ccode>201\u003C\u002Fcode> with the new run, \u003Ccode>queued\u003C\u002Fcode>, and its \u003Ccode>id\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>The body becomes the run's input as sent. It is not checked against the task's \u003Ccode>inputs\u003C\u002Fcode>, and their defaults are\nnot filled in, so send every value the run needs.\u003C\u002Fp>\n\u003Cp>\u003Ccode>{id}\u003C\u002Fcode> is the task's id. To find it from the slug, \u003Ccode>GET \u002Fv1\u002Ftasks?slug=triage-new-issues\u003C\u002Fcode> lists the tasks whose\nslug contains it. A \u003Ca href=\"\u002Fdocs\u002Fapi\u002Fwebhooks\">webhook\u003C\u002Fa> starts a task by its slug instead, with your webhook secret\nrather than a token.\u003C\u002Fp>\n\u003Cdiv class=\"prose__table\">\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Status\u003C\u002Fth>\n\u003Cth>Code\u003C\u002Fth>\n\u003Cth>Why\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>404\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>task_not_found\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>No task with that id in your enterprise.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>404\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>task_not_runnable\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>It cannot take new work: it is \u003Ccode>deprecated\u003C\u002Fcode>, or nobody can run it (it has no \u003Ccode>assigneeRole\u003C\u002Fcode>, or nobody can take that role on its team or enterprise-wide). A deprecated task answers this even while a run of it is under way.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>409\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>overlap\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The task already has a run under way. A run waiting at a gate does not count.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>409\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>member_busy\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Every member holding its role on its team is busy with other work.\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003C\u002Fdiv>\n\u003Cp>A refused start begins nothing, and appears on \u003Cstrong>Runs\u003C\u002Fstrong> as \u003Ccode>skipped_overlap\u003C\u002Fcode>. Starting a run is not safe to repeat\nblindly: see \u003Ca href=\"\u002Fdocs\u002Fapi\u002Fconventions#safe-retries\">Safe retries\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"read-the-run\">Read the run\u003C\u002Fh2>\n\u003Cp>\u003Ccode>GET \u002Fv1\u002Fruns\u002F{id}\u003C\u002Fcode>, with \u003Ccode>runs:read\u003C\u002Fcode>, reads the run with its events and gates:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;run&quot;: {\n    &quot;id&quot;: &quot;5c0e9f7a-1b2d-4e3f-a4b5-c6d7e8f90a1b&quot;,\n    &quot;status&quot;: &quot;succeeded&quot;,\n    &quot;input&quot;: { &quot;repo&quot;: &quot;acme\u002Fweb&quot;, &quot;issue&quot;: 128 },\n    &quot;costUsdCents&quot;: 4,\n    &quot;startedAt&quot;: &quot;2026-09-28T09:00:03.000Z&quot;,\n    &quot;finishedAt&quot;: &quot;2026-09-28T09:01:41.000Z&quot;\n  },\n  &quot;events&quot;: [{ &quot;id&quot;: &quot;…&quot;, &quot;type&quot;: &quot;run.succeeded&quot;, &quot;payload&quot;: { &quot;result&quot;: &quot;…&quot; }, &quot;createdAt&quot;: &quot;2026-09-28T09:01:41.000Z&quot; }],\n  &quot;gates&quot;: []\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The answer also carries the task and version it ran, what came before it and what it handed to, its execution, and\nits cost.\u003C\u002Fp>\n\u003Cp>The API does not call you when a run ends, so read it until it has:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>succeeded\u003C\u002Fcode>, \u003Ccode>failed\u003C\u002Fcode> or \u003Ccode>cancelled\u003C\u002Fcode>: it has ended. A \u003Ccode>run.succeeded\u003C\u002Fcode> event carries its \u003Ccode>result\u003C\u002Fcode>, and a\n\u003Ccode>run.failed\u003C\u002Fcode> event its \u003Ccode>reason\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Ccode>waiting_on_gate\u003C\u002Fcode>: it waits for your decision, and \u003Ccode>gates\u003C\u002Fcode> holds what it waits on. Decide it on \u003Cstrong>Gates\u003C\u002Fstrong>, or with\n\u003Ccode>POST \u002Fv1\u002Fgates\u002F{id}\u002Fdecision\u003C\u002Fcode> and a token scoped \u003Ccode>gates:write\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Ccode>blocked\u003C\u002Fcode>: it is held for a reason outside the task, such as a refused credential or a provider outage, and\n\u003Ccode>blockedReason\u003C\u002Fcode> says which. Most blocked runs resume by themselves.\u003C\u002Fli>\n\u003Cli>\u003Ccode>paused_spend\u003C\u002Fcode>: a spend cap was reached. It is queued again when the cap is raised or spend limits are turned\noff. See \u003Ca href=\"\u002Fdocs\u002Fcontrol\u002Fspend\">Spend and models\u003C\u002Fa>.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Every status is on \u003Ca href=\"\u002Fdocs\u002Fwork\u002Fruns\">Runs\u003C\u002Fa>. On later reads, send \u003Ccode>afterEventId\u003C\u002Fcode> with the id of the newest event you\nhold, and only the events recorded since come back. A few from the same second may come again: skip the ids you\nalready have.\u003C\u002Fp>\n\u003Cp>\u003Ccode>GET \u002Fv1\u002Fruns\u002F{id}\u002Flog\u003C\u002Fcode> answers with the run's full log in \u003Ccode>body\u003C\u002Fcode> once it is stored, and \u003Ccode>404\u003C\u002Fcode> (\u003Ccode>log_not_found\u003C\u002Fcode>)\nuntil then. \u003Ccode>GET \u002Fv1\u002Fruns?taskId=…\u003C\u002Fcode> lists a task's runs, newest first.\u003C\u002Fp>\n",[12,16,19,23,26,29,32,35],{"id":13,"text":14,"level":15,"planned":8},"a-task-as-json","A task as JSON",2,{"id":17,"text":18,"level":15,"planned":8},"create-a-task","Create a task",{"id":20,"text":21,"level":22,"planned":8},"a-task-that-merges","A task that merges",3,{"id":24,"text":25,"level":15,"planned":8},"the-checks","The checks",{"id":27,"text":28,"level":15,"planned":8},"change-a-task","Change a task",{"id":30,"text":31,"level":22,"planned":8},"versions","Versions",{"id":33,"text":34,"level":15,"planned":8},"start-a-run","Start a run",{"id":36,"text":37,"level":15,"planned":8},"read-the-run","Read the run",1791124519341]