---
title: Tag
kind: resource
slug: tag
summary: "A free-form label attached to a Droplet. Stored as a string in `App.tags`; anyone with edit permission can add or remove one. The curated counterpart with structured field schemas is the [Type](/docs/resources/type) resource."
---

# Tag

A free-form label attached to a Droplet. Stored as a string in `App.tags`; anyone with edit permission can add or remove one. The curated counterpart with structured field schemas is the [Type](/docs/resources/type) resource.

Tags are how Droplets find each other. Any string in a Droplet's [Droplet.tags](/docs/resources/droplet) array is a tag — there is no per-row Tag model. Most tags are free-form labels (`onboarding`, `q3-2026`, `customer:acme`); a small curated subset are [Types](/docs/resources/type), which carry a schema of structured fields. The two share the same column in storage but are surfaced as separate resources in the API: `GET /api/tags` aggregates free-form usage for discovery, `GET /api/types` returns the registry. See the [Types & tags concept doc](/docs/types) for the model.

## The Tag object

### Attributes

- **`name`** `string` — The tag string as stored in `App.tags`. Lowercase, trimmed, deduped on write. Maximum 50 chars per tag, 20 tags per Droplet.
- **`usageCount`** `number` — How many Droplets currently wear this tag (across all Spaces — not ACL-scoped).

### JSON example

```json
{
  "name": "onboarding",
  "usageCount": 14
}
```

## Methods

### List tags in use

`GET /api/tags`

Aggregate of free-form tag names currently attached to at least one Droplet, sorted by usage count descending. Excludes Type names (registry entries) — those live at `GET /api/types`. **Restricted to `@drop-library-admins`** — the aggregate is unconditioned by per-Droplet ACL, so it could surface tag names from PRIVATE / SPECIFIC droplets to viewers who can't read them; the curator gate keeps that contained. For per-viewer typeahead, use `GET /api/tags/autocomplete` (ACL-scoped).

**Parameters**

- **`limit`** _(number · query)_ — Max rows (default 100, max 500).

**CLI**

```bash
drop tags list
```

**MCP**

```json
{
  "tool": "drop_tags_list",
  "arguments": {}
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/tags' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "tags": [
    {
      "name": "onboarding",
      "usageCount": 14
    },
    {
      "name": "q3-2026",
      "usageCount": 9
    },
    {
      "name": "customer:acme",
      "usageCount": 4
    }
  ]
}
```

### Create a Tag

`POST /api/tags`

Mint a new Tag in a given scope. `USER` tags are private to the caller; `SPACE` tags require `spaceSlug` and are visible to the Space's members; `GLOBAL` tags are visible to the whole org and restricted to taxonomers (the route enforces). The same name can exist independently across scopes — `customer-acme` as a USER tag does not collide with `customer-acme` at SPACE level.

**Parameters**

- **`name`** _(string · body · required)_ — The Tag name (slug-style).
- **`scope`** _(string · body)_ — USER | SPACE | GLOBAL (default USER).
- **`spaceSlug`** _(string · body)_ — Required when scope=SPACE.
- **`description`** _(string · body)_ — Optional summary (≤500 chars).

**CLI**

```bash
drop tags create --name customer-acme --scope SPACE --space-slug platform --description "Acme account workspace."
```

**MCP**

```json
{
  "tool": "drop_tags_create",
  "arguments": {
    "name": "customer-acme",
    "scope": "SPACE",
    "spaceSlug": "platform",
    "description": "Acme account workspace."
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/tags' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"customer-acme","scope":"SPACE","spaceSlug":"platform","description":"Acme account workspace."}'
```

**Response** (201)

```json
{
  "id": "tg_01HZX9...",
  "name": "customer-acme",
  "scope": "SPACE",
  "ownerId": "space_platform",
  "description": "Acme account workspace.",
  "createdById": "usr_jonathan",
  "createdAt": "2026-06-10T18:00:00.000Z",
  "updatedAt": "2026-06-10T18:00:00.000Z",
  "promotedFromId": null
}
```

### Retrieve a Tag (shadow-aware)

`GET /api/tags/{name}`

Resolve a Tag name to the viewer's active Tag plus any shadowed entries at other scopes. Without an explicit `scope`, the route picks USER > SPACE > GLOBAL precedence (the same precedence the chip rail uses when wearing a name). With an explicit `scope`, the result is constrained to that scope and `shadows` is empty. Useful for explaining "you typed `priority` — there's your USER `priority` AND the GLOBAL `priority`; which did you mean?" in the editor.

**Parameters**

