---
title: "Edit many links at once from the Assistant, MCP or the API"
description: "Insert the same condition or step into many links' workflows and restyle their pages in one call each: one change for all links, per-link overrides, links named by id or short link, per-link results, the limit of 50 links a call, and what BULK_EDIT_NOT_STARTED means."
canonical: "https://linkbreakers.com/help/article/edit-many-links-at-once-from-the-assistant-mcp-or-api"
last-updated: 2026-11-05
---

# Edit many links at once from the Assistant, MCP or the API

> Insert the same condition or step into many links' workflows and restyle their pages in one call each: one change for all links, per-link overrides, links named by id or short link, per-link results, the limit of 50 links a call, and what BULK_EDIT_NOT_STARTED means.

## Short answer

Two calls change many links at once, instead of one call per link: `workflow_steps_bulk_insert` puts the same step, usually a condition, into each link's workflow, and `page_themes_bulk_update` gives each link's page the same look. Send a list of links and one change. Links that differ go in the same call, as overrides. Each call takes up to 50 links, each link succeeds or fails on its own, and a link that comes back with `BULK_EDIT_NOT_STARTED` was left untouched because the call ran out of time, so send it again in a new call.

## Quick summary

- The Assistant and the MCP server have both tools; over REST they are `POST /v1/workflow-steps/bulk-insert` and `POST /v1/page-themes/bulk-update`
- The simple form is a list of links plus one change; overrides are for the links that differ
- Name links by id or by short link
- The HTTP status is 200 when the call ran, so read `results`: there is one per link
- Up to 50 links a call; more is refused as a whole with `BULK_EDIT_TOO_LARGE`
- A call has 3 seconds, so it may finish only part of a batch: send the `BULK_EDIT_NOT_STARTED` links again
- Calling the insert again for a link that succeeded inserts a second step
- Each link that takes a step gets its canvas laid out automatically, which resets a canvas arranged by hand

## Where to use it

| Where | Insert a step | Restyle pages | Sign-in |
|---|---|---|---|
| The Assistant | Ask for the change in your own words | Ask for the change in your own words | Your Linkbreakers account |
| MCP server | `workflow_steps_bulk_insert` | `page_themes_bulk_update` | OAuth; see [MCP server setup](/help/article/mcp-server-setup) |
| REST API | `POST /v1/workflow-steps/bulk-insert` | `POST /v1/page-themes/bulk-update` | Secret key with `workflows:write` or `page_themes:write`; see [API token scopes](/help/article/api-token-scopes) |

The MCP server does not accept API tokens: it answers `401` with `API tokens are not accepted by the MCP server; connect with OAuth`.

The examples below use `curl` against `https://api.linkbreakers.com` with a secret key in `$LINKBREAKERS_TOKEN`. MCP tools take the same fields.

## Insert one step on every link

This sends iPhone and iPad visitors of three links to one page. Everyone else keeps going where the link went before.

```bash
curl -X POST https://api.linkbreakers.com/v1/workflow-steps/bulk-insert \
  -H "Authorization: Bearer $LINKBREAKERS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "links": ["5b061d3a-7b05-4e2f-bfd5-b9bd9e6e563a", "spring-menu", "https://go.acme.com/summer-sale"],
    "eventAction": "EVENT_ACTION_DEVICE_PLATFORM_CONDITION",
    "payload": {
      "devicePlatformCondition": {
        "options": [
          { "id": "ios", "name": "iPhone and iPad", "value": "DEVICE_PLATFORM_CONDITION_VALUE_IOS" }
        ]
      }
    },
    "branches": [
      { "branchId": "ios", "destination": "https://example.com/app" }
    ]
  }'
```

- The step goes right after each link's entry, in front of whatever the entry led to.
- `payload` is the same as when creating one step. Leave its next step ids out: `branches` does the wiring.
- `branches` lists only the branches that go somewhere new, each with its URL, and a redirect is created for it. A branch you do not list, `else` included, keeps going where the link went before.
- A branch id is the option's id, the country code on a country condition, the language code on a language condition, or `else` for everyone else.
- Conditions and interaction steps can be inserted. An exit or an entry step is refused with `INVALID_EVENT_ACTION`, because it would strand the steps after it.

## Set one page theme on every link

Send one entry in `changes`: `slug` for a template, `theme` for values, or both to adjust a template, in which case the values win.

```bash
curl -X POST https://api.linkbreakers.com/v1/page-themes/bulk-update \
  -H "Authorization: Bearer $LINKBREAKERS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "changes": [
      { "links": ["5b061d3a-7b05-4e2f-bfd5-b9bd9e6e563a", "spring-menu"], "slug": "midnight-glass" }
    ]
  }'
```

`GET /v1/page-theme-templates?scope=PAGE_THEME_TEMPLATE_SCOPE_AVAILABLE` lists the templates a workspace can use. A template without a slug is named with `pageThemeTemplateId`. See [what a page theme is](/help/article/whats-a-page-theme).

## Change what differs on some links

Overrides sit in the same call, so the simple case stays short.

**On a step**, `overrides` names a link and the branches it does differently. An override replaces only the branches it names. A link named only in `overrides` need not be repeated in `links`, and it counts toward the 50. An override can also set `insertBefore` to a step id of that link, to place the step there rather than right after the entry.

```json
{
  "links": ["spring-menu", "autumn-menu"],
  "eventAction": "EVENT_ACTION_COUNTRY_CONDITION",
  "payload": { "countryCondition": { "countries": [{ "countryCode": "FR" }] } },
  "branches": [{ "branchId": "FR", "destination": "https://example.com/fr" }],
  "overrides": [
    { "link": "autumn-menu", "branches": [{ "branchId": "FR", "destination": "https://example.com/fr/automne" }] }
  ]
}
```

