# Automate client follow-up

Automations do the follow-up a broker would otherwise remember to do: email the client and the
realtor when a deal moves into a stage, make a call task the next day, wish a past client happy
birthday. Over the API you build exactly what the Pulse builder in BrokerPlus builds, and you turn
it on the same way the broker does.

The words an automation sends live in [templates](/templates). Write those first.

## How an automation works

An automation has one trigger and one or more steps.

- The **trigger** (`type`) is the event that starts it. `stage.transitioned` fires when a deal moves
  into a stage, `deal.status_changed` when the lender approves a deal, and `birthday` on a client's
  birthday. `config` holds the trigger's settings, such as which stage.
- **Checks** in `filters` decide which deals or contacts it starts for, for example purchases only.
- Each **step** does one thing: send an email or a text from a template, create a task, add a tag,
  move the deal, and more. `delay_days` makes a step wait, and `only_if` adds checks for that step.
- A **graph** holds an automation that forks or waits. A `branch` node sends the run down one of two
  paths, and a `waitFor` node waits for the client to reply. A graph compiles to steps, so an
  automation with a graph has `steps` too.

`GET /v1/automations/catalog` is the builder's whole vocabulary: every trigger type and its `config`,
every step `kind` and its `params`, who a message can go to, the fields a check can compare, and the
node kinds of a graph. Read it before you build, and you'll only name things that exist.

## What a key can do

A key sees the automations on its broker's Pulse board, and no others. Creating, changing, turning
on, or deleting one needs the broker to have permission to change automations in BrokerPlus.
Without it, the API returns `403 AUTOMATION_MANAGE_FORBIDDEN`.

| Request                               | What it does                                          |
| ------------------------------------- | ----------------------------------------------------- |
| `POST /v1/automations`                | Creates an automation, switched off.                  |
| `PATCH /v1/automations/{id}`          | Changes it. A change to what it does switches it off. |
| `POST /v1/automations/{id}/duplicate` | Makes a copy, switched off.                           |
| `POST /v1/automations/{id}/turn-on`   | Turns it on now.                                      |
| `POST /v1/automations/{id}/turn-off`  | Turns it off now.                                     |
| `DELETE /v1/automations/{id}`         | Turns it off and removes it from the board.           |

Because every write leaves the automation off, you can draft and refine one safely. Nothing runs
until it is turned on.

## Turn an automation on

An automation that is on runs with nobody watching. `POST /v1/automations/{id}/turn-on` turns it on
now, the same as its switch in BrokerPlus, and returns `200` with the automation. When
`can_message_clients` is `true`, its steps can reach clients from that moment. Read each step before
you turn it on.

A change to what the automation does switches it off again, and you turn it on again. A key with the
`no_sending` limit can't turn on an automation that messages clients (`403 KEY_LIMITED`). That limit
is the switch that keeps a key from reaching clients.

## Ask before sending

Each step has `ask_before_sending`. When it is `true`, the step puts a draft in the broker's queue
and the client gets nothing until the broker approves that draft. When it is `false`, the step sends
with no review.

Leave it out and BrokerPlus uses the catalog's `default_ask_before_sending` for that kind, which is
`true` for every step that reaches a client. Some steps always ask (`always_asks_before_sending` in
the catalog), and they refuse `false`.

## Example: email the client and the realtor when a deal moves into a stage

This is the automation brokers ask for most. When a deal moves into the "Conditions removed" stage,
BrokerPlus emails the client and the realtor from a saved template, held in the broker's queue for
review. A day later, it creates a task to call the client.

### 1. Get the ids

Get the stage id from `GET /v1/pipelines` and the template id from `GET /v1/templates`. The
recipients come from the catalog's `recipients`: `primary_applicant` is the client, and
`partner:realtor` is the realtor on the deal.

An email step can also go to a teammate by their role on the deal, for example
`deal-role:Closing Officer`, `deal-role:@underwriter` or `deal-role:@submitting`. Use the role name
as your team has it in BrokerPlus. The email goes to whoever holds that role on each deal. In
`ccRecipients` or `bccRecipients`, an empty role is left off. In `toRecipients`, a step with nobody
left to send to gets the status `needs_attention`, and its reason names the empty role.

### 2. Create the automation

<CodeTabs syncKey="lang">

```bash title="curl"
curl -X POST "https://api.mos.getbrokerplus.ca/v1/automations" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7b1d0c1e-3f8a-4a51-9a63-2c5f8a0e4d21" \
  -d '{
    "name": "Conditions removed",
    "type": "stage.transitioned",
    "config": { "stageId": "cmt4au1kd0000p3iuh6c0q2re" },
    "steps": [
      {
        "kind": "email",
        "template_id": "cmt4atz7t0000p3iuy1r9c3fe",
        "params": { "toRecipients": ["primary_applicant", "partner:realtor"] },
        "ask_before_sending": true
      },
      {
        "kind": "task",
        "delay_days": 1,
        "params": { "title": "Call the client about next steps", "priority": "medium" }
      }
    ]
  }'
```

