---
title: "Call automations.repair_step"
method: POST
path: "/api/v1/tools/automations.repair_step"
tags: ["automations"]
---

# Call automations.repair_step

`POST /api/v1/tools/automations.repair_step`

## Request

- Base URL: `https://app.goosybear.ai`
- URL: `https://app.goosybear.ai/api/v1/tools/automations.repair_step`
- Auth: HTTP bearer

'why did the email step fail?', 'fix the step that broke on the last run' — repair ONE step of an automation in this workspace, named by its board_id, that failed on its last run. Without a fix it reads how that step failed and how it is configured today, and changes nothing; omit step_key to repair the step that failed on the last run, which the answer names. With a fix it answers in two calls: without confirmation_id it proposes the change and shows that one step before and after, changing nothing; the same call again with that confirmation_id, on a member's own API key, saves it as a new draft that does not run until it is switched on. A fix that would touch any other step, the order or what starts the automation is refused. Not for rewriting the whole automation — that is `automations.author`.

Every result names the workspace it ran in. Say which workspace the answer is about. When the account has more than one workspace and none is selected, this tool refuses with `workspace_ambiguous` and lists the choices — offer them, never pick one.

## Headers

- `x-request-id` string

## Request body

- object
  - `board_id` string: The board of the automation to repair. Omit on an automation board — the conversation already knows which one it is on.
  - `step_key` string: The machine key of the step that FAILED. Leave it out to repair the step that failed on the last run; the answer names that step. Call without `fix` first to read what it was given, what it produced and how it failed.
  - `fix` object: The change to THAT ONE step. Omit on the first call. A field you leave out is left exactly as it is. You may not change the step's key, its kind, what it hands on, any other step, the order, or what starts the automation — a fix that would is REFUSED.
    - `label` string: A new name for the step.
    - `description` string: A new plain-language description of the step.
    - `implementation` string: The registered action this step should run instead, e.g. `ghl.send_email`.
    - `params` object: The step's settings, in full. What you send REPLACES the step's current settings, so include the ones that were already right. An `email.send` step's saved email is set by its `emailId`, never by the email's words.
    - `delay` object
      - `duration` object
        - `days` integer
        - `hours` integer
        - `minutes` integer
      - `until` union
        - string
        - object
          - `path` string, required
          - `offset` string, required
      - `timezone` string
    - `waitForEvent` object
      - `event` 'tag_added' | 'reply_received', required
      - `tag` string
      - `timeoutMinutes` integer, required
  - `confirmation_id` string, uuid: Omit to propose: the first call says what would happen, returns a confirmation_id and changes nothing. To go ahead, send the same arguments again with that confirmation_id, on a member's own API key; a service account's key can propose but never confirm.
  - `workspace` string: Which workspace to run in — its slug. Omit to use your default. With more than one reachable workspace and no default, the call is refused and the choices are listed.

## Response `200`

The call was admitted and dispatched. `ok` says whether the tool succeeded — a refusal the tool itself produced is still a 200, exactly as it is a successful JSON-RPC result over MCP.

