Skip to main content

Draft Management

Looking for the product view?
The Help Center explains this surface as users see it: Editing & draft protection.

Apps use a draft-based editing workflow: changes happen on a separate draft copy and only reach the published version on an explicit commit.

The model

  • One draft per app. A draft is a second App row whose editingId points at the original. Opening an editor when a draft already exists returns that draft (with its lock state), regardless of who created it; there is never a second draft.
  • A draft is a full working copy. Creating one copies the library (every file), copies all entity bindings, clones the workspace database schema, and deploys the draft's server routes, so the builder preview runs against the draft end to end.
  • Drafts carry an edit lock. editingLockedBy names the active editor; editingLocked is computed from the last heartbeat (60-second window). Another editor can take the lock with _takeover.
  • Managed apps cannot be drafted. Apps with origin deployed or marketplace refuse _edit outright (403); their changes would be overwritten by the next CLI deploy or marketplace sync. This is an entity-level invariant that even superusers cannot bypass.

Workflow Overview

  1. Open: POST /apps/{id}/_edit creates (or returns) the draft
  2. Edit: modify the draft's files via /apps/{draftId}/contents/...
  3. Review: GET /apps/{id}/draft/_diff summarizes binding changes
  4. Commit: POST /apps/{id}/draft/_commit publishes to the original
  5. Discard: DELETE /apps/{id}/draft abandons the draft

GET /api/apps/drafts

List all drafts belonging to the current user across all apps, most recently modified first, each embedding the original app under editing.

Authentication: Required

List my in-progress draftsGET /api/apps/drafts
Drafts owned by the caller, most recently modified first, each embedding the original app it edits.
Response · 200
[
{
"naturalId": "admin:order-desk-1",
"isManaged": false,
"permissions": {
"assignTags": false,
"bind": false,
"changeOwner": false,
"copy": false,
"delete": false,
"edit": false,
"manage": false,
"purge": false,
"restore": false,
"run": false,
"share": false,
"switchRoles": false,
"write": false
},
"id": "00000000-0000-4000-8000-000000000001",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk-1",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000002",
"webhookSecret": null,
"dbRolePassword": null,
"deletedAt": null,
"editingId": "00000000-0000-4000-8000-000000000003",
"deployedAt": "2026-01-15T16:20:00.000Z",
"editingSession": "00000000-0000-4000-8000-000000000004",
"editingHeartbeatAt": null,
"editingLockedBy": "admin",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"editing": {
"naturalId": "admin:order-desk",
"isManaged": false,
"permissions": {
"assignTags": false,
"bind": false,
"changeOwner": false,
"copy": false,
"delete": false,
"edit": false,
"manage": false,
"purge": false,
"restore": false,
"run": false,
"share": false,
"switchRoles": false,
"write": false
},
"id": "00000000-0000-4000-8000-000000000003",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000005",
"webhookSecret": "encrypted:iv:0000000000000000",
"dbRolePassword": null,
"deletedAt": null,
"editingId": null,
"deployedAt": null,
"editingSession": null,
"editingHeartbeatAt": null,
"editingLockedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null
}
}
]
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/apps/{id}/_edit

Open a draft for editing. If a draft already exists for this app (created by anyone), it is returned with its current lock state instead.

Authentication: Required

Permissions Required: app:edit (app:write and the app is not managed). The managed-app refusal is unconditional and happens before the permission check.

Payload (optional):

FieldTypeDescription
chatIduuidAn existing builder chat to link to the draft
Open a draft for editingPOST /api/apps/admin%3Aorder-desk/_edit
One draft per app: if a draft already exists (even owned by someone else), it is returned with its lock state instead of creating a second one.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_print"
},
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/_deploy"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/draft/_diff"
},
"inf:commit-draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/draft/_commit"
},
"inf:app-takeover": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/00000000-0000-4000-8000-000000000003",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/00000000-0000-4000-8000-000000000003/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/view/?token=%7Btoken%7D",
"title": "Share Page"
}
},
"naturalId": "admin:order-desk-1",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk-1",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000003",
"webhookSecret": null,
"dbRolePassword": null,
"deletedAt": null,
"editingId": "00000000-0000-4000-8000-000000000002",
"deployedAt": "2026-01-15T16:20:00.000Z",
"editingSession": "00000000-0000-4000-8000-000000000004",
"editingHeartbeatAt": null,
"editingLockedBy": "admin",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
Captured from the API examples test suite; ids and timestamps are normalized.