- **`name`** _(string · path · required)_ — The Tag name.
- **`scope`** _(string · query)_ — Constrain resolution to one scope.
- **`spaceSlug`** _(string · query)_ — Disambiguate SPACE scope.

**CLI**

```bash
drop tags get priority
```

**MCP**

```json
{
  "tool": "drop_tags_get",
  "arguments": {
    "name": "priority"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/tags/priority' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "resolved": {
    "id": "tg_01HZUSR...",
    "name": "priority",
    "scope": "USER",
    "ownerId": "usr_jonathan",
    "description": null,
    "createdById": "usr_jonathan",
    "createdAt": "2026-06-09T12:00:00.000Z",
    "updatedAt": "2026-06-09T12:00:00.000Z",
    "promotedFromId": null
  },
  "shadows": [
    {
      "id": "tg_01HZGLOB...",
      "name": "priority",
      "scope": "GLOBAL",
      "ownerId": null,
      "description": "Org-wide priority signal.",
      "createdById": "usr_taxonomer",
      "createdAt": "2026-05-01T00:00:00.000Z",
      "updatedAt": "2026-05-01T00:00:00.000Z",
      "promotedFromId": null
    }
  ]
}
```

### Update a Tag

`PATCH /api/tags/{name}`

Edit a Tag's name or description. Renaming cascades through AppTagApply rows transactionally — every droplet wearing the old name now wears the new one. At least one of `newName` / `description` is required (omit both → `400 NO_CHANGES`). Rename collisions within the same scope return `409 TAG_NAME_CONFLICT`. GLOBAL renames are taxonomer-only; SPACE renames require Space admin; USER renames are owner-only.

**Parameters**

- **`name`** _(string · path · required)_ — Current Tag name.
- **`scope`** _(string · body · required)_ — Scope of the Tag.
- **`ownerId`** _(string · body)_ — Disambiguating ownerId (null for GLOBAL).
- **`newName`** _(string · body)_ — New Tag name (cascades to AppTagApply).
- **`description`** _(string · body)_ — Replacement description (empty string clears).

**CLI**

```bash
drop tags update customer-acme --scope SPACE --description "Acme — net-new ARR account."
```

**MCP**

```json
{
  "tool": "drop_tags_update",
  "arguments": {
    "name": "customer-acme",
    "scope": "SPACE",
    "description": "Acme — net-new ARR account."
  }
}
```

**HTTP**

```bash
curl -X PATCH 'https://drop.himarley.ai/api/tags/customer-acme' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scope":"SPACE","description":"Acme — net-new ARR account."}'
```

**Response** (200)

```json
{
  "id": "tg_01HZX9...",
  "name": "customer-acme",
  "scope": "SPACE",
  "ownerId": "space_platform",
  "description": "Acme — net-new ARR account.",
  "createdById": "usr_jonathan",
  "createdAt": "2026-06-10T18:00:00.000Z",
  "updatedAt": "2026-06-10T19:30:00.000Z",
  "promotedFromId": null
}
```

### Delete a Tag

`DELETE /api/tags/{name}`

Hard-delete a Tag. Cascades to AppTagApply rows transactionally — the chip disappears from every droplet that wore it. GLOBAL deletes are taxonomer-only; SPACE deletes require Space admin; USER deletes are owner-only. Unlike Types, Tags have no soft-delete / restore window — deletion is final. Promoted-from lineage on derived Tags is preserved as a backref (`promotedFromId`); deleting the source Tag does NOT remove the derived row.

**Parameters**

- **`name`** _(string · path · required)_ — The Tag name.
- **`scope`** _(string · query · required)_ — Scope of the Tag.
- **`ownerId`** _(string · query)_ — Disambiguating ownerId (null for GLOBAL).

**CLI**

```bash
drop tags delete customer-acme --scope SPACE
```

**MCP**

```json
{
  "tool": "drop_tags_delete",
  "arguments": {
    "name": "customer-acme",
    "scope": "SPACE"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/tags/customer-acme' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "deleted": true,
  "name": "customer-acme",
  "scope": "SPACE",
  "applicationsCascaded": 14
}
```

### Promote a Tag to a wider scope

`POST /api/tags/{name}/promote`

Promote a USER or SPACE Tag to SPACE or GLOBAL. Creates a NEW Tag row at the target scope with `promotedFromId` pointing at the source — the source row is preserved (the owner can drop it manually if redundant). AppTagApply rows attached to the source are NOT rewritten; the chip rail's shadow-aware lookup will surface the wider-scope match on next render. GLOBAL targets are taxonomer-only; SPACE targets require Space admin on `targetSpaceSlug`. Promoting onto an existing name at the target scope returns `409 TAG_NAME_CONFLICT`.

