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

# Call automations.author

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

## Request

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

Propose, then confirm, one complete draft version of an existing automation board — 'add a step that emails new leads', 'rewrite this automation to wait three days first'. Confirming validates the connector bindings, builder availability, the graph and the trigger before anything is written, and the version is saved as a draft that does not run. Not for switching a version on — that is `automations.api_activate`.

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, uuid, required
  - `name` string, required: What this automation is called, in the member's words.
  - `description` string: One or two plain sentences saying what it is for.
  - `steps` object[], required: The steps, in the order they run. At most 24. An automated or ask step with no implementation is saved as a shape and refused when the automation is switched on.
    - `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.
    - `description` string: What this step does, in one sentence.
    - `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.
    - `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
    - `outputs` string[]
    - `promptFirst` boolean
    - `retries` integer
    - `retry` object: Only when the member wants an action tried again after it fails (a service that was briefly down): { "max": 3, "wait": "PT10S" }, at most 5 more tries, at most 30 seconds apart. Only on an action step or a callback wait; a think retries on its own.
      - `max` integer, required
      - `wait` string
    - `on_error` 'stop' | 'continue' | 'route': What happens when this step fails. stop (the default, leave it off) ends the run there. continue carries on to the next step, which can read steps.<key>.ok (false) and steps.<key>.code. route follows this step's edge with output "error" instead of its usual next step: add that edge.
    - `loop` object: A loop step: "items" (one {{ … }} that gives the list; the body reads {{ item }} and {{ index }}) or "repeat" ({ "max": 5 } times, or { "until": "{{ json.done }}", "max": 10 } over each round's answer), and its "body": an action, its params, and "on_error" ("continue" skips a failed item).
      - `items` string
      - `repeat` object
        - `until` string
        - `max` integer, required
      - `over` string
      - `until` unknown
      - `while` unknown
      - `concurrency` integer
      - `onItemFailure` 'skip' | 'stop' | 'fail'
      - `body` object, required
        - `implementation` string, required
        - `params` object
        - `retries` integer
        - `retry` object
          - `max` integer, required
          - `wait` string
        - `on_error` 'stop' | 'continue' | 'route'
    - `sketch` true
    - `compute` object
      - `task` string, required
      - `payload` object
      - `resumeOn` 'completion'
      - `reliability` object
        - `deadlineMs` integer, required
        - `maxAttempts` integer, required
        - `idempotencyKey` string, required
  - `edges` object[]: Only when the run does not simply go through the steps in order: which step leads to which. Leave it out for a straight line; an empty list is refused.
    - `from` string, required
    - `to` string, required
    - `when` unknown
    - `on` 'failure'
    - `whenLabel` string
    - `output` string: Which outcome this edge follows. Out of a branch: the name of the rule it follows, or "else" for the edge taken when no rule holds. Out of an ask: approved, declined, answered or timed_out; once one edge out of an ask names one, every edge out of it does, and an outcome with no edge ends the run. "error": the edge a step with on_error route follows when it fails.
  - `trigger` union: What fires it. Omit when the operator did not say — never invent a schedule.
    - object
      - `kind` 'manual', required
    - object
      - `kind` 'webhook', required
    - object
      - `kind` 'schedule', required
      - `cron` string, required: Five whitespace-separated cron fields, e.g. `0 9 * * 1`. Required for `schedule`.
      - `timezone` string, required: An IANA timezone name, e.g. `America/Chicago`. Required for `schedule` — ask rather than assume one.
    - object
      - `kind` 'app_event', required
      - `event_key` string, required: Which event fires it. Required for `app_event`. `crm.tag_applied` — When a tag is added to a contact in the CRM — any tag, or only the one tag you choose. `crm.tag_removed` — When a tag is taken off a contact in the CRM. `crm.contact_created` — When a new contact is added to the CRM. `crm.contact_updated` — When a contact's own details are edited in the CRM — optionally only when their email, name or phone changes. `crm.contact_deleted` — When a contact is removed from the CRM. `crm.opportunity_opened` — When a deal is created on one of your pipelines. `crm.opportunity_closed` — When a deal is won, lost or abandoned. `crm.stage_changed` — When a deal moves from one pipeline stage to another. `appointment.booked` — When someone books a time on one of your calendars. `appointment.status_changed` — When a booking is confirmed, showed, marked a no-show, or cancelled. `invoice.paid` — When an invoice is paid in full. `payment.received` — When an order is paid — the amount and currency come with it. `email.opened` — When a contact opens an email you sent them. `email.clicked` — When a contact clicks a link in an email you sent them. `email.bounced` — When an email to a contact could not be delivered. `inbox.reply_received` — When a contact replies on any channel your inbox carries. `inbox.tag_applied` — When someone puts a tag on a conversation in the inbox. `pages.lead.captured` — When someone submits a form on one of your published pages. `contact.date_reached` — When a contact's birthday comes round — on the day, or a chosen number of days before or after, at the time you choose. `board_row.date_reached` — When a date in one of your board tables comes round — on the day, or a chosen number of days before or after, at the time you choose. `automation.sequence_enrolled` — When another automation enrols someone into this sequence — the emails and waits below start for that person.
      - `changed_fields` string[]: ONLY for `crm.contact_updated`: narrow it to the fields the operator named, e.g. `["phone"]` for "when their phone number changes". Omit for any change to the contact. Passing it with ANY OTHER event key is REFUSED — no other event says which fields changed, so a filter on one would match nothing and the automation would silently never run. A contact field we do not store — an address, a company, a custom field — cannot be named either, so say so rather than choosing a neighbouring one.
      - `date_field` union: REQUIRED for `contact.date_reached`, and refused on every other key: WHICH stored date to watch. Only dates this product actually stores can be named — a renewal date or a CRM custom field cannot, so say so rather than choosing a neighbouring one.
        - 'date_of_birth'
        - object
          - `table_id` string, uuid, required
          - `column_id` string, required
      - `offset_days` integer: REQUIRED for `contact.date_reached`, and refused on every other key: how many days BEFORE the date to fire. `0` is on the day, `3` is three days before. Negative is not accepted — an anniversary that has passed is not a date being reached.
      - `direction` 'before' | 'after': For a date event: fire before (default) or after the date.
      - `at_local_time` string: For a date event: the local time it fires, HH:MM (default 09:00).
      - `timezone` string: An IANA timezone name, e.g. `America/Chicago`. Required for `schedule` — ask rather than assume one.
      - `payload` object: For a `custom.<name>` event: its fields, each `{ name, type, required }` (type text, number, boolean, date, email or phone).
        - `fields` object[], required
          - `name` string, required
          - `type` 'text' | 'number' | 'boolean' | 'date' | 'email' | 'phone', required
          - `required` boolean
      - `pipeline_stage_ids` string[]: ONLY for a deal event (`crm.opportunity_opened`, `crm.opportunity_closed`, `crm.stage_changed`): narrow it to the pipeline stages the operator named, e.g. the id of "Proposal sent" for "when a deal reaches proposal". Omit for any stage on any pipeline. These are the `id` values `pipeline.list` returns for the STAGES — never a GoHighLevel pipeline or stage ref, which would match nothing. Passing it with ANY OTHER event key is REFUSED — no other event says which stage it happened on, so a scope on one would match nothing and the automation would silently never run.
      - `tag` string: For `crm.tag_applied`: the one tag name it waits for; omit for any tag.
      - `form` string: For `pages.lead.captured`: the one page form it waits for, as page.read's `forms` names it; omit for any form. A form whose `event` is set starts through that `custom.*` event instead.
  - `sketch_trigger` object
    - `kind` 'manual' | 'schedule' | 'webhook' | 'app_event', required: What will start it, before it is set up.
  - `review_gated` boolean
  - `callable` object
    - `inputSchema` object
    - `outputSchema` object
    - `output` object
  - `run_queue` string
  - `error_workflow` string, uuid: Only when the member names one: the id of another automation in this workspace that runs whenever a run of this one fails. Its input names what failed, under failed: the run, the automation, the step, and why: its code and message. Leave it out on a change to keep the one already set.
  - `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
      - 'confirmation_required'
      - 'saved'
    - `confirmation_id` string, uuid
    - `expires_in_seconds` integer
    - `request_summary` string: What will happen, in one sentence, for a person to approve before confirming.
    - `board_id` string, uuid, required
    - `expected_latest_version` integer, nullable
    - `version` integer
    - `display_name` string
    - `created` boolean
    - `status` 'draft'
    - `bound_steps` string[]
    - `unbound_steps` string[]
    - `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`.

## Changes

- **2026-10-19** `b398332ed38c`: 1 breaking, 1 info
  - removed `subschema #3, subschema #4` from the `trigger` request property `oneOf` list
  - added `subschema #3, subschema #4` to the `trigger` request property `oneOf` list

[Operation history](https://skmtc.dev/goosybear/apis/goosy-bear-api/changes/api/v1/tools/automations.author/post.md)

---

[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/bd8f793b36b5?raw)