Key Behaviors:

  • The draft has its own UUID, its own auto-suffixed slug (order-desk-1), editingId pointing at the original, and ownerId set to the caller
  • The library is copied (the draft's libraryId differs from the original's), entity bindings are copied, the workspace schema is cloned, and the draft is deployed before the route returns
  • editingSession and editingLockedBy are initialized to the caller
  • If the workspace clone or deploy fails, the error is surfaced (no silently broken draft)

Error Responses:

  • 404 Not Found - App does not exist or user lacks read access
  • 403 Forbidden - App is managed, or user lacks edit permission

GET /api/apps/{id}/draft

Retrieve the app's draft (addressed by the original app's id).

Authentication: Required

Get an App's draftGET /api/apps/00000000-0000-4000-8000-000000000001/draft
editingLocked reflects whether an editor heartbeat was seen in the last 60 seconds.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_print"
},
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_deploy"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_diff"
},
"inf:commit-draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit"
},
"inf:app-takeover": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/_takeover"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files-draft",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files-draft/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/view/?token=%7Btoken%7D",
"title": "Share Page"
},
"inf:original": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001"
}
},
"naturalId": "admin:order-desk-1",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000002",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk-1",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000003",
"webhookSecret": null,
"dbRolePassword": null,
"deletedAt": null,
"editingId": "00000000-0000-4000-8000-000000000001",
"deployedAt": "2026-01-15T16:20:00.000Z",
"editingSession": "00000000-0000-4000-8000-000000000004",
"editingHeartbeatAt": null,
"editingLockedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"datasets": {},
"library": {
"naturalId": "admin:order-desk-files-draft",
"connectionOwner": "admin",
"isLocal": true,
"permissions": {
"assignTags": true,
"changeOwner": true,
"configure": true,
"connect": true,
"edit": true,
"delete": true,
"write": true,
"sync": true,
"rename": true,
"share": true,
"editConnection": true
},
"id": "00000000-0000-4000-8000-000000000003",
"type": "local",
"name": "Order Desk Files (Draft)",
"slug": "order-desk-files-draft",
"description": null,
"ownerId": "admin",
"data": null,
"folderId": null,
"shared": false,
"assistantAccess": false,
"embedded": true,
"syncedAt": null,
"syncInterval": null,
"snapshotOfId": null,
"chatId": null,
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"integration": null
},
"editingLocked": false,
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
Captured from the API examples test suite; ids and timestamps are normalized.

editingLocked is true while an editor heartbeat was seen within the last 60 seconds; editingLockedBy is only reported while the lock is live. The response carries a Location header with the draft's own URL.

Error Responses:

  • 404 Not Found - Original app doesn't exist, user lacks read access, or no draft exists

PUT /api/apps/{id}/draft

Create or return the draft, like POST /_edit with PUT semantics (no chatId payload). Same permission gate and same create-side effects.


GET /api/apps/{id}/draft/_diff

Structured diff of the app's bindings between original and draft, for the commit confirmation UI.