**On a theme**, add an entry naming those links after the first one. A link named in a later entry gets that entry instead, as a whole: the two are not merged, so a value only the earlier entry sets is not applied to that link.

```json
{
  "changes": [
    { "links": ["spring-menu", "autumn-menu"], "slug": "midnight-glass" },
    { "links": ["autumn-menu"], "theme": { "canvasColor": { "type": "THEME_COLOR_TYPE_SOLID", "colors": ["#F5F0EB"] } } }
  ]
}
```

## Name links by id or short link

A link can be named by its id or by its short link, with or without the domain: `spring-menu`, `linkbreak.ing/spring-menu` or `https://linkbreak.ing/spring-menu`. Anything after `?` or `#` is ignored. A link named twice is edited once.

- **Capital letters:** a short link is matched as you write it, so a generated one such as `nTYmnJycM` keeps its capitals.
- **The same short link on several domains:** the bare name fails on that link with `LINK_REF_AMBIGUOUS`, and the message lists the addresses. Name it with its domain.
- **A link of another workspace:** it reads the same as one that does not exist, `LINK_NOT_FOUND`.

## Read the result of each link

The HTTP status says the call ran, not that every link changed. There is one result per link, in the order the request named them, plus two counts.

```json
{
  "results": [
    {
      "link": "5b061d3a-7b05-4e2f-bfd5-b9bd9e6e563a",
      "linkId": "5b061d3a-7b05-4e2f-bfd5-b9bd9e6e563a",
      "status": "BULK_EDIT_STATUS_SUCCEEDED",
      "errorCode": "",
      "errorMessage": "",
      "stepId": "92ccd43b-6cb9-4187-9e28-3c1f1450c7fb",
      "issues": []
    },
    {
      "link": "no-such-link",
      "linkId": "",
      "status": "BULK_EDIT_STATUS_FAILED",
      "errorCode": "LINK_NOT_FOUND",
      "errorMessage": "\"no-such-link\" is not a link of this workspace: name it by id or short link, as links_list shows them",
      "stepId": "",
      "issues": []
    }
  ],
  "succeededCount": 1,
  "failedCount": 1
}
```

- `link` is the name you sent; `linkId` is empty when that name matched no link.
- On the insert, `stepId` is the new step and `issues` is what workflow validation finds on the link now. On the theme call, `pageThemeId` is the link's page theme.
- A link that fails on the insert is left as it was: its new steps are removed and its redirect restored.
- `BULK_EDIT_LINK_FAILED` means an error on our side. That link can be retried.

**Only retry the links that failed.** Calling the insert again for a link that succeeded inserts a second step.

A few things refuse the whole call instead, with a `4xx` and nothing changed: naming no link (`BULK_EDIT_EMPTY`), naming more than 50 (`BULK_EDIT_TOO_LARGE`), or a step that cannot be inserted (`INVALID_EVENT_ACTION`).

## What BULK_EDIT_NOT_STARTED means

Every request has 3 seconds, and each call edits a few links at a time. When the time is nearly up, the links it has not reached are not started. They come back with `status` `BULK_EDIT_STATUS_FAILED`, `errorCode` `BULK_EDIT_NOT_STARTED`, and they count in `failedCount`.

Those links were not touched, so sending them again is safe. Put only those links in a new call, and repeat until none is left. The Assistant is told to do this without asking you.

How many links fit in a call depends on the change and on the load at that moment. On one test on a demo workspace on 2026-11-05, over REST:

- **Inserting a condition:** each call finished 4 links and took 2 to 3 seconds, so at that rate 11 links take three calls.
- **Setting one template:** one call finished all 11 links in about 1 second.

These come from one test, not a guarantee. Reading `results` and sending `BULK_EDIT_NOT_STARTED` links again works whatever the number is.

## What the insert does to an existing link

**The canvas is laid out again.** Each link that takes a step gets a full auto-layout, on the Assistant, MCP and REST alike. A canvas arranged by hand is reset. The tools that change one step at a time do not do this.

**The link's redirect and its analytics.** A link usually ends on one redirect, and what happens to it depends on the branches you list:

- **Some branches listed:** the redirect does not change. The `else` branch leads to it, so those visitors go where they went before, and the step keeps its analytics.
- **Every branch listed, `else` included, none going to the current URL:** the redirect would be unreachable, so it takes the URL of the `else` branch instead. It stays the same step with a new URL, so that step's analytics carry on under the new URL. If that history should stay tied to the old URL, leave `else` out.
- **A branch going to the current URL:** it reuses the existing redirect rather than creating a second one for the same URL.

## Limits and caveats

- Up to 50 distinct links a call, counting links named only in overrides
- A call has 3 seconds and may finish only part of its links; there is no background job for bulk edits
- Bulk deletes are not part of this: deleting stays one item at a time

## Frequently asked questions

### Do I need to look up each link's steps or page theme first?

No. Name the links by id or short link. The step goes right after each link's entry, and the call finds each link's page theme. You only need a link's steps to place a step elsewhere with `insertBefore`.

### What happens if I send the insert twice for the same links?

Each link that already succeeded gets a second step. Keep the `results` of the first call and send again only the links that failed.

### Is it safe to resend a link with BULK_EDIT_NOT_STARTED?

Yes. That link was not changed. Send it in a new call with the same change.

### Can I use an API token with the MCP server?

No. The MCP server signs in with OAuth and refuses API tokens. API tokens work with the REST API, the SDKs and the CLI.

### What should I do with more than 50 links?

Split them over several calls of at most 50 links each, and read the results of each call.

## Last reviewed

2026-11-05
