Core Resources
View as Markdown

Droplet

A single-file HTML or Markdown app served on Drop. Versioned content, access policy, optional Space membership.

The Droplet is Drop's core unit. Every Droplet has a stable slug that lives in its URL (/view/{slug}), a current published version, an access policy (PRIVATE / SPACE / SPECIFIC / EVERYONE), and optional membership in one Space. Skills (appType: SKILL) and Drops (appType: DROP) share this same shape; the only difference is rendering and feature gates.

The Droplet object

Every Droplet carries these attributes.

Attributes

slug
string
URL-friendly identifier. Stable for the lifetime of the Droplet — appears in /view/{slug} and every API path. Minimum 2 characters, lowercase with hyphens.
name
string
Human-readable display name. Shown on the dashboard and in every share context.
description
string· nullable
One-line description shown alongside the name on the dashboard.
appType
enum
DROPLETDROPSKILL
Default DROPLET (static HTML/Markdown). DROP is feature-flagged Node.js apps with databases; SKILL is a Claude Code skill recipe rendered with markdown preview.
internalAccess
enum
PRIVATESPACESPECIFICEVERYONE
Who at Hi Marley can view this Droplet. SPACE is live-bound to the Droplet's owning Space membership. SPECIFIC uses a named-people grant list. Set via the Set access method.
accessMode
enum
LINKPASSWORDSSO
Anonymous-share mode for external share links. Combines with internalAccess via the share-link layer.
currentVersion
integer
The latest version number. Incremented on every deploy and rollback.
publishedVersion
integer· nullable
The version currently published at the live URL. May lag currentVersion if the latest version is a draft.
spaceId
string· nullable
If the Droplet lives in a Space, the Space's id. Set via Move into a Space. null for personal-workspace Droplets.
tags
string[]
Free-form tags. Used by search (tag:retro) and Space featured-tags curation. Lowercase-normalized server-side.
archivedAt
timestamp· nullable
Set when the Droplet is non-destructively archived (hidden from default lists, still recoverable). null when active.
More attributes+ Show 12
id
string
CUID primary key. Stable but rarely surfaced; use slug in API paths.
s3Key
string
S3 path to the live HTML/Markdown file. {slug}/index.html. Internal — rarely needed by API consumers.
sourceFormat
enum
HTMLMARKDOWN
Detected at deploy time from file extension. Markdown is rendered server-side with a Hi Marley stylesheet.
fileSize
integer
Size of the live file in bytes.
searchText
string
Extracted text content used for full-text search. Regenerated on every deploy.
ownerId
string
The User who created the Droplet. Carries owner-only permissions (transfer, delete, change access mode).
status
enum
ACTIVEARCHIVEDDELETED
Hard-delete state. ARCHIVED here means *destructively* removed (S3 + edge cleanup done) — distinct from the non-destructive archivedAt.
commentsEnabled
boolean
Whether /view/{slug} shows the comments side panel. Default true.
allowEmbedding
boolean
Whether the Droplet can be embedded in an iframe on a non-Drop origin. Default false.
inlineEditingEnabled
boolean
Owner-only flag: enables the in-browser pencil editor on /view/{slug}. Default false.
createdAt
timestamp
ISO 8601 timestamp of creation.
updatedAt
timestamp
ISO 8601 timestamp of last metadata or content update.
THE DROPLET OBJECT
{
  "id": "clx1abc...",
  "slug": "q2-retro-board",
  "name": "Q2 retro",
  "description": "Quarterly retrospective board for the platform team",
  "appType": "DROPLET",
  "internalAccess": "SPACE",
  "accessMode": "LINK",
  "currentVersion": 3,
  "publishedVersion": 3,
  "s3Key": "q2-retro-board/index.html",
  "fileSize": 45200,
  "sourceFormat": "HTML",
  "searchText": "Q2 platform retro board with cards for went well…",
  "tags": [
    "retro",
    "platform"
  ],
  "spaceId": "spc_platform_team",
  "ownerId": "usr_alex",
  "status": "ACTIVE",
  "archivedAt": null,
  "commentsEnabled": true,
  "allowEmbedding": false,
  "inlineEditingEnabled": false,
  "createdAt": "2026-04-12T14:22:00.000Z",
  "updatedAt": "2026-05-31T09:15:00.000Z"
}

