# Labels

Labels are the tags your team puts on creators in the Archive app. The API works on the same taxonomy: a label you create here shows up in the app, and a label applied in the app is listed here. Labels have no id in this API. **A label is its name.**

## Label names are the identity

Every label argument takes a name, and matching ignores case and surrounding whitespace: `vip`, `VIP` and `Vip` all name the same label. The API never stores a second spelling of a label that already exists. It returns the spelling the workspace stored, the one someone typed first, and you should pass that spelling back as it is.

A label with `deleting: true` is being removed by a workspace-wide deletion. Do not assign it, and do not create it again until it disappears from the list.

## List labels

[`labels`](/api/v2/docs/queries/labels) returns every label in the workspace, sorted by name ignoring case. The list is not paginated and holds at most 1,000 labels.

```graphql
query LabelsDefault {
  labels {
    name
    deleting
  }
}
```

```http
Authorization: Bearer docs_demo_token_0000000000000000000000
WORKSPACE-ID: 6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a
```

```json
{
  "data": {
    "labels": [
      {
        "name": "Gifting",
        "deleting": false
      },
      {
        "name": "Plant Parents",
        "deleting": false
      },
      {
        "name": "Retired 2024",
        "deleting": false
      },
      {
        "name": "VIP",
        "deleting": false
      }
    ]
  }
}
```

## Create labels

[`createLabels`](/api/v2/docs/mutations/create-labels) adds up to 50 names per call, each at most 100 characters. A name the workspace already has is not an error: it comes back with its stored spelling, so repeating the call is safe. In the example `vip` resolves to the stored `VIP`, and only `Holiday 2025` is new.

```graphql
mutation CreateLabelsDefault($labelNames: [String!]!) {
  createLabels(labelNames: $labelNames) {
    labels {
      name
      deleting
    }
    userErrors {
      field
      message
    }
  }
}
```

```json
{
  "labelNames": [
    "Holiday 2025",
    "vip"
  ]
}
```

```http
Authorization: Bearer docs_demo_token_0000000000000000000000
WORKSPACE-ID: 6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a
```

```json
{
  "data": {
    "createLabels": {
      "labels": [
        {
          "name": "Holiday 2025",
          "deleting": false
        },
        {
          "name": "VIP",
          "deleting": false
        }
      ],
      "userErrors": []
    }
  }
}
```

Creating a label does not put it on anyone. A blank name, an over-long name or a name that is being deleted refuses the whole call with a `userErrors` entry on `["labelNames"]`, and nothing is created.

## Assign and remove