```ts title="TypeScript"
const res = await fetch("https://api.mos.getbrokerplus.ca/v1/automations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BROKERPLUS_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    name: "Conditions removed",
    type: "stage.transitioned",
    config: { stageId: "cmt4au1kd0000p3iuh6c0q2re" },
    steps: [
      {
        kind: "email",
        template_id: "cmt4atz7t0000p3iuy1r9c3fe",
        params: { toRecipients: ["primary_applicant", "partner:realtor"] },
        ask_before_sending: true,
      },
      { kind: "task", delay_days: 1, params: { title: "Call the client about next steps", priority: "medium" } },
    ],
  }),
});
const { data } = await res.json();
```

```python title="Python"
import os
import uuid

import requests

res = requests.post(
    "https://api.mos.getbrokerplus.ca/v1/automations",
    json={
        "name": "Conditions removed",
        "type": "stage.transitioned",
        "config": {"stageId": "cmt4au1kd0000p3iuh6c0q2re"},
        "steps": [
            {
                "kind": "email",
                "template_id": "cmt4atz7t0000p3iuy1r9c3fe",
                "params": {"toRecipients": ["primary_applicant", "partner:realtor"]},
                "ask_before_sending": True,
            },
            {
                "kind": "task",
                "delay_days": 1,
                "params": {"title": "Call the client about next steps", "priority": "medium"},
            },
        ],
    },
    headers={
        "Authorization": f"Bearer {os.environ['BROKERPLUS_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
)
data = res.json()["data"]
```

</CodeTabs>

The response is `201` and the automation, switched off (`"enabled": false`).

```json
{
  "success": true,
  "data": {
    "object": "automation",
    "id": "cmt4au7q90001p3iu2b8x1k7d",
    "name": "Conditions removed",
    "description": null,
    "type": "stage.transitioned",
    "type_label": "Deal Moved into a Stage",
    "config": { "stageId": "cmt4au1kd0000p3iuh6c0q2re" },
    "filters": { "match": "all", "rules": [] },
    "enabled": false,
    "can_message_clients": true,
    "repeat_policy": "once_per_subject",
    "team_id": "cmt49zz1v0000p3iub7d3e5qa",
    "canvas_id": null,
    "canvas_name": null,
    "sort_order": null,
    "anchor_offset_days": null,
    "drawn_on_canvas": false,
    "graph": null,
    "steps": [
      {
        "object": "automation_step",
        "id": "cmt4au7qa0002p3iu8u0v2h1m",
        "position": 0,
        "kind": "email",
        "label": "Send an email",
        "delay_days": 0,
        "send_at_time": null,
        "ask_before_sending": true,
        "template_id": "cmt4atz7t0000p3iuy1r9c3fe",
        "template_name": "Conditions removed",
        "params": {
          "templateId": "cmt4atz7t0000p3iuy1r9c3fe",
          "toRecipients": ["primary_applicant", "partner:realtor"],
          "ccRecipients": [],
          "bccRecipients": []
        },
        "only_if": { "match": "all", "rules": [] },
        "created_at": "2026-10-07T15:02:11.000Z",
        "updated_at": "2026-10-07T15:02:11.000Z"
      },
      {
        "object": "automation_step",
        "id": "cmt4au7qa0003p3iuw2n6c4ve",
        "position": 1,
        "kind": "task",
        "label": "Create a task",
        "delay_days": 1,
        "send_at_time": null,
        "ask_before_sending": false,
        "template_id": null,
        "template_name": null,
        "params": { "title": "Call the client about next steps", "priority": "medium" },
        "only_if": { "match": "all", "rules": [] },
        "created_at": "2026-10-07T15:02:11.000Z",
        "updated_at": "2026-10-07T15:02:11.000Z"
      }
    ],
    "created_at": "2026-10-07T15:02:11.000Z",
    "updated_at": "2026-10-07T15:02:11.000Z"
  }
}
```

### 3. Turn it on

The email step can reach a client, so read the steps before you turn it on.

```bash
curl -X POST "https://api.mos.getbrokerplus.ca/v1/automations/cmt4au7q90001p3iu2b8x1k7d/turn-on" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY" \
  -H "Idempotency-Key: 0c9e4f8b-52a7-4f1e-8d3b-6a2e1f7c9b40"
```

The response is `200` with the automation, and `enabled` is `true`. The next deal that moves into the
stage gets an email waiting in the broker's drafts queue, because the step asks before sending.

