Actions
Action endpoints perform specific operations on apps like copying, printing, and more.
POST /api/apps/{id}/_copy
Create a copy of an app, including its library and all associated resources.
Authentication: Required
Permissions Required:
- Member+ role (copy permission) on the source app
- Member role (create permission) for apps
Managed Apps (deployed or marketplace origin, isManaged: true) cannot be
copied: the route returns 403 Forbidden for every caller, superusers
included, because a copy would fork the App out of its managed update
lifecycle.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
Payload:
{
"name": "Sales Dashboard (Copy)",
"progress": "optional-progress-callback-id"
}
Payload Fields:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Name for the copy (default: original name) |
progress | string | No | Progress callback ID for tracking |
Returns the copied app with status 201 Created and a Location header
pointing to the new app.
{
"name": "Order Desk (Staging)"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging"
},
"inf:owner": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/owner"
},
"inf:draft": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/draft"
},
"inf:app-shares": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/shares"
},
"inf:app-tags": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/tags"
},
"inf:favorite": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/favorite"
},
"inf:app-tag": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/tags/{tagId}",
"templated": true
},
"inf:export-bundle": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/_export"
},
"inf:cloud-publish": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/_cloud-publish"
},
"inf:copy": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/_copy"
},
"inf:print": [
{
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/_print"
},
{
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/_print",
"title": "Print to PDF"
}
],
"inf:migrate": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/_migrate"
},
"inf:deploy": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/_deploy"
},
"inf:app-mcp": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/mcp"
},
"inf:app-tokens": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/tokens"
},
"inf:app-roles": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/roles"
},
"inf:app-dependencies": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/dependencies"
},
"inf:app-dependency": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/dependencies/{name}",
"templated": true
},
"inf:app-environment": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/environment"
},
"inf:app-environment-var": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/environment/{key}",
"templated": true
},
"inf:app-suggest-icon": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/_suggest-icon"
},
"inf:app-modernize-manifest": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/_modernize-manifest"
},
"inf:app-draft-diff": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/draft/_diff"
},
"edit": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/_edit"
},
"inf:app-share": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/shares/{principalId}",
"templated": true
},
"inf:entrypoint": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/view/",
"title": "index.html"
},
"inf:library": {
"href": "https://informer.example.com/api/libraries/00000000-0000-4000-8000-000000000002",
"title": "App Files"
},
"inf:library-contents": {
"href": "https://informer.example.com/api/libraries/00000000-0000-4000-8000-000000000002/contents/index.html",
"title": "Raw Contents"
},
"share": {
"href": "https://informer.example.com/api/apps/admin%3Aorder-desk-staging/view/?token=%7Btoken%7D",
"title": "Share Page"
}
},
"naturalId": "admin:order-desk-staging",
"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,
"unmanage": true,
"write": true
},
"id": "00000000-0000-4000-8000-000000000003",
"type": "app",
"name": "Order Desk (Staging)",
"tenant": "acme",
"ownerId": "admin",
"slug": "order-desk-staging",
"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",
"folderId": 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",
"contentModifiedAt": "2026-01-15T16:20:00.000Z"
}
Copy Behavior:
The copy operation duplicates:
- All app metadata (name, description, defn, settings)
- The embedded library (if present) with all files
- All entity-binding rows (
app_entity: dependency slots and whitelist entries) - Environment variable rows, encrypted values included (same tenant, so the ciphertext stays valid)
- The copy is owned by the current user, is not shared by default, and gets a new UUID and slug
Not Copied:
- Shares (AppShare records)
- Tags (TagEntity records)
- The workspace schema and its data (recreated empty by the next migrate/deploy)
- Agents (AppAgent records — re-created on next deploy)
- Favorite status
- Edit history
- Tokens
Library Copy:
If the app has an embedded library (libraryId):
- A new library is created with the name
"{original name} (Copy)" - All files from the original library are copied to the new library
- The new app references the new library
- The copy is fully independent (editing one doesn't affect the other)
Error Responses:
400 Bad Request- Invalid payload or copy operation failed404 Not Found- App doesn't exist or user lacks access403 Forbidden- User lacks copy or create permission403 Forbidden- App is managed (isManaged: true); a copy would fork it out of its managed lifecycle
Transaction Safety:
The entire copy operation runs in a database transaction. If any step fails, the entire operation is rolled back.
POST /api/apps/{id}/_print
Export an app as a PDF.
Authentication: Required
Permissions Required: Member role (run permission)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
Payload:
Print configuration depends on the app driver. For report apps:
{
"format": "letter",
"orientation": "portrait",
"margins": {
"top": 0.5,
"right": 0.5,
"bottom": 0.5,
"left": 0.5
},
"headerTemplate": "<div>...</div>",
"footerTemplate": "<div>...</div>",
"displayHeaderFooter": true,
"printBackground": true
}
Common Payload Fields:
| Field | Type | Default | Description |
|---|---|---|---|
format | string | "letter" | Paper format (letter, legal, a4, etc.) |
orientation | string | "portrait" | Page orientation (portrait, landscape) |
margins | object | {} | Page margins in inches |
printBackground | boolean | true | Include background graphics |
Response:
Returns a PDF file as a binary stream.
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="Sales Dashboard.pdf"
[PDF binary data]
Use Cases:
- Export dashboards for offline viewing
- Generate reports for email distribution
- Archive app snapshots
- Print physical copies
Implementation Notes:
- Uses headless Chrome/Puppeteer for rendering
- Captures the current state of the app
- Respects app's CSS print styles
- May timeout for very complex apps
Error Responses:
404 Not Found- App doesn't exist or user lacks access403 Forbidden- User lacks run permission500 Internal Server Error- PDF generation failed
POST /api/apps/{id}/_unmanage
Release a CLI-deployed App back to origin: "ui", restoring builder editing.
Authentication: Required
Permissions Required:
- Admin role (
app:write) on the owning team, fortype: "app"
Deliberately gated on app:write rather than app:edit. app:edit resolves
to app:write && !isManaged, so gating on it would make this permission
unsatisfiable on exactly the Apps it exists to release.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | App UUID or natural ID |
No payload. Returns the updated App with origin: "ui" and
isManaged: false.
Why this exists: origin: "deployed" is otherwise a one-way door.
POST /_edit refuses managed Apps unconditionally (an entity invariant, not a
permission, so superusers are refused too), and the only other path that
resets origin is the draft-commit flow, which needs a draft, which needs
_edit. An App claimed by mistake would be un-editable permanently.
What releasing does not do: it does not sever the App from its source
project. The library still holds the deploy's files, package.json in the
project still carries the App's UUID, and the next npm run deploy re-claims
the App as managed and replaces its files. Releasing is for adopting an App
whose source project is gone, not for forking a live one; use
_copy for that.
Errors:
| Status | Condition |
|---|---|
400 Bad Request | App is already origin: "ui" ("App is not managed") |
400 Bad Request | App is origin: "marketplace" ("Marketplace apps cannot be released: uninstall the app instead") |
403 Forbidden | Caller lacks app:write on the owning team |
404 Not Found | App does not exist, or the caller has no read access to it |
Marketplace installs are refused because they stay bound to their pack through
pack_install.localAppId and keep receiving upgrades; releasing one would
leave an App the next sync overwrites. Uninstalling is the supported exit. The
refusal is a 400 rather than a 403 because it is a fact about the App's
kind, not about the caller.
That protection is not only this route's. A beforeUpdate hook on the App
model rejects any write that moves a row off origin: "marketplace", so it
holds for every write path, present and future, not just this endpoint. The
route's 400 is the clean answer for an API caller; the model hook is what
makes it true of the row.
HAL affordances: the App resource advertises inf:app-unmanage only when
origin is "deployed", and advertises edit only when the App is not
managed. Clients should drive off those rels rather than recomputing from
origin.
Other Actions
Export Bundle
Apps can be exported as bundles (not yet documented in routes).
Route: GET /api/apps/{id}/_export
Use Case: Export app and its library as a portable bundle for backup or transfer.
Cloud Publish
Apps can be published to cloud services (not yet documented in routes).
Route: POST /api/apps/{id}/_cloud-publish
Use Case: Publish app to external hosting or app marketplaces.
Common Action Patterns
Copy and Modify
// 1. Copy the app
const copy = await POST('/api/apps/analytics:sales-dashboard/_copy', {
name: 'Sales Dashboard - Modified'
});
// 2. Edit the copy
await POST(`/api/apps/${copy.id}/_edit`);
// 3. Modify files in the draft
await PUT(`/api/apps/${draftId}/contents/index.html`, {
content: modifiedHTML
});
// 4. Commit changes
await POST(`/api/apps/${copy.id}/draft/_commit`);
Create PDF Archive
// Generate PDF of current state
const pdf = await POST('/api/apps/analytics:sales-dashboard/_print', {
format: 'letter',
orientation: 'landscape',
printBackground: true
});
// Save to file or send via email
await fs.writeFile('dashboard-archive.pdf', pdf);
Copy to Different Owner
// 1. Copy the app (owned by current user)
const copy = await POST('/api/apps/analytics:sales-dashboard/_copy', {
name: 'Sales Dashboard - Marketing Copy'
});
// 2. Transfer ownership to Marketing team
await PUT(`/api/apps/${copy.id}/owner`, {
teamId: 'marketing'
});
Batch Copy
// Copy multiple apps
const sourceApps = [
'analytics:sales-dashboard',
'analytics:customer-report',
'analytics:inventory-tracker'
];
const copies = [];
for (const appId of sourceApps) {
const copy = await POST(`/api/apps/${appId}/_copy`, {
name: `${appId} (Backup ${new Date().toISOString()})`
});
copies.push(copy);
}
console.log(`Created ${copies.length} backups`);
Permissions Summary
| Action | Required Permission | Description |
|---|---|---|
| Copy | Member+ (copy) + Member (create) | Copy source app and create new app |
| Member (run) | Generate PDF from app | |
| Unmanage | Admin (write) | Release a deployed app back to origin: "ui" |
| Export Bundle | Member+ (write) | Export app as bundle |
| Cloud Publish | Publisher (share) | Publish to external services |