Core Resources
View as Markdown

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 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, 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 for the model.

The Tag object

Every Tag carries these attributes.

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).
THE TAG OBJECT
{
  "name": "onboarding",
  "usageCount": 14
}

Methods

Endpoints to operate on a Tag. The example switches between curl, drop CLI, and MCP via the dropdown — your choice persists across the page.

GET

List tags in use

/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).
GET/api/tags
drop tags list
Response200
{
  "tags": [
    {
      "name": "onboarding",
      "usageCount": 14
    },
    {
      "name": "q3-2026",
      "usageCount": 9
    },
    {
      "name": "customer:acme",
      "usageCount": 4
    }
  ]
}
POST

Create a Tag

/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).
POST/api/tags
drop tags create --name customer-acme --scope SPACE --space-slug platform --description "Acme account workspace."
Response201
{
  "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
}
GET

Retrieve a Tag (shadow-aware)

/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.
GET/api/tags/{name}
drop tags get priority
Response200
{
  "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
    }
  ]
}
PATCH

Update a Tag

/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).
PATCH/api/tags/{name}
drop tags update customer-acme --scope SPACE --description "Acme — net-new ARR account."
Response200
{
  "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

Delete a Tag

/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).
DELETE/api/tags/{name}
drop tags delete customer-acme --scope SPACE
Response200
{
  "deleted": true,
  "name": "customer-acme",
  "scope": "SPACE",
  "applicationsCascaded": 14
}
POST

Promote a Tag to a wider scope

/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.
POST/api/tags/{name}/promote
drop tags promote customer-acme --source-scope USER --target-scope SPACE --target-space-slug platform
Response201
{
  "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
  }
}
POST

Apply a Tag to a Droplet

/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.
POST/api/apps/{slug}/tags
drop tags apply q3-strategy-doc --tag-name customer-acme --scope SPACE
Response200
{
  "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": []
}
DELETE

Remove a Tag from a Droplet

/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.
DELETE/api/apps/{slug}/tags/{tagName}
drop tags remove q3-strategy-doc customer-acme
Response200
{
  "removed": true,
  "slug": "q3-strategy-doc",
  "tagName": "customer-acme"
}