Diff a draft before committingGET /api/apps/00000000-0000-4000-8000-000000000001/draft/_diff
Summarizes dependency-slot and API-whitelist changes for the commit confirmation UI. This draft has no binding changes yet.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_diff"
}
},
"dependencies": [],
"whitelist": {
"added": [],
"removed": []
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

Response Shape:

FieldDescription
dependenciesOne entry per dependency slot: { name, status, was, now, changes } where status is added, removed, changed, or preserved, and changes lists which of target/targetId/runAs/options moved
whitelist{ added, removed } arrays of raw API-whitelist rows

Error Responses:

  • 404 Not Found - No draft exists for this app

POST /api/apps/{draftId}/_takeover

Take over the edit lock on a draft. Addressed at the draft's id (a request against a non-draft app is 400).

Authentication: Required

Permissions Required: app:write on the draft

Payload (optional): { clientId: <uuid> } identifying the new editor session.

Take over another editor's draftPOST /api/apps/00000000-0000-4000-8000-000000000001/_takeover
Addressed at the DRAFT id, not the original. Reassigns the editing lock to the caller and broadcasts the change to the previous editor.
Request body
{}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_print"
},
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/_deploy"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/draft/_diff"
},
"inf:commit-draft": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000002/draft/_commit"
},
"inf:app-takeover": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/_takeover/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files-draft",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files-draft/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/view/?token=%7Btoken%7D",
"title": "Share Page"
}
},
"naturalId": "admin:order-desk-1",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk-1",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000003",
"webhookSecret": null,
"dbRolePassword": null,
"deletedAt": null,
"editingId": "00000000-0000-4000-8000-000000000002",
"deployedAt": "2026-01-15T16:20:00.000Z",
"editingSession": "00000000-0000-4000-8000-000000000004",
"editingHeartbeatAt": "2026-01-15T16:20:00.000Z",
"editingLockedBy": "admin",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"datasets": [],
"library": {
"naturalId": "admin:order-desk-files-draft",
"connectionOwner": "admin",
"isLocal": true,
"permissions": {
"assignTags": true,
"changeOwner": true,
"configure": true,
"connect": true,
"edit": true,
"delete": true,
"write": true,
"sync": true,
"rename": true,
"share": true,
"editConnection": true
},
"id": "00000000-0000-4000-8000-000000000003",
"type": "local",
"name": "Order Desk Files (Draft)",
"slug": "order-desk-files-draft",
"description": null,
"ownerId": "admin",
"data": null,
"folderId": null,
"shared": false,
"assistantAccess": false,
"embedded": true,
"syncedAt": null,
"syncInterval": null,
"snapshotOfId": null,
"chatId": null,
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"integration": null
},
"editing": {
"naturalId": "admin:order-desk",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000002",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk",
"description": "Track and fulfill open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": null,
"libraryId": "00000000-0000-4000-8000-000000000005",
"webhookSecret": "encrypted:iv:0000000000000000",
"dbRolePassword": null,
"deletedAt": null,
"editingId": null,
"deployedAt": null,
"editingSession": null,
"editingHeartbeatAt": null,
"editingLockedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null
},
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
Captured from the API examples test suite; ids and timestamps are normalized.

The previous editor is notified over the app-edit-activity channel and their builder surfaces the takeover dialog.


POST /api/apps/{id}/draft/_commit

Commit the draft, publishing its state onto the original app. This is the only save path: there is no "save but keep editing" variant; committing always ends the editing session.

Authentication: Required

Permissions Required: app:edit (managed apps cannot reach commit)