[`addLabelsToCreators`](/api/v2/docs/mutations/add-labels-to-creators) and [`removeLabelsFromCreators`](/api/v2/docs/mutations/remove-labels-from-creators) take up to 40 creator ids and up to 50 label names, and write synchronously. Both return the creators as stored. For more than 40 creators, use the [bulk mutations](#assign-and-remove-in-bulk).

```graphql
mutation AddLabelsToCreatorsDefault($creatorIds: [ID!]!, $labelNames: [String!]!) {
  addLabelsToCreators(creatorIds: $creatorIds, labelNames: $labelNames) {
    creators {
      id
      customAttributes
    }
    userErrors {
      field
      message
    }
  }
}
```

```json
{
  "creatorIds": [
    "7750875f-6037-5751-a493-86ac5fc3e9c9",
    "75fd5375-7f33-5331-a6cf-86e0c697c6f6"
  ],
  "labelNames": [
    "Gifting"
  ]
}
```

```http
Authorization: Bearer docs_demo_token_0000000000000000000000
WORKSPACE-ID: 6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a
```

```json
{
  "data": {
    "addLabelsToCreators": {
      "creators": [
        {
          "id": "75fd5375-7f33-5331-a6cf-86e0c697c6f6",
          "customAttributes": {
            "age": null,
            "emails": [],
            "gender": "45b18b20-e86d-5c85-831c-19ada8104b25",
            "labels": [
              "Gifting"
            ],
            "location": "United States",
            "full_name": null,
            "phone_numbers": []
          }
        },
        {
          "id": "7750875f-6037-5751-a493-86ac5fc3e9c9",
          "customAttributes": {
            "age": null,
            "emails": [],
            "gender": "52e6750a-79ca-5f7e-9352-228c7b9cb18f",
            "labels": [
              "Gifting"
            ],
            "location": "United States",
            "full_name": null,
            "phone_numbers": []
          }
        }
      ],
      "userErrors": []
    }
  }
}
```

- Labels are set membership. Assigning a label a creator already carries, or removing one it does not carry, succeeds and changes nothing, so both calls are safe to repeat.
- **Assigning creates a missing label.** Check `labels` first when you mean an existing label: a typo adds a new one. Names you assign are capped at 100 characters.
- If any creator id is not a creator of this workspace, the whole call is refused with a `userErrors` entry on `["creatorIds"]` naming it, and nothing is written.
- Removing a label leaves the label itself in the workspace. Names you remove have no length cap, so any existing label can be removed.

```graphql
mutation RemoveLabelsFromCreatorsDefault($creatorIds: [ID!]!, $labelNames: [String!]!) {
  removeLabelsFromCreators(creatorIds: $creatorIds, labelNames: $labelNames) {
    creators {
      id
      customAttributes
    }
    userErrors {
      field
      message
    }
  }
}
```

```json
{
  "creatorIds": [
    "5e6f95ca-070b-5cef-8e0c-08d855365419"
  ],
  "labelNames": [
    "vip"
  ]
}
```

```http
Authorization: Bearer docs_demo_token_0000000000000000000000
WORKSPACE-ID: 6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a
```

```json
{
  "data": {
    "removeLabelsFromCreators": {
      "creators": [
        {
          "id": "5e6f95ca-070b-5cef-8e0c-08d855365419",
          "customAttributes": {
            "age": null,
            "emails": [],
            "gender": "45b18b20-e86d-5c85-831c-19ada8104b25",
            "labels": [],
            "location": "United States",
            "full_name": null,
            "phone_numbers": []
          }
        }
      ],
      "userErrors": []
    }
  }
}
```

To verify an assignment, query [`creator(id:)`](/api/v2/docs/queries/creator) with `customAttributes` and read its `labels` key. `customAttributes` is a JSON value, so `labels` is a key inside it, not a field you can select.

### What a synchronous call costs

A synchronous assignment is priced per creator: **50 points for the first creator plus 100 for each further one**, so 40 creators cost 3,950 points plus the payload fields you select. A call that costs more than your plan’s whole [rate-limit](/api/v2/docs/guides/rate-limits) capacity is refused with a `userErrors` entry on `["creatorIds"]` naming the largest call your plan allows, and nothing is written or charged.

Synchronous label writes also share a per-workspace label-write limit: a workspace labels at most **40 creators at once, then one more creator every 2 seconds**, across every caller and both entry points. Past it the call fails with HTTP 429, `RateLimit-Policy: "label_writes";q=40;w=80` and a `Retry-After` header. Wait that long, then retry.

## Assign and remove in bulk

[`bulkAddLabelsToCreators`](/api/v2/docs/mutations/bulk-add-labels-to-creators) and [`bulkRemoveLabelsFromCreators`](/api/v2/docs/mutations/bulk-remove-labels-from-creators) take up to 1,000 creator ids and the same label names, and do the work in the background. They cost a **flat 250 points** whatever the number of creators and do not draw from the label-write limit. Pick by size:

| Creators        | Use                                                        | Price              |
| --------------- | ---------------------------------------------------------- | ------------------ |
| 1 to 40         | `addLabelsToCreators` / `removeLabelsFromCreators`         | 50 + 100 × (n − 1) |
| 41 to 1,000     | `bulkAddLabelsToCreators` / `bulkRemoveLabelsFromCreators` | 250                |
| more than 1,000 | several bulk calls of up to 1,000 each                     | 250 per call       |

```graphql
mutation BulkAddLabelsToCreatorsDefault($creatorIds: [ID!]!, $labelNames: [String!]!) {
  bulkAddLabelsToCreators(creatorIds: $creatorIds, labelNames: $labelNames) {
    scheduled
    operationId
    acceptedCount
    skippedCreatorIds
    userErrors {
      field
      message
    }
  }
}
```

```json
{
  "creatorIds": [
    "7750875f-6037-5751-a493-86ac5fc3e9c9",
    "75fd5375-7f33-5331-a6cf-86e0c697c6f6"
  ],
  "labelNames": [
    "Plant Parents"
  ]
}
```

```http
Authorization: Bearer docs_demo_token_0000000000000000000000
WORKSPACE-ID: 6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a
```

```json
{
  "data": {
    "bulkAddLabelsToCreators": {
      "scheduled": true,
      "operationId": "bb437896-b6cb-5c2e-9d2b-a838b77bf025",
      "acceptedCount": 2,
      "skippedCreatorIds": [],
      "userErrors": []
    }
  }
}
```

- An id that is not a creator of this workspace does not refuse the call. It is returned in `skippedCreatorIds`, and the rest are scheduled; `acceptedCount` is the number of creators scheduled. When no id resolves, nothing is scheduled and `scheduled` is `false`.
- At most **2** label bulk operations can be queued or processing in a workspace at once, counting bulk label edits made in the Archive app. A third call is refused with a `userErrors` entry on `["base"]` that names the operations in flight. Wait for one of them to finish, then retry.
- The same set-membership rules apply: repeating a call is safe, assigning creates a missing label, and removing leaves the label in the workspace.

```graphql
mutation BulkRemoveLabelsFromCreatorsDefault($creatorIds: [ID!]!, $labelNames: [String!]!) {
  bulkRemoveLabelsFromCreators(creatorIds: $creatorIds, labelNames: $labelNames) {
    scheduled
    operationId
    acceptedCount
    skippedCreatorIds
    userErrors {
      field
      message
    }
  }
}
```

```json
{
  "creatorIds": [
    "5e6f95ca-070b-5cef-8e0c-08d855365419"
  ],
  "labelNames": [
    "vip"
  ]
}
```

```http
Authorization: Bearer docs_demo_token_0000000000000000000000
WORKSPACE-ID: 6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a
```

```json
{
  "data": {
    "bulkRemoveLabelsFromCreators": {
      "scheduled": true,
      "operationId": "05034d30-4102-538c-b292-a592d0b9feda",
      "acceptedCount": 1,
      "skippedCreatorIds": [],
      "userErrors": []
    }
  }
}
```

`scheduled: true` comes with the `operationId` of the run. Poll [`operation(id:)`](/api/v2/docs/queries/operation) until `status` is `COMPLETED`, `PARTIAL` or `FAILED`, read any per-creator failures with [`operationRecords`](/api/v2/docs/queries/operation-records), then spot-check a creator with `creator(id:)`. Bulk label operations appear in [`operations`](/api/v2/docs/queries/operations) with `operationType` `update_custom_attributes` (add) or `remove_custom_attributes` (remove), carry one record per creator, and send no `operation.completed` webhook.

## Rename a label

[`renameLabel`](/api/v2/docs/mutations/rename-label) renames one label across the whole workspace: every creator carrying it follows the new spelling. The rename runs in the background. `scheduled: true` comes with the `operationId` of the run; nothing has changed yet when the call returns.

```graphql
mutation RenameLabelDefault($labelName: String!, $newLabelName: String!) {
  renameLabel(labelName: $labelName, newLabelName: $newLabelName) {
    scheduled
    operationId
    userErrors {
      field
      message
    }
  }
}
```

```json
{
  "labelName": "Plant Parents",
  "newLabelName": "Plant Lovers"
}
```

```http
Authorization: Bearer docs_demo_token_0000000000000000000000
WORKSPACE-ID: 6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a
```

```json
{
  "data": {
    "renameLabel": {
      "scheduled": true,
      "operationId": "fb352d7d-aafd-544a-93f8-eed5bf50ad39",
      "userErrors": []
    }
  }
}
```

The new name is at most 100 characters. Renaming onto a **different** label that already exists is refused, as there is no implicit merge (use [`mergeLabels`](#merge-two-labels) for that); a new spelling of the same label (`VIP` to `vip`) is allowed. Each refusal is a `userErrors` entry with `scheduled: false`, on the input at fault:

- `["labelName"]` — the label does not exist, is being deleted, is stored under several spellings, or is already being renamed (wait until the first rename shows in `labels`, then retry); or the workspace’s labels are busy with another change (retry shortly).
- `["newLabelName"]` — the new name is blank, too long, being deleted, or the name of another label.
- `["base"]` — the workspace has no user to attribute the rename to.

> **Renaming can change what campaign automations match**
>
> Campaign automations compare label text case-sensitively. An automation that matched the old spelling may stop matching after the rename, even after a change of case only. Confirm a rename with whoever owns those automations first.

## Merge two labels

[`mergeLabels`](/api/v2/docs/mutations/merge-labels) folds one label into another existing label across the whole workspace: every creator carrying `labelName` ends up carrying `intoLabelName`, and `labelName` is deleted. A creator that already carried both keeps one. The merge runs in the background; `scheduled: true` comes with the `operationId` of the run.

```graphql
mutation MergeLabelsDefault($labelName: String!, $intoLabelName: String!) {
  mergeLabels(labelName: $labelName, intoLabelName: $intoLabelName) {
    scheduled
    operationId
    userErrors {
      field
      message
    }
  }
}
```

```json
{
  "labelName": "Retired 2024",
  "intoLabelName": "Gifting"
}
```

```http
Authorization: Bearer docs_demo_token_0000000000000000000000
WORKSPACE-ID: 6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a
```

```json
{
  "data": {
    "mergeLabels": {
      "scheduled": true,
      "operationId": "d0e2a7ec-c713-539e-bcc7-ae6fe33da461",
      "userErrors": []
    }
  }
}
```

**A merge is irreversible.** Afterwards nothing tells the two labels’ creators apart, and the merged label is gone. One call merges one label into one target. Each refusal is a `userErrors` entry with `scheduled: false`, on the input at fault:

- `["labelName"]` — the label does not exist, is being deleted, is stored under several spellings, or is already being merged (wait until it disappears from `labels`, then retry); or the workspace’s labels are busy with another change (retry shortly).
- `["intoLabelName"]` — the target is blank, does not exist (a merge never creates a label), is being deleted, is stored under several spellings, or is the same label as `labelName` in any spelling (use `renameLabel` to respell a label).
- `["base"]` — the workspace has no user to attribute the merge to.

> **Merging changes what campaign automations match**
>
> Automations that matched the merged label stop matching its creators, and automations that match the target start matching them. Confirm a merge, with both names, with whoever owns those automations first.

## Delete a label

[`deleteLabel`](/api/v2/docs/mutations/delete-label) permanently removes one label from the workspace and from every creator that carries it. **The creators are kept.** Deletion is irreversible and runs in the background.

```graphql
mutation DeleteLabelDefault($labelName: String!) {
  deleteLabel(labelName: $labelName) {
    scheduled
    operationId
    labelName
    userErrors {
      field
      message
    }
  }
}
```

```json
{
  "labelName": "Retired 2024"
}
```

```http
Authorization: Bearer docs_demo_token_0000000000000000000000
WORKSPACE-ID: 6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a
```

```json
{
  "data": {
    "deleteLabel": {
      "scheduled": true,
      "operationId": "d6d1a270-cb9a-57fc-8c4c-e101c51e79d5",
      "labelName": "Retired 2024",
      "userErrors": []
    }
  }
}
```

- The label name has no length cap, so a label of any length can be deleted.
- Deleting a label that is already being deleted returns `scheduled: true` with the operation already running. Every caller converges on one deletion per label.
- A name that matches no label is a `userErrors` entry on `["labelName"]` with `scheduled: false`. The check is best effort: a deletion that another caller starts at the same moment can still answer `scheduled: true`.
- `labelName` in the payload echoes the name you sent, not the stored spelling.

Until the deletion finishes the label stays in `labels`, showing `deleting: true` once the background run has marked it, and it may still appear on creators and in search.

## Follow a delete, rename or merge

Delete, rename and merge return an `operationId`. Poll [`operation(id:)`](/api/v2/docs/queries/operation) until `status` is `COMPLETED` or `FAILED`, then confirm with `labels`: after a rename the new spelling is listed and the old one is gone; after a merge or a delete the source label is gone.

A label operation reports progress through `status`, `total` and `processed` only. Its record counts are always 0 and its item-id lists always empty; that is not a failure. Label operations, including ones started in the app, appear in [`operations`](/api/v2/docs/queries/operations) with `operationType` `delete_label`, `rename_label` or `merge_labels`. They send no `operation.completed` webhook.

## Busy and retry

A label write that collides with another write to the same labels is not a `userErrors` entry. It fails with a GraphQL error carrying `extensions.code` `SERVICE_UNAVAILABLE`, and your creators are unchanged. A delete whose background run could not be queued fails the same way, but its operation already exists. Retry after a short wait; a retried delete resumes that same deletion. See [errors](/api/v2/docs/guides/errors#error-codes).

## Attribution

Assignments, removals (synchronous or bulk), renames, merges and deletions are recorded as made by a person in your workspace:

1. the user who authorized the OAuth token, if they are a member of the workspace;
2. otherwise the person who issued the API token, if they are a member;
3. otherwise the workspace’s creator, if they are still a member and not an Archive employee;
4. otherwise the workspace’s earliest-joined member who is not an Archive employee.

If none of them applies, the request is refused with a `userErrors` entry on `["base"]`. Creating a label needs no user.

## From an MCP client

The nine MCP tools take the same arguments and return the same payloads: [`getLabels`](/api/v2/docs/mcp/read-tools#get-labels), [`createLabels`](/api/v2/docs/mcp/write-tools#create-labels), [`addLabelsToCreators`](/api/v2/docs/mcp/write-tools#add-labels-to-creators), [`removeLabelsFromCreators`](/api/v2/docs/mcp/write-tools#remove-labels-from-creators), [`bulkAddLabelsToCreators`](/api/v2/docs/mcp/write-tools#bulk-add-labels-to-creators), [`bulkRemoveLabelsFromCreators`](/api/v2/docs/mcp/write-tools#bulk-remove-labels-from-creators), [`renameLabel`](/api/v2/docs/mcp/write-tools#rename-label), [`mergeLabels`](/api/v2/docs/mcp/write-tools#merge-labels) and [`deleteLabel`](/api/v2/docs/mcp/write-tools#delete-label). They charge the same prices and have the same limits as the GraphQL mutations. `renameLabel` and `mergeLabels` do not draw from the label-write limit. An agent should confirm with the user before it calls `deleteLabel` (echoing the label name), `renameLabel` (echoing both names and the automation caveat) or `mergeLabels` (echoing both names, the irreversibility and the automation caveat), and treat `scheduled: true` as queued, not done. A busy collision is an `isError` result to retry.