Methods

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

POST

Create a Droplet

/api/cli/deploy

Deploy a new Droplet from a single HTML or Markdown file. Auto-creates the slug if not provided. Pass ?draft=true to land in your private workspace instead of publishing. Two-step: this returns a presigned S3 upload URL; PUT the file to that URL, then POST to confirm.

POST/api/cli/deploy
drop create app.html \
  --slug q2-retro-board \
  --name "Q2 retro" \
  --description "Quarterly retrospective board"

# Draft (lands in your private workspace)
drop create app.html --slug q2-retro-board --draft
Response201
{
  "app": {
    "id": "clx1abc...",
    "slug": "q2-retro-board",
    "name": "Q2 retro",
    "description": "Quarterly retrospective board",
    "appType": "DROPLET",
    "accessMode": "LINK",
    "sourceFormat": "HTML",
    "status": "ACTIVE",
    "createdAt": "2026-04-12T14:22:00.000Z"
  },
  "uploadUrl": "https://drop-apps.s3.amazonaws.com/q2-retro-board/v1/index.html?X-Amz-...",
  "isNew": true
}
POST

Archive

/api/apps/{slug}/archive

Non-destructively archive a Droplet — sets archivedAt, hides it from default lists, but preserves all content and audit history. Restore with the unarchive endpoint. Idempotent: a second call returns 204 with no body.

POST/api/apps/{slug}/archive
drop archive q2-retro-board
Response200
{
  "slug": "q2-retro-board",
  "archivedAt": "2026-05-31T09:15:00.000Z"
}
POST

Unarchive

/api/apps/{slug}/unarchive

Clear archivedAt and return the Droplet to the active lane. Idempotent: a second call returns 204 with no body.

POST/api/apps/{slug}/unarchive
drop unarchive q2-retro-board
Response200
{
  "slug": "q2-retro-board",
  "archivedAt": null
}
GET

List pipeline stages

/api/apps/{slug}/pipeline

Return the latest-attempt PipelineStage row for each of the droplet's 7 background stages (upload, text-extract, summary, KB ingest, edge sync, CDN invalidation, vector indexing). Stages that have never run are absent. Caller must be able to read the droplet.

GET/api/apps/{slug}/pipeline
drop pipeline q2-retro-board
Response200
{
  "slug": "q2-retro-board",
  "stages": [
    {
      "stage": "UPLOAD",
      "status": "SUCCEEDED",
      "attempt": 1,
      "startedAt": "2026-06-07T15:00:00.000Z",
      "endedAt": "2026-06-07T15:00:01.230Z",
      "lastError": null,
      "metadata": {
        "version": 3,
        "fileSize": 84321
      }
    }
  ]
}
GET

Retrieve a Droplet

/api/apps/{slug}

Return the full Droplet object by slug. Includes Space context (live member count when internalAccess is SPACE) and the effective audience (computed from access mode + Space policy). 404 to anyone who can't see it (privacy by indistinguishability).

Parameters

slug
string· path· required
The Droplet's URL slug.
GET/api/apps/{slug}
drop get q2-retro-board
Response200
{
  "id": "clx1abc",
  "slug": "q2-retro-board",
  "name": "Q2 retro",
  "description": "Quarterly retrospective board for the platform team",
  "appType": "DROPLET",
  "internalAccess": "SPACE",
  "accessMode": "LINK",
  "currentVersion": 3,
  "publishedVersion": 3,
  "spaceId": "spc_platform_team",
  "tags": [
    "retro",
    "platform"
  ],
  "archivedAt": null,
  "ownerId": "usr_alex",
  "status": "ACTIVE",
  "s3Key": "q2-retro-board/index.html",
  "fileSize": 45200,
  "searchText": "Q2 platform retro…",
  "sourceFormat": "HTML",
  "commentsEnabled": true,
  "allowEmbedding": false,
  "inlineEditingEnabled": false,
  "createdAt": "2026-04-12T14:22:00.000Z",
  "updatedAt": "2026-05-31T09:15:00.000Z"
}
PATCH