**Parameters**

- **`name`** _(string · path · required)_ — The source Tag name.
- **`sourceScope`** _(string · body · required)_ — Source scope (USER or SPACE).
- **`sourceOwnerId`** _(string · body)_ — Disambiguating ownerId.
- **`targetScope`** _(string · body · required)_ — Target scope (SPACE or GLOBAL).
- **`targetSpaceSlug`** _(string · body)_ — Required when targetScope=SPACE.

**CLI**

```bash
drop tags promote customer-acme --source-scope USER --target-scope SPACE --target-space-slug platform
```

**MCP**

```json
{
  "tool": "drop_tags_promote",
  "arguments": {
    "name": "customer-acme",
    "sourceScope": "USER",
    "targetScope": "SPACE",
    "targetSpaceSlug": "platform"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/tags/customer-acme/promote' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sourceScope":"USER","targetScope":"SPACE","targetSpaceSlug":"platform"}'
```

**Response** (201)

```json
{
  "promoted": {
    "id": "tg_01HZNEW...",
    "name": "customer-acme",
    "scope": "SPACE",
    "ownerId": "space_platform",
    "description": null,
    "createdById": "usr_jonathan",
    "createdAt": "2026-06-10T20:00:00.000Z",
    "updatedAt": "2026-06-10T20:00:00.000Z",
    "promotedFromId": "tg_01HZSRC..."
  },
  "source": {
    "id": "tg_01HZSRC...",
    "name": "customer-acme",
    "scope": "USER",
    "ownerId": "usr_jonathan",
    "description": null,
    "createdById": "usr_jonathan",
    "createdAt": "2026-06-09T12:00:00.000Z",
    "updatedAt": "2026-06-09T12:00:00.000Z",
    "promotedFromId": null
  }
}
```

### Apply a Tag to a Droplet

`POST /api/apps/{slug}/tags`

Attach a Tag to a Droplet by creating an AppTagApply row. The Tag must exist in some scope visible to the caller; without `scope` the route uses USER > SPACE > GLOBAL precedence (matches the chip rail). Idempotent — applying the same (Droplet, Tag) pair returns 200 with the existing row. The caller must have write access to the Droplet; archived Droplets reject with `409 APP_ARCHIVED`. The response includes `shadows` (Tags at other scopes with the same name) so the UI can warn "you applied the USER copy; there's also a GLOBAL one."

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`tagName`** _(string · body · required)_ — The Tag name to apply.
- **`scope`** _(string · body)_ — Constrain to one scope (default: precedence lookup).
- **`ownerId`** _(string · body)_ — Disambiguating ownerId.

**CLI**

```bash
drop tags apply q3-strategy-doc --tag-name customer-acme --scope SPACE
```

**MCP**

```json
{
  "tool": "drop_tags_apply",
  "arguments": {
    "slug": "q3-strategy-doc",
    "tagName": "customer-acme",
    "scope": "SPACE"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/q3-strategy-doc/tags' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tagName":"customer-acme","scope":"SPACE"}'
```

**Response** (200)

```json
{
  "applied": {
    "id": "tg_01HZX9...",
    "name": "customer-acme",
    "scope": "SPACE",
    "ownerId": "space_platform",
    "description": "Acme account workspace.",
    "createdById": "usr_jonathan",
    "createdAt": "2026-06-10T18:00:00.000Z",
    "updatedAt": "2026-06-10T18:00:00.000Z",
    "promotedFromId": null
  },
  "shadows": []
}
```

### Remove a Tag from a Droplet

`DELETE /api/apps/{slug}/tags/{tagName}`

Detach a Tag from a Droplet by deleting its AppTagApply row. The Tag row itself is untouched — only the application is removed. Idempotent — removing a Tag that isn't applied returns 200. The caller must have write access to the Droplet. When multiple AppTagApply rows match the same `tagName` (because shadows are attached at multiple scopes), all matching rows for that name on the Droplet are removed — symmetric with the chip-rail "remove this chip" affordance.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`tagName`** _(string · path · required)_ — The Tag name to remove.

**CLI**

```bash
drop tags remove q3-strategy-doc customer-acme
```

**MCP**

```json
{
  "tool": "drop_tags_remove",
  "arguments": {
    "slug": "q3-strategy-doc",
    "tagName": "customer-acme"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/apps/q3-strategy-doc/tags/customer-acme' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "removed": true,
  "slug": "q3-strategy-doc",
  "tagName": "customer-acme"
}
```