- union
  - object: The tool ran and answered.
    - `ok` true, required
    - `state` union, required
      - 'diagnosis'
      - 'confirmation_required'
      - 'repaired'
    - `board_id` string, required
    - `step_key` string, required
    - `step_label` string
    - `error_message` string, nullable
    - `step` object
      - `key` string, required: A stable key: lowercase letters, digits and underscores, starting with a letter (gather_answers); later steps read its answer as steps.<key>.
      - `label` string, required: The step's name in plain language.
      - `kind` 'automated' | 'approval' | 'code_step' | 'delay' | 'conversation' | 'wait_for_event' | 'compute' | 'think' | 'ask' | 'loop' | 'branch' | 'set' | 'wait' | 'run' | 'code': What the step is. automated (the default, leave it off): runs an action by its implementation. ask: stops for a person or the contact, then carries on with the answer. wait: waits a set time, until a date, for something to happen, until a read says done, or for a callback. think: a model answers its instruction from the inputs it names, in the shape its JSON Schema declares. run: starts another automation of this workspace, and may wait for its answer. branch: sends the run down one of its edges by its rules. set: gives later steps the fields it names. loop: runs its body once for each item of a list, or until a test holds. code: JavaScript or Python, written when the step is set up with `automations.bind_step`. Every kind but automated and ask takes no implementation.
      - `description` string: What this step does, in one sentence.
      - `implementation` string: An automated step's action, by its catalogue key: contact.add_tag, contact.remove_tag, email.send, notify.member, opportunity.move_stage, contact.update_field, data.query, data.write, automation.call, or a connected tool's connector.<app>.<tool>; an ask step's is automation.ask. An unknown key is refused by name and nothing is saved, so never invent one.
      - `params` object: The step's settings, in its action's own shape: { "tag": "hot" } for contact.add_tag, { "emailId": "<a saved email's id>" } for email.send. Every action refuses a setting it does not take and names it, so ask the member rather than guessing one, and never pass settings without an implementation except on the kinds that take none. An ask step takes { "target": { "kind": "member" } or { "kind": "role", "role": "admin" } or { "kind": "contact" }, "question": "…", "answerKind": "approve_decline" or "choice" or "text", "options": ["…"], "timeout": "P3D" }: the timeout is required (ask the member for it), options only for choice, and a contact answers in text. Its answer is steps.<key>.outcome: approved, declined, answered or timed_out. A think step takes { "instruction": "…", "inputs": { "<name>": "{{ steps.<key>.<field> }}" }, "schema": <a JSON Schema> }: the schema is the answer's shape ({ "type": "string", "enum": ["yes", "no"] } for a decision; an object naming every field later steps read), and later steps read it at steps.<key>.answer. It reads the run only through its inputs, and may name a "slot": "writing.standard", "writing.plus", "writing.max" (the strongest writing last). To answer once for each item of a list, add "items": "{{ steps.<key>.<list> }}" (at most 20) and read each as {{ item }} in the inputs; the answers are steps.<key>.items[i].answer. When the work is many items and nobody waits on the answer (a nightly summary, sorting a list of replies), add "timing": "batch" and say so: the items go to the provider's batch rate, about half the usual price where the step's model has one, and the answers come back within hours rather than seconds. A wait step takes one of: { "for": "P2D" } (how long, an ISO 8601 duration, at most 90 days); { "until": "2026-11-01T09:00" } (a date-time in the workspace's zone, or one {{ … }} that gives an instant); { "event": { "type": "tag_added", "tag": "…" } or { "type": "reply_received" }, "timeout": "P3D" } (until that happens to the run's contact, at most 30 days); { "poll": { "tool": "automation.call" or "connector.<app>.<tool>", "params": { … }, "until": "{{ json.status == 'done' }}", "every": "PT5M" }, "timeout": "PT1H" }; or { "callback": { "request": { … }, "expect": <a JSON Schema> }, "timeout": "PT15M" }. A wait that times out fails as wait_timeout, and the step's on_error decides what happens next. A run step takes { "workflow": "<an automation id in this workspace>", "mode": "wait" or "start", "input": { … } }: wait holds this run until that one answers, read at steps.<key>.<field>; start sets it going and carries on. A set step's settings are the fields it gives later steps ({ "total": "{{ … }}", "label": "…" }), read at steps.<key>.<field>. A branch step takes { "rules": [ { "name": "big_order", "if": "{{ trigger.payload.total > 500 }}" } ], "mode": "first" } and no implementation: each edge out of it names the rule it follows in output, or "else" for the edge taken when no rule holds; mode "all" takes every rule that holds. A code step takes { "language": "javascript" or "python", "instruction": "<what the code does>", "inputs": { "<name>": "{{ … }}" }, "output_schema": <a JSON Schema> }; binding it writes the code, which reads only its inputs and has no network. A browser step reads a website on this workspace's browsing list: `implementation: "platform.browser"` with `{"task":"…","startUrl":"https://…","output":<a JSON Schema>,"maxMinutes":5}` (`output` when later steps read fields; up to 10 minutes). Its answer is at `stepResults.<key>.answer`. When `stepResults.<key>.outcome` is `needs_member` (a CAPTCHA, a sign-in code, a missing login, or a site off the list), follow it with an `ask` to the member quoting `stepResults.<key>.member_step`, then the same browser step again. Any setting, at any depth, reads the run's data with {{ … }} around one CEL expression. A value that is exactly one {{ … }} keeps its type (a number, a list, a map); with words around it, it is text. What an expression reads: trigger (what started the run: a webhook's body at trigger.payload.<field>, an app event's fields at trigger.event.payload.<field>); steps.<key> (an earlier step's answer: a table read's rows at steps.<key>.rows, each { row_id, values: { <column>: … } }; a think's at steps.<key>.answer); json (the step just before); item and index (inside a loop); run.id and run.contactId; contact.consent.email and contact.consent.sms. Operators: + - * / %, == != < <= > >=, && || !, cond ? a : b, x in list. Functions, each called by its bare name: size(x). For text: upper(s), lower(s), trim(s), replace(s, from, to), substring(s, start, end), split(s, sep), join(list, sep), s.contains(t), s.startsWith(t), test(s, re2), extract(s, re2), format("%s of %s", [a, b]). For numbers: round(x), round(x, 2), floor(x), ceil(x), abs(x), sum(list), avg(list), min(list), max(list), int(x), double(x), string(x). For lists: list.map(x, expr), list.filter(x, cond), list.exists(x, cond), first(list), last(list), sort(list), sortBy(list, "field"), unique(list), slice(list, from, to). For maps: has(m.field), get(m, "a.b", default), pick(m, keys), merge(a, b), keys(m), values(m). For dates: now(), dateAdd(d, "P1D"), dateDiff(a, b, "day"), dateFormat(d, "MMM d, h:mm a", "America/Chicago") (yyyy MM dd HH mm, and a zone; UTC when none), startOf(d, "week"). A value that may be missing: coalesce(trigger.payload.nickname, "friend") gives a default, and coalesce(trigger.payload.company, omit) leaves that key out. Examples: "Booking {{ trigger.payload.booking_id }} for {{ trigger.payload.guests }} guests"; { "open": "{{ size(steps.read_tasks.rows) }}" }; "{{ join(steps.read_tasks.rows.map(r, r.values.title), ', ') }}". An expression that cannot be read, names a step that is not before this one, or gives the wrong kind of value is refused at save with its path and its fix.
      - `delay` object
        - `duration` object
          - `days` integer
          - `hours` integer
          - `minutes` integer
        - `until` union
          - string
          - object
            - `path` string, required
            - `offset` string, required
        - `timezone` string
      - `waitForEvent` object
        - `event` 'tag_added' | 'reply_received', required
        - `tag` string
        - `timeoutMinutes` integer, required
    - `run_id` string
    - `test_run` boolean
    - `confirmation_id` string, uuid
    - `expires_in_seconds` integer
    - `request_summary` string: What will happen, in one sentence, for a person to approve before confirming.
    - `applies_to_version` integer
    - `expected_latest_version` integer, nullable
    - `step_before` object
      - `key` string, required: A stable key: lowercase letters, digits and underscores, starting with a letter (gather_answers); later steps read its answer as steps.<key>.
      - `label` string, required: The step's name in plain language.
      - `kind` 'automated' | 'approval' | 'code_step' | 'delay' | 'conversation' | 'wait_for_event' | 'compute' | 'think' | 'ask' | 'loop' | 'branch' | 'set' | 'wait' | 'run' | 'code': What the step is. automated (the default, leave it off): runs an action by its implementation. ask: stops for a person or the contact, then carries on with the answer. wait: waits a set time, until a date, for something to happen, until a read says done, or for a callback. think: a model answers its instruction from the inputs it names, in the shape its JSON Schema declares. run: starts another automation of this workspace, and may wait for its answer. branch: sends the run down one of its edges by its rules. set: gives later steps the fields it names. loop: runs its body once for each item of a list, or until a test holds. code: JavaScript or Python, written when the step is set up with `automations.bind_step`. Every kind but automated and ask takes no implementation.
      - `description` string: What this step does, in one sentence.
      - `implementation` string: An automated step's action, by its catalogue key: contact.add_tag, contact.remove_tag, email.send, notify.member, opportunity.move_stage, contact.update_field, data.query, data.write, automation.call, or a connected tool's connector.<app>.<tool>; an ask step's is automation.ask. An unknown key is refused by name and nothing is saved, so never invent one.
      - `params` object: The step's settings, in its action's own shape: { "tag": "hot" } for contact.add_tag, { "emailId": "<a saved email's id>" } for email.send. Every action refuses a setting it does not take and names it, so ask the member rather than guessing one, and never pass settings without an implementation except on the kinds that take none. An ask step takes { "target": { "kind": "member" } or { "kind": "role", "role": "admin" } or { "kind": "contact" }, "question": "…", "answerKind": "approve_decline" or "choice" or "text", "options": ["…"], "timeout": "P3D" }: the timeout is required (ask the member for it), options only for choice, and a contact answers in text. Its answer is steps.<key>.outcome: approved, declined, answered or timed_out. A think step takes { "instruction": "…", "inputs": { "<name>": "{{ steps.<key>.<field> }}" }, "schema": <a JSON Schema> }: the schema is the answer's shape ({ "type": "string", "enum": ["yes", "no"] } for a decision; an object naming every field later steps read), and later steps read it at steps.<key>.answer. It reads the run only through its inputs, and may name a "slot": "writing.standard", "writing.plus", "writing.max" (the strongest writing last). To answer once for each item of a list, add "items": "{{ steps.<key>.<list> }}" (at most 20) and read each as {{ item }} in the inputs; the answers are steps.<key>.items[i].answer. When the work is many items and nobody waits on the answer (a nightly summary, sorting a list of replies), add "timing": "batch" and say so: the items go to the provider's batch rate, about half the usual price where the step's model has one, and the answers come back within hours rather than seconds. A wait step takes one of: { "for": "P2D" } (how long, an ISO 8601 duration, at most 90 days); { "until": "2026-11-01T09:00" } (a date-time in the workspace's zone, or one {{ … }} that gives an instant); { "event": { "type": "tag_added", "tag": "…" } or { "type": "reply_received" }, "timeout": "P3D" } (until that happens to the run's contact, at most 30 days); { "poll": { "tool": "automation.call" or "connector.<app>.<tool>", "params": { … }, "until": "{{ json.status == 'done' }}", "every": "PT5M" }, "timeout": "PT1H" }; or { "callback": { "request": { … }, "expect": <a JSON Schema> }, "timeout": "PT15M" }. A wait that times out fails as wait_timeout, and the step's on_error decides what happens next. A run step takes { "workflow": "<an automation id in this workspace>", "mode": "wait" or "start", "input": { … } }: wait holds this run until that one answers, read at steps.<key>.<field>; start sets it going and carries on. A set step's settings are the fields it gives later steps ({ "total": "{{ … }}", "label": "…" }), read at steps.<key>.<field>. A branch step takes { "rules": [ { "name": "big_order", "if": "{{ trigger.payload.total > 500 }}" } ], "mode": "first" } and no implementation: each edge out of it names the rule it follows in output, or "else" for the edge taken when no rule holds; mode "all" takes every rule that holds. A code step takes { "language": "javascript" or "python", "instruction": "<what the code does>", "inputs": { "<name>": "{{ … }}" }, "output_schema": <a JSON Schema> }; binding it writes the code, which reads only its inputs and has no network. A browser step reads a website on this workspace's browsing list: `implementation: "platform.browser"` with `{"task":"…","startUrl":"https://…","output":<a JSON Schema>,"maxMinutes":5}` (`output` when later steps read fields; up to 10 minutes). Its answer is at `stepResults.<key>.answer`. When `stepResults.<key>.outcome` is `needs_member` (a CAPTCHA, a sign-in code, a missing login, or a site off the list), follow it with an `ask` to the member quoting `stepResults.<key>.member_step`, then the same browser step again. Any setting, at any depth, reads the run's data with {{ … }} around one CEL expression. A value that is exactly one {{ … }} keeps its type (a number, a list, a map); with words around it, it is text. What an expression reads: trigger (what started the run: a webhook's body at trigger.payload.<field>, an app event's fields at trigger.event.payload.<field>); steps.<key> (an earlier step's answer: a table read's rows at steps.<key>.rows, each { row_id, values: { <column>: … } }; a think's at steps.<key>.answer); json (the step just before); item and index (inside a loop); run.id and run.contactId; contact.consent.email and contact.consent.sms. Operators: + - * / %, == != < <= > >=, && || !, cond ? a : b, x in list. Functions, each called by its bare name: size(x). For text: upper(s), lower(s), trim(s), replace(s, from, to), substring(s, start, end), split(s, sep), join(list, sep), s.contains(t), s.startsWith(t), test(s, re2), extract(s, re2), format("%s of %s", [a, b]). For numbers: round(x), round(x, 2), floor(x), ceil(x), abs(x), sum(list), avg(list), min(list), max(list), int(x), double(x), string(x). For lists: list.map(x, expr), list.filter(x, cond), list.exists(x, cond), first(list), last(list), sort(list), sortBy(list, "field"), unique(list), slice(list, from, to). For maps: has(m.field), get(m, "a.b", default), pick(m, keys), merge(a, b), keys(m), values(m). For dates: now(), dateAdd(d, "P1D"), dateDiff(a, b, "day"), dateFormat(d, "MMM d, h:mm a", "America/Chicago") (yyyy MM dd HH mm, and a zone; UTC when none), startOf(d, "week"). A value that may be missing: coalesce(trigger.payload.nickname, "friend") gives a default, and coalesce(trigger.payload.company, omit) leaves that key out. Examples: "Booking {{ trigger.payload.booking_id }} for {{ trigger.payload.guests }} guests"; { "open": "{{ size(steps.read_tasks.rows) }}" }; "{{ join(steps.read_tasks.rows.map(r, r.values.title), ', ') }}". An expression that cannot be read, names a step that is not before this one, or gives the wrong kind of value is refused at save with its path and its fix.
      - `delay` object
        - `duration` object
          - `days` integer
          - `hours` integer
          - `minutes` integer
        - `until` union
          - string
          - object
            - `path` string, required
            - `offset` string, required
        - `timezone` string
      - `waitForEvent` object
        - `event` 'tag_added' | 'reply_received', required
        - `tag` string
        - `timeoutMinutes` integer, required
    - `step_after` object
      - `key` string, required: A stable key: lowercase letters, digits and underscores, starting with a letter (gather_answers); later steps read its answer as steps.<key>.
      - `label` string, required: The step's name in plain language.
      - `kind` 'automated' | 'approval' | 'code_step' | 'delay' | 'conversation' | 'wait_for_event' | 'compute' | 'think' | 'ask' | 'loop' | 'branch' | 'set' | 'wait' | 'run' | 'code': What the step is. automated (the default, leave it off): runs an action by its implementation. ask: stops for a person or the contact, then carries on with the answer. wait: waits a set time, until a date, for something to happen, until a read says done, or for a callback. think: a model answers its instruction from the inputs it names, in the shape its JSON Schema declares. run: starts another automation of this workspace, and may wait for its answer. branch: sends the run down one of its edges by its rules. set: gives later steps the fields it names. loop: runs its body once for each item of a list, or until a test holds. code: JavaScript or Python, written when the step is set up with `automations.bind_step`. Every kind but automated and ask takes no implementation.
      - `description` string: What this step does, in one sentence.
      - `implementation` string: An automated step's action, by its catalogue key: contact.add_tag, contact.remove_tag, email.send, notify.member, opportunity.move_stage, contact.update_field, data.query, data.write, automation.call, or a connected tool's connector.<app>.<tool>; an ask step's is automation.ask. An unknown key is refused by name and nothing is saved, so never invent one.
      - `params` object: The step's settings, in its action's own shape: { "tag": "hot" } for contact.add_tag, { "emailId": "<a saved email's id>" } for email.send. Every action refuses a setting it does not take and names it, so ask the member rather than guessing one, and never pass settings without an implementation except on the kinds that take none. An ask step takes { "target": { "kind": "member" } or { "kind": "role", "role": "admin" } or { "kind": "contact" }, "question": "…", "answerKind": "approve_decline" or "choice" or "text", "options": ["…"], "timeout": "P3D" }: the timeout is required (ask the member for it), options only for choice, and a contact answers in text. Its answer is steps.<key>.outcome: approved, declined, answered or timed_out. A think step takes { "instruction": "…", "inputs": { "<name>": "{{ steps.<key>.<field> }}" }, "schema": <a JSON Schema> }: the schema is the answer's shape ({ "type": "string", "enum": ["yes", "no"] } for a decision; an object naming every field later steps read), and later steps read it at steps.<key>.answer. It reads the run only through its inputs, and may name a "slot": "writing.standard", "writing.plus", "writing.max" (the strongest writing last). To answer once for each item of a list, add "items": "{{ steps.<key>.<list> }}" (at most 20) and read each as {{ item }} in the inputs; the answers are steps.<key>.items[i].answer. When the work is many items and nobody waits on the answer (a nightly summary, sorting a list of replies), add "timing": "batch" and say so: the items go to the provider's batch rate, about half the usual price where the step's model has one, and the answers come back within hours rather than seconds. A wait step takes one of: { "for": "P2D" } (how long, an ISO 8601 duration, at most 90 days); { "until": "2026-11-01T09:00" } (a date-time in the workspace's zone, or one {{ … }} that gives an instant); { "event": { "type": "tag_added", "tag": "…" } or { "type": "reply_received" }, "timeout": "P3D" } (until that happens to the run's contact, at most 30 days); { "poll": { "tool": "automation.call" or "connector.<app>.<tool>", "params": { … }, "until": "{{ json.status == 'done' }}", "every": "PT5M" }, "timeout": "PT1H" }; or { "callback": { "request": { … }, "expect": <a JSON Schema> }, "timeout": "PT15M" }. A wait that times out fails as wait_timeout, and the step's on_error decides what happens next. A run step takes { "workflow": "<an automation id in this workspace>", "mode": "wait" or "start", "input": { … } }: wait holds this run until that one answers, read at steps.<key>.<field>; start sets it going and carries on. A set step's settings are the fields it gives later steps ({ "total": "{{ … }}", "label": "…" }), read at steps.<key>.<field>. A branch step takes { "rules": [ { "name": "big_order", "if": "{{ trigger.payload.total > 500 }}" } ], "mode": "first" } and no implementation: each edge out of it names the rule it follows in output, or "else" for the edge taken when no rule holds; mode "all" takes every rule that holds. A code step takes { "language": "javascript" or "python", "instruction": "<what the code does>", "inputs": { "<name>": "{{ … }}" }, "output_schema": <a JSON Schema> }; binding it writes the code, which reads only its inputs and has no network. A browser step reads a website on this workspace's browsing list: `implementation: "platform.browser"` with `{"task":"…","startUrl":"https://…","output":<a JSON Schema>,"maxMinutes":5}` (`output` when later steps read fields; up to 10 minutes). Its answer is at `stepResults.<key>.answer`. When `stepResults.<key>.outcome` is `needs_member` (a CAPTCHA, a sign-in code, a missing login, or a site off the list), follow it with an `ask` to the member quoting `stepResults.<key>.member_step`, then the same browser step again. Any setting, at any depth, reads the run's data with {{ … }} around one CEL expression. A value that is exactly one {{ … }} keeps its type (a number, a list, a map); with words around it, it is text. What an expression reads: trigger (what started the run: a webhook's body at trigger.payload.<field>, an app event's fields at trigger.event.payload.<field>); steps.<key> (an earlier step's answer: a table read's rows at steps.<key>.rows, each { row_id, values: { <column>: … } }; a think's at steps.<key>.answer); json (the step just before); item and index (inside a loop); run.id and run.contactId; contact.consent.email and contact.consent.sms. Operators: + - * / %, == != < <= > >=, && || !, cond ? a : b, x in list. Functions, each called by its bare name: size(x). For text: upper(s), lower(s), trim(s), replace(s, from, to), substring(s, start, end), split(s, sep), join(list, sep), s.contains(t), s.startsWith(t), test(s, re2), extract(s, re2), format("%s of %s", [a, b]). For numbers: round(x), round(x, 2), floor(x), ceil(x), abs(x), sum(list), avg(list), min(list), max(list), int(x), double(x), string(x). For lists: list.map(x, expr), list.filter(x, cond), list.exists(x, cond), first(list), last(list), sort(list), sortBy(list, "field"), unique(list), slice(list, from, to). For maps: has(m.field), get(m, "a.b", default), pick(m, keys), merge(a, b), keys(m), values(m). For dates: now(), dateAdd(d, "P1D"), dateDiff(a, b, "day"), dateFormat(d, "MMM d, h:mm a", "America/Chicago") (yyyy MM dd HH mm, and a zone; UTC when none), startOf(d, "week"). A value that may be missing: coalesce(trigger.payload.nickname, "friend") gives a default, and coalesce(trigger.payload.company, omit) leaves that key out. Examples: "Booking {{ trigger.payload.booking_id }} for {{ trigger.payload.guests }} guests"; { "open": "{{ size(steps.read_tasks.rows) }}" }; "{{ join(steps.read_tasks.rows.map(r, r.values.title), ', ') }}". An expression that cannot be read, names a step that is not before this one, or gives the wrong kind of value is refused at save with its path and its fix.
      - `delay` object
        - `duration` object
          - `days` integer
          - `hours` integer
          - `minutes` integer
        - `until` union
          - string
          - object
            - `path` string, required
            - `offset` string, required
        - `timezone` string
      - `waitForEvent` object
        - `event` 'tag_added' | 'reply_received', required
        - `tag` string
        - `timeoutMinutes` integer, required
    - `version` integer
    - `workspace` string, required: The slug of the workspace this call ran in.
    - `working_in` object, required: Which workspace this call ran in, and how that was decided. Present on every workspace-scoped result.
      - `label` string, required: The chip words — e.g. "working in: Acme Main".
      - `note` string, required: Why this workspace — e.g. "your default", "this call only".
      - `workspace` string, required: The resolved workspace's slug.
      - `source` 'call-override' | 'key-pin' | 'stored-default' | 'sole-reachable', required: Which rung of the R7 precedence resolved the workspace.
  - ToolRefusal: A call that was admitted and dispatched, and did not succeed.
    - `ok` false, required
    - `code` string, required: The typed reason — e.g. `workspace_ambiguous`, `invalid_arguments`, or the tool's own failure code.
    - `message` string, required: A sentence a person can act on.
    - `workspaces` string[]: On a workspace refusal: the slugs this key may pass as `workspace`.
    - `issues` object[]: On `invalid_arguments`: which fields failed, and why.
      - `path` string, required
      - `message` string, required
    - `path` string: On an authoring refusal: where in your call's input it points, e.g. `steps.<key>.params.<field>`.
    - `fix` string: On an authoring refusal: the change to your call that would pass.
    - `instruction` string: On an authoring refusal: what to do next with it.