Update a Droplet

/api/apps/{slug}

Patch Droplet metadata: name, description, tags, allowEmbedding, commentsEnabled, inlineEditingEnabled, accessMode, password, publicName, spaceSlug. Use the dedicated Set access endpoint to change internalAccess (audience expansions need an explicit confirm). Tags are server-side normalized: trim → lowercase → drop empty → cap at 50 chars each → dedup → cap at 20 total. Only the owner can change accessMode, password, publicName, spaceSlug, and inlineEditingEnabled; editors can change the other metadata.

Parameters

slug
string· path· required
The Droplet slug.
name
string· body
New display name.
description
string· body
New description.
tags
string[]· body
Replace tags (server normalizes: lowercase, dedup, ≤ 20 entries, ≤ 50 chars each).
commentsEnabled
boolean· body
Toggle the comments panel on /view/{slug}.
allowEmbedding
boolean· body
Allow iframe embedding from non-Drop origins.
inlineEditingEnabled
boolean· body
Owner-only. Enable the in-browser pencil editor.
accessMode
enum· body
Owner-only. LINK | PASSWORD | SSO.
password
string· body
Required when accessMode is PASSWORD.
publicName
string· body
Owner-only. Custom URL path.
spaceSlug
string | null· body
Owner-only. Move into/between Spaces (or null to evict).
PATCH/api/apps/{slug}
drop set q2-retro-board --name "Q2 platform retro" --tags retro,platform,2026q2
Response200
{
  "id": "clx1abc",
  "slug": "q2-retro-board",
  "name": "Q2 platform retro",
  "description": "Quarterly retrospective board for the platform team",
  "appType": "DROPLET",
  "internalAccess": "SPACE",
  "accessMode": "LINK",
  "currentVersion": 3,
  "publishedVersion": 3,
  "spaceId": "spc_platform_team",
  "tags": [
    "retro",
    "platform",
    "2026q2"
  ],
  "archivedAt": null,
  "ownerId": "usr_alex",
  "status": "ACTIVE",
  "s3Key": "q2-retro-board/index.html",
  "fileSize": 45200,
  "searchText": "Q2 platform retro…",
  "sourceFormat": "HTML",
  "commentsEnabled": true,
  "allowEmbedding": false,
  "inlineEditingEnabled": false,
  "createdAt": "2026-04-12T14:22:00.000Z",
  "updatedAt": "2026-05-31T09:15:00.000Z"
}
DELETE

Delete

/api/apps/{slug}