## Errors tell you how to fix the definition

BrokerPlus checks an automation exactly as the builder does. A refusal is `400` (or `404` for a
record that isn't in the brokerage), `details.fieldErrors` names each field at fault, and each
message says how to fix it.

```json
{
  "success": false,
  "error": {
    "code": "INVALID_STEP",
    "message": "steps.1.delay_days: Step 2 waits 0 days, less than step 1 (2 days). The delay counts from the start, so list the steps in the order they run, or set this delay_days to 2 or more.",
    "details": {
      "formErrors": [],
      "fieldErrors": {
        "steps.1.delay_days": [
          "Step 2 waits 0 days, less than step 1 (2 days). The delay counts from the start, so list the steps in the order they run, or set this delay_days to 2 or more."
        ]
      }
    }
  }
}
```

| `code`                       | What is wrong                                                                          |
| ---------------------------- | -------------------------------------------------------------------------------------- |
| `VALIDATION_ERROR`           | A field has the wrong shape, or `type` is not in the catalog.                          |
| `INVALID_STEP`               | A step kind is unknown, a setting in `params` is missing or wrong, or the steps are out of order. |
| `INVALID_TRIGGER_CONFIG`     | A setting in `config` is missing or wrong for the trigger.                             |
| `INVALID_TRIGGER_FILTERS`    | A check names a field that the trigger cannot read.                                    |
| `INVALID_GRAPH`              | The graph cannot compile, for example a branch with one path.                          |
| `TEMPLATE_SUBJECT_MISMATCH`  | A template uses a merge field that the trigger cannot fill, or one that does not exist. |
| `NOT_FOUND`                  | A template, stage, tag, or other record is not in the brokerage.                       |

A step that writes its own words to a client, such as `draft_email`, is refused. Save the words as a
[template](/templates) and send it from an `email` or `sms` step instead.

## Read the automations

`GET /v1/automations` lists automations, newest first. Filter with `type` (for example
`stage.transitioned`) and `enabled=true`, or sync with `updated_since` (see [Sync](/sync)).
`GET /v1/automations/{id}` returns one.

## Change an automation

`PATCH /v1/automations/{id}` changes only the fields you send. `steps` replaces every step, and
`graph` replaces the whole graph.

- A change to `config`, `stage_id`, `filters`, `repeat_policy`, `anchor_offset_days`, `steps`, or
  `graph` switches the automation off.
- A change to only `name`, `description`, or `canvas_id` leaves it as it was.
- `type` can't change. Create a new automation, or send a graph whose trigger node has the new type.
- When the automation has a graph (`drawn_on_canvas` is `true`), send a changed `graph`. A `steps`
  change returns `409 DRAWN_ON_CANVAS`, because a list of steps cannot hold its branches.

`POST /v1/automations/{id}/duplicate` makes a copy named `<name> copy`. `DELETE /v1/automations/{id}`
turns it off and removes it from the board, and its runs stay readable.

## Group automations on the canvas

On the automation canvas, a broker files automations in groups. A group belongs to one broker team,
and only its members see it. Over the API:

| Request                                           | What it does                                                    |
| ------------------------------------------------- | --------------------------------------------------------------- |
| `GET /v1/automations/canvases`                    | Lists the groups, highest `position` first.                     |
| `POST /v1/automations/canvases`                   | Makes a group on the broker's active team.                      |
| `PATCH /v1/automations/canvases/{id}`             | Renames, describes, or moves a group.                           |
| `DELETE /v1/automations/canvases/{id}`            | Deletes the group and turns off and deletes its automations.    |
| `POST /v1/automations/canvases/{id}/turn-on`      | Turns on every automation in the group, as described below.     |
| `POST /v1/automations/canvases/{id}/turn-off`     | Turns off every automation in the group now.                    |

File an automation in a group with `canvas_id` on `POST /v1/automations` or
`PATCH /v1/automations/{id}`. `automation_count` and `enabled_count` tell you whether the group is on,
off, or partly on.

A group has no switch of its own: turning it on turns on each automation in it now, the same as
`POST /v1/automations/{id}/turn-on`. `results` has a line for each automation: `on` or `unchanged`.
If an automation that is off can message clients and your key has the `no_sending` limit, the
response is `403 KEY_LIMITED` and nothing changes.

## Stop automations messaging a client

A stop keeps automations from messaging one client, the same as Stop automated outreach on a contact
in BrokerPlus. `POST /v1/automations/stops` with `contact_id` stops every automation for every deal of
the client. Add `deal_id` to stop them for one deal, or `automation_types` to stop only some kinds.
This only means fewer messages, so it happens now.

`GET /v1/automations/stops` lists the stops. A stop can also come from a stage that stops automated
messages (`held_by_stage`) or from a client tag automation (`held_by_tag_ids`). Those end by
themselves when the client leaves the stage or loses the tag.

`POST /v1/automations/stops/{id}/lift` lifts the stop now and returns `200`. Every automation it held
back can message the client again. A key with the `no_sending` limit can't lift a stop
(`403 KEY_LIMITED`).

## Read the runs

A run is one automation working through its steps for one deal or contact. `GET /v1/automation-runs`
lists them. Filter by `automation_id`, `deal_id`, or `contact_id`.
`POST /v1/automation-runs/{id}/start` starts a `scheduled` run now, the same as Run now in
BrokerPlus, and returns the run. Its steps can reach the client, so a key with the `no_sending`
limit gets `403 KEY_LIMITED`. `POST /v1/automation-runs/{id}/cancel` removes a `scheduled` run
before it starts.

```bash
curl "https://api.mos.getbrokerplus.ca/v1/automation-runs?automation_id=cmt4au7q90001p3iu2b8x1k7d&deal_id=cmscllt4h01xyp3iuq7dk2m8z" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

```json
{
  "success": true,
  "data": [
    {
      "object": "automation_run",
      "id": "cmt4c1v2e0007p3iu0d6k8q3x",
      "automation_id": "cmt4au7q90001p3iu2b8x1k7d",
      "automation_name": "Conditions removed",
      "status": "running",
      "subject_type": "deal",
      "subject_id": "cmscllt4h01xyp3iuq7dk2m8z",
      "owner_user_id": "cmsck2v8d0000p3iu5f9d8k1a",
      "fired_at": "2026-10-08T13:40:02.000Z",
      "created_at": "2026-10-08T13:40:02.000Z",
      "updated_at": "2026-10-08T13:40:05.000Z"
    }
  ],
  "pagination": { "total": 1, "page": 1, "page_size": 50, "total_pages": 1 }
}
```

`GET /v1/automation-runs/{id}/steps` lists what each step of a run did or will do, in the order they
fall due: `draft_id` for a draft it wrote, `communication_id` for a message it sent, `task_id` for a
task it made, and `reason` when a step was skipped, suppressed, failed, or needs the broker.

## See who a deal's mail comes from

`GET /v1/deals/{id}/sender` tells you who the client sees on mail about a deal, worked out exactly
as a real send works it out: from the broker assigned to the deal.

<CodeTabs syncKey="lang">

```bash title="curl"
curl "https://api.mos.getbrokerplus.ca/v1/deals/cmscllt4h01xyp3iuq7dk2m8z/sender" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

```ts title="TypeScript"
const res = await fetch("https://api.mos.getbrokerplus.ca/v1/deals/cmscllt4h01xyp3iuq7dk2m8z/sender", {
  headers: { Authorization: `Bearer ${process.env.BROKERPLUS_API_KEY}` },
});
const { data } = await res.json();
```

```python title="Python"
import os

import requests

res = requests.get(
    "https://api.mos.getbrokerplus.ca/v1/deals/cmscllt4h01xyp3iuq7dk2m8z/sender",
    headers={"Authorization": f"Bearer {os.environ['BROKERPLUS_API_KEY']}"},
)
data = res.json()["data"]
```

</CodeTabs>

```json
{
  "success": true,
  "data": {
    "deal_id": "cmscllt4h01xyp3iuq7dk2m8z",
    "sender_user_id": "cmsck2v8d0000p3iu5f9d8k1a",
    "sender_name": "Alex Broker",
    "assigned_broker_id": "cmsck2v8d0000p3iu5f9d8k1a",
    "assigned_broker_name": "Alex Broker",
    "mailbox": "alex@example.com",
    "team_id": "cmt49zz1v0000p3iub7d3e5qa",
    "team_name": "Example Mortgage Group",
    "brand": {
      "source": "team",
      "primary_color": "#1f3a5f",
      "secondary_color": "#c9a227",
      "logo_url": "https://cdn.example.com/logo.png"
    },
    "legal_name": "Example Mortgage Group Inc."
  }
}
```

- `sender_user_id` is the broker whose name and signature the client sees. A team member who does
  not send from their own mailbox sends as the team's broker.
- `mailbox` is the address the mail leaves from. `null` means no connected mailbox can send for this
  deal, so client email can't go out until one is connected.
- `brand` is the brand of the team that owns the deal. `source` is `team` when the team set its own
  colors or logo, and `brokerage` when it uses the brokerage brand.
- `legal_name` is the business name in the footer of marketing mail. It is the same for every team.

The API doesn't set the sender directly. To send a deal's mail as another broker, assign that broker
to the deal with `PATCH /v1/deals/{id}`. The team's brand is changed in BrokerPlus.
