Versions & Lifecycle
Version lifecycle for decision apps: immutable published versions, content-hash publishes, enable and disable, plus health and dependents inspection endpoints.
App configuration is versioned like a Spec: edits accumulate in a single draft, publish freezes the draft forever, and every run pins exactly one version. GET /v1/apps/:id/versions lists every version with its status (draft, published, superseded), content_hash, and timestamps; GET /v1/apps/:id/versions/:n returns one version including its full content. Published rows never change — the ledger can therefore always name the exact logic that decided any historical run.
POST /v1/apps/:id/versions/:n/publish publishes the current draft (answering 200). The :n must be the draft's own number — publishing anything else is a 409 naming the actual draft. Publish re-validates the content, supersedes the previously published version, activates the new one, locks the slug on the first publish, and fires the app.version.activated webhook. It is idempotent by content: a draft whose content_hash equals the active version publishes as a no-op returning the active version.
In v1, publish activates: POST /v1/apps/:id/versions/:n/activate returns 200 when :n is already the active version and 501 otherwise — re-activating a superseded version (rollback) is not implemented yet; publish a new version with the old content instead. The pre-publish verdict diff exists precisely so you can see what a candidate changes before this one-way door.
POST /v1/apps/:id/enable (configure tier) and POST /v1/apps/:id/disable flip the run gate. Enabling requires a published version (409 before the first publish) and — when the active version declares acts (external side effects) — a human workspace owner: machine tokens and non-owner sessions get 403 Enabling an app with actions requires a workspace owner. Disabling is the kill switch and stays at plain operate with no extra ceremony: stopping an app must always be easier than starting it. A disabled app refuses live runs with 409 but still accepts dry runs.
/v1/apps/:id/health/v1/apps/:id/dependentscurl — publish, enable, inspect
curl -s -X POST https://api.talonic.com/v1/apps/$APP_ID/versions/2/publish \
-H "Authorization: Bearer tlnc_your_api_key"
# → { "version": 2, "status": "published", "content_hash": "aaed0a6d6e01f073..." }
curl -s -X POST https://api.talonic.com/v1/apps/$APP_ID/enable \
-H "Authorization: Bearer tlnc_your_api_key"
# → { "enabled": true }
curl -s https://api.talonic.com/v1/apps/$APP_ID/health \
-H "Authorization: Bearer tlnc_your_api_key"Response — GET /v1/apps/:id/health
{
"window_hours": 24,
"runs": {
"total": 12,
"by_status": { "completed": 11, "failed": 1 }
},
"reviews_open": 2,
"last_run_at": "2026-08-29T11:31:59.140Z"
}Version history composes with the ledger: every run records app_slug@vN, every sealed record names the deciding version, and GET /v1/runs/:runId/verdicts/summary returns the deciding versionId. When behavior changes between two runs, diff the two versions' content via GET /v1/apps/:id/versions/:n — the content hash tells you instantly whether the logic differed at all.
Presentation and pinning
How a workspace shows an app is not versioned content. PATCH /v1/apps/:id/presentation sets any of display_name (max 120 chars), display_icon (a lowercase icon name, or null to let the surface infer the mark again), and description (one sentence, max 2000 chars; null or blank clears it). It publishes no version, never touches the slug — MCP tool names derive from it — and is stored per workspace, so two workspaces running the same standard app can name it differently. POST /v1/apps/:id/pin and POST /v1/apps/:id/unpin add the app to or remove it from the workspace's sidebar rail; nav_pinned_at records the pin order. All three are configure-tier display choices: pinning a disabled app is legal and leaves its lifecycle untouched.
curl — rename and pin
curl -s -X PATCH https://api.talonic.com/v1/apps/$APP_ID/presentation \
-H "Authorization: Bearer tlnc_your_api_key" -H "Content-Type: application/json" \
-d '{"display_name": "PO Match", "display_icon": "receipt"}'
# → { "display_name": "PO Match", "display_icon": "receipt", "description": "Holds supplier invoices that drift from their purchase order." }
curl -s -X POST https://api.talonic.com/v1/apps/$APP_ID/pin \
-H "Authorization: Bearer tlnc_your_api_key"
# → { "nav_pinned": true, "nav_pinned_at": "2026-10-06T09:12:44.120Z" }The trigger floor
An app subscribed to data product publications is triggered when a contributing pipeline completes, and a background sweep re-dispatches completions that were missed. The trigger floor (trigger_floor_at on the manifest) bounds that: a pipeline that completed at or before the floor is never dispatched to the app, on the live path or by the sweep. The first enable stamps the floor automatically, so a newly subscribed app does not replay its whole history as "missed" work; a later re-enable keeps it. POST /v1/apps/:id/trigger-floor (operate tier, beside the kill switch) moves it: an empty body sets it to now — stopping a replay in flight while live completions keep triggering — {"at": "<ISO-8601 instant with timezone>"} places it, and {"at": null} clears it so the sweep may replay its configured window.
curl — stop a replay of past pipelines
curl -s -X POST https://api.talonic.com/v1/apps/$APP_ID/trigger-floor \
-H "Authorization: Bearer tlnc_your_api_key" -H "Content-Type: application/json" -d '{}'
# → { "trigger_floor_at": "2026-10-06T09:20:03.511Z" }