Irreversible hard-delete (029-freshness FR-028). Gated by the clean-slate rule (caller is sole owner AND zero comments AND zero view events AND no AppAccess grants). On gate failure returns 409 NOT_CLEAN_SLATE with { code, reason, message }. On success: removes the App row (Prisma cascade-deletes versions, comments, share links, access rows, stars, view events), removes the S3 prefix {slug}/, removes the DynamoDB edge record, and triggers a CloudFront invalidation for /{slug}/*. Use Archive for recoverable removal.

Parameters

slug
string· path· required
The Droplet slug.
DELETE/api/apps/{slug}
drop delete q2-retro-board
Response200
{
  "success": true,
  "hardDeleted": true
}
PUT

Set access

/api/apps/{slug}/access

Change internalAccess (PRIVATE / SPACE / SPECIFIC / EVERYONE). Audience expansions return 409 CONFIRMATION_REQUIRED until { confirmed: true } is included; reductions are immediate. See POST /api/apps/{slug}/access/preview for the audience diff. The full request body supports people (email grant list) and grants (mixed audience: email + User Group grants); see the route source for the complete schema.

Parameters

slug
string· path· required
The Droplet slug.
internalAccess
enum· body
PRIVATE | SPACE | SPECIFIC | EVERYONE.
confirmed
boolean· body
Apply an audience-expanding change past the 409 gate.
PUT/api/apps/{slug}/access
drop set-access q2-retro-board --internal-access EVERYONE --confirmed
Response200
{
  "internalAccess": "EVERYONE",
  "changed": [
    "mode"
  ]
}
POST

Move into a Space

/api/apps/{slug}/move

Move a Droplet into / between / out of a Space. Flips internalAccess to SPACE on move-in; resets to PRIVATE on move-out (FR-015). Preserves prior SPECIFIC grants as supplemental rows on move-in (FR-013) so a later move-out can recover them. Owner-only. Returns 409 CONFIRMATION_REQUIRED with the access preview for audience-expanding moves; never for reductions.

Parameters

slug
string· path· required
The Droplet slug.
toSpaceId
string | null· body
Target Space id, or null to evict to personal workspace.
confirmed
boolean· body
Apply an audience-expanding move past the 409 gate.
POST/api/apps/{slug}/move
drop move q2-retro-board --to-space-id spc_platform_team --confirmed
Response200
{
  "app": {
    "slug": "q2-retro-board",
    "spaceId": "spc_platform_team",
    "internalAccess": "SPACE"
  },
  "changed": [
    "space",
    "visibility",
    "preserved_specific"
  ],
  "preservedSpecificCount": 2
}
POST

Rollback to a version

/api/apps/{slug}/versions/{version}/rollback

Roll back to a previous version. The target version's content is copied to the live path and the CDN cache is invalidated. The previous live version stays in history as a separate row.

Parameters

slug
string· path· required
The Droplet slug.
version
number· path· required
Target version number.
POST/api/apps/{slug}/versions/{version}/rollback
drop rollback q2-retro-board 5
Response200
{
  "success": true,
  "version": {
    "id": "clxver5",
    "version": 5,
    "fileSize": 45200,
    "deployedBy": {
      "name": "Alex Chen",
      "email": "alex@himarley.com"
    },
    "notes": null,
    "createdAt": "2026-04-12T14:22:00.000Z"
  }
}
GET

List versions

/api/apps/{slug}/versions

Return the full version history for a Droplet — newest first. Each row includes the deployer, the file size, optional notes, the conversion status (for Markdown Droplets), and whether the version is the current published one.

Parameters

slug
string· path· required
The Droplet slug.
GET/api/apps/{slug}/versions
drop versions q2-retro-board
Response200
{
  "versions": [
    {
      "id": "clxver3",
      "version": 3,
      "fileSize": 45200,
      "deployedBy": {
        "name": "Alex Chen",
        "email": "alex@himarley.com"
      },
      "notes": "Fixed chart colors",
      "isCurrent": true,
      "createdAt": "2026-05-31T09:15:00.000Z",
      "sourceFormat": "HTML"
    }
  ],
  "currentVersion": 3,
  "publishedVersion": 3
}
POST

Redeploy

/api/apps/{slug}/redeploy

Provision the next published version of a Droplet. Returns a presigned S3 upload URL — PUT the new HTML/Markdown content to that URL, and the version is live as soon as the upload completes. Owner/editor only; baseVersion provides optimistic concurrency.

Parameters

slug
string· path· required
The Droplet slug.
fileSize
number· body
Expected upload size in bytes.
notes
string· body
Version notes.
contentType
string· body
text/html (default) | text/markdown.
baseVersion
number· body
Optimistic concurrency baseline.
POST/api/apps/{slug}/redeploy
drop redeploy q2-retro-board --notes "Updated team list" --file-size 45200
Response200
{
  "uploadUrl": "https://drop-apps.s3.amazonaws.com/q2-retro-board/v4/index.html?X-Amz-...",
  "version": 4,
  "app": {
    "id": "clx1abc",
    "slug": "q2-retro-board",
    "name": "Q2 retro",
    "deployCount": 4,
    "lastDeployedAt": "2026-05-31T09:15:00.000Z"
  }
}
POST

Duplicate

/api/apps/{slug}/duplicate

Create a new Droplet seeded from this one's latest version. The new Droplet starts in PRIVATE mode under the caller's ownership; pass an optional name and newSlug. Returns the new Droplet record. Slug collisions auto-resolve by appending -2, -3, etc.

Parameters

slug
string· path· required
The source Droplet slug.
name
string· body
Name for the duplicate.
newSlug
string· body
Slug for the duplicate (auto-resolves collisions).
POST/api/apps/{slug}/duplicate
drop duplicate q2-retro-board --name "Q3 retro template"
Response201
{
  "id": "clxnew",
  "slug": "q2-retro-board-copy",
  "name": "Q3 retro template",
  "description": "Quarterly retrospective board for the platform team",
  "appType": "DROPLET",
  "internalAccess": "PRIVATE",
  "accessMode": "LINK",
  "currentVersion": 1,
  "publishedVersion": 1,
  "spaceId": null,
  "tags": [],
  "archivedAt": null,
  "ownerId": "usr_alex",
  "status": "ACTIVE",
  "s3Key": "q2-retro-board-copy/index.html",
  "fileSize": 45200,
  "searchText": null,
  "commentsEnabled": true,
  "allowEmbedding": false,
  "inlineEditingEnabled": false,
  "createdAt": "2026-05-31T09:15:00.000Z",
  "updatedAt": "2026-05-31T09:15:00.000Z"
}
POST

Star a Droplet

/api/apps/{slug}/star

Mark a Droplet as starred for the calling user. Stars are per-user — they pin the Droplet to the top of the dashboard and surface it in drop list --starred. Idempotent: re-starring returns the original starredAt timestamp.

Parameters

slug
string· path· required
The Droplet slug.
POST/api/apps/{slug}/star
drop star q2-retro-board
Response200
{
  "slug": "q2-retro-board",
  "starredAt": "2026-05-31T09:15:00.000Z"
}
DELETE

Unstar a Droplet

/api/apps/{slug}/star

Remove the caller's star on a Droplet. Idempotent: returns removed: false if there was nothing to remove.

Parameters

slug
string· path· required
The Droplet slug.
DELETE/api/apps/{slug}/star
drop unstar q2-retro-board
Response200
{
  "slug": "q2-retro-board",
  "removed": true
}
POST

Pin to Space

/api/apps/{slug}/pin

Pin a Droplet to the top of its Space. Only valid for Droplets that belong to a Space. Pinning is per-Droplet (not per-user — every Space member sees the same pinned set). Owner/editor only.

Parameters

slug
string· path· required
The Droplet slug.
POST/api/apps/{slug}/pin
drop pin q2-retro-board
Response200
{
  "slug": "q2-retro-board",
  "pinnedAt": "2026-05-31T09:15:00.000Z"
}
DELETE

Unpin from Space

/api/apps/{slug}/pin

Remove a Droplet's pin within its Space. Owner/editor only. Idempotent.

Parameters

slug
string· path· required
The Droplet slug.
DELETE/api/apps/{slug}/pin
drop unpin q2-retro-board
Response200
{
  "slug": "q2-retro-board",
  "pinnedAt": null
}
POST

Publish a draft

/api/apps/{slug}/publish

Promote one of the caller's drafts to a new published version. Squashes any sibling drafts in the workspace (only the chosen draft survives, the rest are discarded). With no draftId, publishes the latest draft. If the Droplet was previously archived, publish clears archivedAt and the response includes wokeFromArchive: true (029-freshness FR-018 wake-on-publish). Owner/editor only.

Parameters

slug
string· path· required
The Droplet slug.
draftId
string· body
Draft id or shortId — defaults to latest.
notes
string· body
Override the draft's notes.
POST/api/apps/{slug}/publish
drop publish q2-retro-board
Response200
{
  "version": 4,
  "publishedAt": "2026-05-31T09:15:00.000Z",
  "siblingsDiscarded": 2
}
POST

Unpublish

/api/apps/{slug}/unpublish

Take a published Droplet offline. Sets publishedVersion = null (preserves currentVersion so republish can restore the same version). Viewers see an "unavailable" placeholder. Drafts are unaffected. Owner/editor only.

Parameters

slug
string· path· required
The Droplet slug.
POST/api/apps/{slug}/unpublish
drop unpublish q2-retro-board
Response200
{
  "ok": true
}
POST

Republish

/api/apps/{slug}/republish

Restore a previously-unpublished Droplet by setting publishedVersion = currentVersion. Idempotent on already-published Droplets. Owner/editor only.

Parameters

slug
string· path· required
The Droplet slug.
POST/api/apps/{slug}/republish
drop republish q2-retro-board
Response200
{
  "version": 4
}
GET

List drafts

/api/apps/{slug}/drafts

List the calling user's drafts on this Droplet. Drafts are private per-caller — co-editors cannot see them. Returns 404 (not 403) when the caller lacks edit rights (privacy by indistinguishability).

Parameters

slug
string· path· required
The Droplet slug.
GET/api/apps/{slug}/drafts
drop drafts q2-retro-board
Response200
{
  "drafts": [
    {
      "id": "clxdr1",
      "shortId": 1,
      "fileSize": 46123,
      "sourceFormat": "HTML",
      "notes": "WIP: refactor cards",
      "createdAt": "2026-05-31T09:15:00.000Z",
      "commentCount": 0
    }
  ]
}
GET

Retrieve a draft

/api/apps/{slug}/drafts/{draftId}

Get metadata for one of the caller's drafts on a Droplet. draftId may be the CUID or the workspace-local short ID (e.g. 1, 2). Returns 404 when not owned by the caller (privacy by indistinguishability).

Parameters

slug
string· path· required
The Droplet slug.
draftId
string· path· required
Draft CUID or shortId.
GET/api/apps/{slug}/drafts/{draftId}
drop draft get q2-retro-board 1
Response200
{
  "id": "clxdr1",
  "shortId": 1,
  "fileSize": 46123,
  "sourceFormat": "HTML",
  "notes": "WIP: refactor cards",
  "createdAt": "2026-05-31T09:15:00.000Z",
  "commentCount": 0
}
DELETE

Discard a draft

/api/apps/{slug}/drafts/{draftId}

Discard a single draft from the caller's workspace. Returns 204 on success. 404 when the draft is gone or never existed (the CLI / MCP layer treats 404 as idempotent success).

Parameters

slug
string· path· required
The Droplet slug.
draftId
string· path· required
Draft CUID or shortId.
DELETE/api/apps/{slug}/drafts/{draftId}
drop draft discard q2-retro-board 1
Response204
{
  "discarded": true
}
DELETE

Discard all drafts

/api/apps/{slug}/drafts

Discard every draft in the caller's workspace on this Droplet. Always returns 204 (idempotent — re-running on an empty workspace is a no-op).

Parameters

slug
string· path· required
The Droplet slug.
DELETE/api/apps/{slug}/drafts
drop drafts discard-all q2-retro-board
Response204
{
  "discarded": true
}
PUT

Set Droplet Type

/api/apps/{slug}/type

Set the Droplet's Type slot (App.typeId) by resolving a Type by name. Resolution honors scope (USER | SPACE | GLOBAL) when provided, otherwise falls back to most-specific-wins from the viewer's context (user → space → global). NON-DESTRUCTIVE: existing typeFields are PRESERVED across a Type change, and any sign-off — votes and audit log — survives it. Values the new Type does not declare simply do not render, and are still there if you switch back. A topic-shaped Type seeds its draft status for keys that are absent, so a returning decision keeps the status it left with. To genuinely discard a decision, use droplet.clearType, which is the explicit act and does purge. Owner/editor only.

Parameters

slug
string· path· required
The Droplet slug.
typeName
string· body· required
Name of the Type to attach.
scope
enum· body
USER | SPACE | GLOBAL — narrows Type resolution.
spaceSlug
string· body
Disambiguate when scope=SPACE and the viewer belongs to multiple Spaces with a Type of this name.
PUT/api/apps/{slug}/type
drop droplets set-type q2-retro-board --type-name recommendation --scope SPACE --space-slug platform-team
Response200
{
  "slug": "q2-retro-board",
  "typeId": "typ_recommendation_platform",
  "typeName": "recommendation",
  "scope": "SPACE",
  "typeFields": {}
}
DELETE

Clear Droplet Type

/api/apps/{slug}/type

Clear the Droplet's Type slot — sets App.typeId = null and resets App.typeFields = {}. On a decision this is the un-decision path: the sign-offs and the recorded call go with the Type, reported as purgedSignoff. Idempotent: returns cleared: false when there was nothing to clear. Owner/editor only.

Parameters

slug
string· path· required
The Droplet slug.
DELETE/api/apps/{slug}/type
drop droplets clear-type q2-retro-board
Response200
{
  "slug": "q2-retro-board",
  "cleared": true,
  "purgedSignoff": {
    "responses": 3,
    "hadVerdict": true
  }
}
PUT

Set a typed field on a Droplet

/api/apps/{slug}/fields/{fieldKey}

Write a single value into App.typeFields[fieldKey]. The Droplet must have a Type slot set (App.typeId != null); the server validates fieldKey against the Type's schema and coerces / rejects value per the field definition. Per-field errors return 400 TYPE_FIELD_* with { field } extras. Archived Droplets reject writes with 409 APP_ARCHIVED. Owner/editor only.

Parameters

slug
string· path· required
The Droplet slug.
fieldKey
string· path· required
The field key (defined by the Droplet's current Type).
value
string | number | boolean | null | array· body· required
The new value — scalar, null (clear), or audience-ref array. Server validates against the Type schema.
PUT/api/apps/{slug}/fields/{fieldKey}
drop droplets set-field q2-retro-board status --value in-flight
Response200
{
  "slug": "q2-retro-board",
  "fieldKey": "status",
  "value": "in-flight",
  "typeFields": {
    "status": "in-flight",
    "priority": "high"
  }
}
DELETE

Clear a typed field on a Droplet

/api/apps/{slug}/fields/{fieldKey}

Remove a single key from App.typeFields. Idempotent: returns cleared: false when the key was already absent. Owner/editor only.

Parameters

slug
string· path· required
The Droplet slug.
fieldKey
string· path· required
The field key to remove.
DELETE/api/apps/{slug}/fields/{fieldKey}
drop droplets clear-field q2-retro-board priority
Response200
{
  "slug": "q2-retro-board",
  "fieldKey": "priority",
  "cleared": true,
  "typeFields": {
    "status": "in-flight"
  }
}
POST

Transfer ownership

/api/apps/{slug}/transfer

Hand a Droplet to another person. Callable by the current owner or a site admin — the admin path exists because the common case (the owner is on PTO or has left) is exactly when the owner cannot act. The prior owner keeps an EDITOR grant unless keepPriorOwnerAsEditor is false.

Ownership is not an audience change: the recipient must already be able to see the Droplet, or the call returns 409 RECIPIENT_NO_ACCESS. Grant them access first, then transfer. internalAccess, accessMode, Space membership, and every draft workspace are left alone — drafts are keyed per-user and stay with their author.

Transferring to the current owner is a no-op (204). Archived Droplets are rejected.

Parameters

slug
string· path· required
The Droplet slug.
to
string· body· required
Email of the new owner. Must already have access to the Droplet.
keepPriorOwnerAsEditor
boolean· body
Default true. Leaves the prior owner an EDITOR grant so they keep write access. Set false when offboarding.
POST/api/apps/{slug}/transfer
drop transfer forum-video-review --to jackie.booth@himarley.com --keep-prior-owner-as-editor
Response200
{
  "id": "clx1abc",
  "slug": "forum-video-review",
  "name": "Forum Video Review",
  "description": null,
  "appType": "DROPLET",
  "internalAccess": "EVERYONE",
  "accessMode": "LINK",
  "currentVersion": 42,
  "publishedVersion": 42,
  "spaceId": null,
  "tags": [
    "forum-video"
  ],
  "archivedAt": null,
  "ownerId": "usr_jackie",
  "status": "ACTIVE",
  "s3Key": "forum-video-review/index.html",
  "fileSize": 107111,
  "searchText": "Forum video review…",
  "sourceFormat": "HTML",
  "commentsEnabled": true,
  "allowEmbedding": false,
  "inlineEditingEnabled": true,
  "createdAt": "2026-08-12T16:18:33.352Z",
  "updatedAt": "2026-09-11T10:02:00.000Z"
}
POST

Transfer everything one person owns

/api/admin/ownership/transfer-by-owner

Move every Droplet a person owns to someone else — the offboarding path. Restricted to the @drop-owner-transfer role; being a site admin is not sufficient.

Deliberately returns a count, never a list: total plus a breakdown by access mode. That is enough to judge whether you named the right person (fourteen is plausible, four hundred is a typo) without displaying titles. Archived Droplets are excluded.

The count is recipient-aware: total is what will actually move, and blocked is what the recipient cannot read (a Space they are not in, a SPECIFIC list they are not on). Transfer never widens an audience, so those stay put.

One call moves at most 50, chosen from the movable set rather than the most recent, so batch slots are never spent on Droplets the recipient cannot read. remaining counts movable leftovers only — it reaches zero, where a raw owned-count would stall forever on blocked rows. Counting stops at 500; past that truncated is set and the numbers are a floor. Re-running is safe: a Droplet already owned by the recipient is a no-op.

Call with dryRun: true first — the UI does. Partial success is the contract: each Droplet goes through the same per-Droplet rules, and anything that could not move comes back in skipped with a code.

Bulk emits ONE notification per party rather than one per Droplet, while still writing a per-Droplet audit row.

Parameters

from
string· body· required
Email of the departing owner.
to
string· body· required
Email of the person receiving them.
reason
string· body· required
Who asked and why. Required.
keepPriorOwnerAsEditor
boolean· body
Default false for this mode.
dryRun
boolean· body
Return counts without moving anything.
POST/api/admin/ownership/transfer-by-owner
drop transfer-all --from allison.paraskevas@himarley.com --to jackie.booth@himarley.com --reason "Allison on PTO through 9/22; Jackie running the forum review." --dry-run
Response200
{
  "dryRun": true,
  "total": 12,
  "byAccess": {
    "EVERYONE": 9,
    "PRIVATE": 3
  },
  "archived": 1,
  "blocked": 2,
  "truncated": false
}
POST

Retry pipeline stage

/api/apps/{slug}/pipeline/{stage}/retry

Re-run a single pipeline stage on this droplet. Creates a new PipelineStage row at attempt = (prior + 1) and returns the terminal status. Synchronous — the caller waits for the stage to complete. Caller must be droplet owner or sys admin.

POST/api/apps/{slug}/pipeline/{stage}/retry
drop pipeline retry q2-retro-board KB_INGEST
Response200
{
  "slug": "q2-retro-board",
  "stage": "KB_INGEST",
  "status": "SUCCEEDED",
  "attempt": 2,
  "lastError": null
}