> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gtm-api.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Run a mass action

> Preview a bulk plan, commit it with the consent token, then monitor the run to settlement.

A mass action is a bulk dispatch on the Orchestration service: up to 100 items, each run through a plan of 1 to 3 steps, paced by a randomized schedule. The API forces a two-phase flow so nothing bulk ever fires from a single call: first a preview that validates everything and mints a consent token, then a commit that consumes it.

<Tip>
  **Give this to your AI agent** and keep the go-ahead for yourself:

  "On gtm-api (MCP connector at [https://mcp.gtm-api.com/mcp](https://mcp.gtm-api.com/mcp), or REST at app.gtm-api.com with my key): build a mass-action plan for the targets I give you, run the preview, and show me the findings and the consent summary. Wait for my explicit approval before committing. After the commit, monitor the run and report per-item outcomes when it settles."
</Tip>

## 1. Preview the plan

`POST /api/mass-actions/preview` validates the whole plan without persisting, charging or sending anything, and reports every finding at once.

```json POST /orchestration/v4/api/mass-actions/preview theme={null}
{
  "title": "July outreach, batch 3",
  "target_entity": "linkedin-connection-requests",
  "scope": {
    "kind": "targets",
    "targets": [
      { "ln_id": "ACoAAAdbzRUBAAAAAAAAAAAAAAAAAAAAAAAAAAA" }
    ]
  },
  "plan": {
    "steps": [
      {
        "tool": "linkedin-connection-requests.send-linkedin-connection-request",
        "args": {
          "linkedin_account_sid": "ln_ac_Hx7kQ3mN2pL4",
          "note": "Hi, enjoyed your post on pipeline reviews."
        }
      }
    ]
  },
  "schedule": { "interval_seconds_min": 120, "interval_seconds_max": 600 },
  "canary_mode": "first_item"
}
```

What the validator enforces:

* **Step vocabulary.** Only step-eligible verbs can appear in `plan.steps[].tool`. Anything else fails with `validation_failed` and a `field_errors` entry naming the authorable set, so the plan is repairable in one pass.
* **One sender per run.** A send-class step names the account it sends from in `args.linkedin_account_sid`. A target's payload carries the target (`ln_id`, the profile URN) and its per-send extras (`note`, `client_reference`), never the sender: a plan that leaves the sender out of `args` fails preview with `sender_required`.
* **Scope shape.** `objects` (existing rows by sid), `targets` (per-item payload identities), `generate` (N slots, step 1 must create the object), or `none` (a standing run an auto-scrape feeds later). All bounded at 100 items.
* **A target is one person.** Name them by `ln_id` (the profile URN, `ACoAA...`) or `sn_id` (the Sales Navigator id, `ACwAA...`); `profile_id` takes either. `ln_member_id` is the decoded member number (digits only) and cannot be dispatched by a send verb on its own; a vanity slug goes in `nickname`. A slug in `ln_member_id`, an id that does not decode, or a Recruiter id where a member verb wants a profile URN fails the preview on that target (`scope.targets.3.ln_member_id`, `ln_member_id_not_numeric`), and the same person named twice (their URN and their Sales Navigator id) counts once.
* **Schedule mandate.** A plan with a send-class step must carry a `schedule`; the per-gap interval is randomized between your min and max, because a fixed cadence is itself a detectable pattern. Omit `schedule` only for plans that can drain immediately.

On success the `action` envelope returns a `preview` block (`items_count`, `dangerous_steps`, `eta`, warnings) plus `commit_token` and `expires_at`.

## 2. Commit it

`POST /api/mass-actions` takes the exact same inputs plus the `commit_token`. The token is an HMAC over those inputs and the caller: edit anything, and the commit fails with `validation_failed`; wait past 15 minutes, and it expires. Re-preview in either case.

Two behaviors to design around:

* **The run is always asynchronous**, even for one item. The returned `sid` is your monitoring handle.
* **A still-valid token can be replayed**, and a replay creates a second identical run. Discard the token the moment the commit succeeds.

With `canary_mode: "first_item"`, only item 1 dispatches until it succeeds. A canary failure pauses the whole run with `paused_reason: canary_failed`, so a broken template costs you one send, not a hundred.

