Skip to main content
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.
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, 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.”

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.
POST /orchestration/v4/api/mass-actions/preview
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 step that acts as one of the team’s accounts names it in its args: linkedin_account_sid for a connect, message, follow, react or comment step, email_account_sid for an email step. 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.
  • One text per run. A message or a comment step carries the text every target gets in args.text; a plan without it fails preview with text_required instead of dying on its first item.
  • The sender can send right now. Before a run is created, the preview asks the LinkedIn service about every sender the plan names: an account still in its first sync (initial_sync_pending), lent to another team (shared_out), parked by the subscription (subscription_required), signed out of LinkedIn (signed_out), restricted by LinkedIn (restricted), parked for an error investigation (error_investigation), or not connected in this workspace (not_found) fails preview with sender_not_ready on plan.steps.N.args.linkedin_account_sid, and the message says what to do (wait for linkedin-accounts.initial-sync-done, sign in again, recall the share). The create asks again, so a sender that changed state between preview and commit is refused at the commit. Until 2026-10-23 such a run was created and its first item failed on the canary; a run created minutes after connecting its account was the usual case.
  • 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. Or name them by their vanity slug in nickname (a linkedin.com/in/... link works too): the send step resolves it to the URN when it runs, for free when the team already knows the person (a connection, an invitation, a follower, a lookup of the last day) or the corpus holds their profile from the last 30 days, otherwise with a lite-profile read on the sending account, which spends that account’s enrichment budget and waits for the budget to reset when it is spent. ln_member_id is the decoded member number (digits only), and no send step can dispatch on it alone. 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, or their slug and its link) counts once.
  • Every target is reachable. A connect, message or follow step needs a URN or a slug in each target; a message also takes an existing thread (linkedin_conversation_sid). A target with a member number alone, a company id or no person at all fails the preview with target_identity_required on scope.targets.N, a company page or a Recruiter id in nickname fails it with public_identifier_not_resolvable, and the same holds for a lead appended to a running run (identities.N).
  • 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. The run then waits for your call, one of three:
  • Fix what the item needed and retry it by its sid (POST /api/mass-action-items/retry), then POST /api/mass-actions/{sid}/resume: the retried canary goes alone, and its success opens the gate for the rest.
  • Accept the failure: POST /api/mass-actions/{sid}/release opens the gate by hand, then /resume.
  • Delete the run and start over with a corrected plan (plans are immutable).
A canary run nobody decides on within 7 days settles itself: its pending items are cancelled and mass-actions.settled fires.

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.
Mass Actions list showing runs with item counts, pacing intervals, states and the actor that created each

The Mass Actions list: one row per run, with items done against total, the randomized pacing window, the derived state and who authored it.

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.
Mass action run detail showing a two-step plan and a per-item table with succeeded and failed rows, error text and try counts

A run: the read-only step plan on top, then per-item rows with status, error and attempt count.

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 to the mass-actions.settled and mass-actions.paused events.
  • POST /api/mass-actions/search lists the runs; filter.title (eq, in) finds one by the label you gave it, and filter.q searches the titles.
A spent daily budget pauses the run, not each item. When the sending account’s smart limit is spent, or LinkedIn refuses for the day, the run pauses with paused_reason: limit_reached and hold_till at the moment the budget comes back: the next midnight in the account’s own timezone (its sync_config.timezone, UTC when it keeps none). The scheduler resumes the run then and re-plans the remaining items onto a fresh pacing chain from that moment, so they neither hammer the limit nor all go out at the reset in one burst. mass-actions.paused and mass-actions.resumed fire on the way. Resuming by hand before hold_till only parks the run again at the first item the budget refuses; pausing by hand replaces the hold and waits for your resume. Control verbs while it runs: POST /api/mass-actions/{sid}/pause and /resume, /release to open a closed canary gate by hand, and /append-items to feed more leads into a run that is going. A run held for a spent limit still takes them; a run paused for any other reason, or deleted, answers 409 run_not_active with context.reason_detail saying which and context.detail what to do.

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 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:
POST /orchestration/v4/api/mass-action-items/retry
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.