Commit a draftPOST /api/apps/00000000-0000-4000-8000-000000000001/draft/_commit
Publishes the draft: name/defn/settings/description and the draft library replace the originals, bindings are copied over, the draft is destroyed, and the app is redeployed.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_print"
},
{
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/_deploy"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/draft/_diff"
},
"edit": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/_edit"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/00000000-0000-4000-8000-000000000001/draft/_commit/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/admin%3Aorder-desk-files/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk/view/?token=%7Btoken%7D",
"title": "Share Page"
}
},
"naturalId": "admin:order-desk",
"isManaged": false,
"permissions": {
"assignTags": true,
"bind": true,
"changeOwner": true,
"copy": true,
"delete": true,
"edit": true,
"manage": true,
"purge": true,
"restore": true,
"run": true,
"share": true,
"switchRoles": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000001",
"type": "report",
"name": "Order Desk",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk",
"description": "Track, fulfill, and audit open orders",
"defn": {
"entryPoint": "index.html"
},
"settings": {
"sandbox": true,
"passDatasets": true
},
"origin": "ui",
"source": null,
"sourceId": null,
"shared": false,
"defnUpdatedAt": "2026-01-15T16:20:00.000Z",
"libraryId": "00000000-0000-4000-8000-000000000002",
"webhookSecret": "encrypted:iv:0000000000000000",
"dbRolePassword": null,
"deletedAt": null,
"editingId": null,
"deployedAt": "2026-01-15T16:20:00.000Z",
"editingSession": null,
"editingHeartbeatAt": null,
"editingLockedBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"folderId": null,
"datasets": [],
"library": {
"naturalId": "admin:order-desk-files",
"connectionOwner": "admin",
"isLocal": true,
"permissions": {
"assignTags": true,
"changeOwner": true,
"configure": true,
"connect": true,
"edit": true,
"delete": true,
"write": true,
"sync": true,
"rename": true,
"share": true,
"editConnection": true
},
"id": "00000000-0000-4000-8000-000000000003",
"type": "local",
"name": "Order Desk Files",
"slug": "order-desk-files",
"description": null,
"ownerId": "admin",
"data": null,
"folderId": null,
"shared": false,
"assistantAccess": false,
"embedded": true,
"syncedAt": null,
"syncInterval": null,
"snapshotOfId": null,
"chatId": null,
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"integration": null
},
"editing": null,
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
Captured from the API examples test suite; ids and timestamps are normalized.

What commits:

  • defn, settings, and description are copied from the draft; name only when the draft renamed the app; defnUpdatedAt is stamped
  • The draft's library replaces the original's (the old library is destroyed; snapshot libraries are detached first so snapshots survive)
  • Entity bindings on the original are replaced with the draft's
  • The app's origin becomes ui (a UI-committed app is user-owned)
  • The draft row, its workspace schema, and its routes are destroyed
  • The original app is redeployed with its new content. If that deploy fails the commit itself has already landed; re-trigger with POST /apps/{id}/_deploy

Error Responses:

  • 404 Not Found - App or draft doesn't exist
  • 403 Forbidden - App is managed or user lacks edit permission

DELETE /api/apps/{id}/draft

Discard the draft without committing.

Authentication: Required

Permissions Required: app:write on the original app

Discard a draftDELETE /api/apps/00000000-0000-4000-8000-000000000001/draft
Responds 200 with an empty body. The draft, its copied library, and its workspace are destroyed; the original app is untouched.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

Behavior:

  • Destroys the draft row, its copied library, its workspace schema, and its deployed routes; re-parents any snapshots taken while editing onto the original app
  • The original app is unchanged

POST /api/apps/{id}/draft/_heartbeat

Deprecated no-op. Older clients posted this every 30 seconds to hold the edit lock; the route now always answers 204 No Content without writing anything (the row-update storm it caused under load was removed). It still 404s when the draft is gone and 403s without write access, so older clients degrade correctly.


Common Patterns

Edit → Modify → Commit

// 1. Start editing
const draft = await POST('/api/apps/analytics:sales-dashboard/_edit');

// 2. Make changes to draft files
await PUT(`/api/apps/${draft.id}/contents/index.html`, {
content: updatedHTML
});

// 3. Update draft metadata
await PUT(`/api/apps/${draft.id}`, {
description: 'Updated description'
});

// 4. Review and commit
const diff = await GET('/api/apps/analytics:sales-dashboard/draft/_diff');
await POST('/api/apps/analytics:sales-dashboard/draft/_commit');

Edit → Cancel

const draft = await POST('/api/apps/analytics:sales-dashboard/_edit');
await PUT(`/api/apps/${draft.id}/contents/index.html`, { ... });
await DELETE('/api/apps/analytics:sales-dashboard/draft');

Respect the lock

const draft = await POST('/api/apps/analytics:sales-dashboard/_edit');

if (draft.editingLocked && draft.editingLockedBy !== me) {
// someone else is editing; offer a takeover
await POST(`/api/apps/${draft.id}/_takeover`, { clientId });
}