## 3. Monitor to settlement

Runs are visible in the app whoever authored them: yourself, an API key, an agent over
MCP, or the platform. **Mass Actions** lists them with their pacing, live counts and
state.

<Frame caption="The Mass Actions list: one row per run, with items done against total, the randomized pacing window, the derived state and who authored it.">
  <img src="https://mintcdn.com/getsalesio/-R0KXJStre05Nt_b/images/kb/mass-actions-list.png?fit=max&auto=format&n=-R0KXJStre05Nt_b&q=85&s=d1633ef86914473245d2ad405a013213" alt="Mass Actions list showing runs with item counts, pacing intervals, states and the actor that created each" width="1980" height="1600" data-path="images/kb/mass-actions-list.png" />
</Frame>

Opening a run shows the plan it plays and every item's outcome, which is the fastest way
to tell a broken template from a transient failure.

<Frame caption="A run: the read-only step plan on top, then per-item rows with status, error and attempt count.">
  <img src="https://mintcdn.com/getsalesio/-R0KXJStre05Nt_b/images/kb/mass-action-drawer.png?fit=max&auto=format&n=-R0KXJStre05Nt_b&q=85&s=3183f74052d421bd49adac44c4e9f173" alt="Mass action run detail showing a two-step plan and a per-item table with succeeded and failed rows, error text and try counts" width="1400" height="1600" data-path="images/kb/mass-action-drawer.png" />
</Frame>

Over the API:

* `GET /api/mass-actions/{sid}?include[]=metrics` returns the run with per-status item counts.
* `POST /api/mass-action-items/search` with `filter: { "mass_action_sid": { "eq": "..." } }` lists the individual items and their step logs.
* Or skip polling: subscribe a [webhook](/guides/receive-webhooks) to the `mass-actions.settled` and `mass-actions.paused` events.

Control verbs while it runs: `POST /api/mass-actions/{sid}/pause` and `/resume`, and `/release` for a standing run.

## 4. Retry failures

`POST /api/mass-action-items/retry` re-enters failed items at their current step. Completed steps are never re-executed, so there are no duplicate creates and no double sends. Target exactly one of a single item `sid` or a `filter` (pass `mass_action_sid` in the filter unless you mean every run). Only `status: failed` rows match, and a retry matching nothing returns `retried_count: 0`, not an error.

Two guards hold whatever you pass. An item is retried at most 10 times, and an item of a deleted run, or a cancelled one, is never retried. By `sid` these answer `422 retry_limit_exceeded` and `409 cancelled_cannot_retry`. A filter leaves such items alone and reports the first kind in `left_at_retry_limit_count`.

A send is not sent again on a guess. Each send of a run (a message, a connection request, a comment) goes out under a key of its own ([Sending once](/concepts/sending-once) explains the keys), and a send whose answer was lost waits with `wait_reason: send_outcome_unknown` while the run asks the owning service what came of it. It goes again only when the answer is `not_sent`. If nobody can tell within 24 hours, the item fails with `send_unconfirmed:` in its `error_message`, and a plain retry asks again for another day. An `item_timeout:` failure on a send step is asked about the same way. To send it anyway, look on LinkedIn first: the conversation, the person's invitations, the post's comments. If it is not there, retry that one item with your word:

```json POST /orchestration/v4/api/mass-action-items/retry theme={null}
{
  "sid": "ma_im_YOUR_ITEM",
  "resend_unverified": true
}
```

The step goes again naming the attempt in doubt, and the owning service still holds it while that attempt could land. `resend_unverified` vouches for one send, so it takes a `sid` and never a `filter` (`422 resend_unverified_needs_sid`). A comment carries a key since 2026-10-05 like every other send. A send step that began before its key existed, such as a comment of a run that was already going, cannot be asked about: a plain retry leaves it failed with a note and counts it in `left_unconfirmed_count`, and only `resend_unverified` sends it again.

On a step that is not a send, an `item_timeout:` failure still means the outbound call may have landed: check the target before retrying a step that is not safe to repeat.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.