## Other responses

- `400`: The request could not be used: the body is present but not a JSON object (`tenant_api.malformed_body`); it is a top-level array carrying more messages than this endpoint accepts (`tenant_mcp.batch_too_large`); or the `Idempotency-Key` header is malformed (`tenant_api.idempotency_key_invalid`), disagrees with the body's `idempotency_key` (`tenant_api.idempotency_key_conflict`), or was sent to an action that cannot replay a retry (`tenant_api.idempotency_unsupported`). Nothing ran.
- `401`: No bearer credential, or one that did not resolve. Deliberately ONE undifferentiated answer for absent / unknown / revoked / expired, so the endpoint is never an oracle over the key space.
- `403`: The credential is good but the surface is not open to it — the platform or account API switch is off, or the holder lacks the `api.access` capability. The `code` says which.
- `404`: No such tool. The body names no tool and is identical for every unrecognised name.
- `429`: Over this key's per-minute cap. Carries `Retry-After` and `error.retry_after_seconds`.

---

[API](https://skmtc.dev/goosybear/apis/goosy-bear-api.md) · [All operations](https://skmtc.dev/goosybear/apis/goosy-bear-api/llms.txt) · [OpenAPI document](https://skmtc.dev/goosybear/apis/goosy-bear-api/revisions/0e033701d3f7?raw)
