Skip to main content

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.

GET/v1/apps/:id/health
GET/v1/apps/:id/dependents

curl — 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"
}
Publishing and activating fire one webhook, app.version.activated, carrying app_id, version, and content_hash — the push-side twin of the catalog ETag. Agents holding a cached tool schema for the app should re-read GET /v1/apps on this event.

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" }

Frequently asked questions

Why did publish return the previous version untouched?+
The draft's content hash matched the active version, so publish no-opped — this is the idempotent content-hash publish. Byte-identical logic never mints a new version, which keeps version numbers meaningful and lets clients retry publish safely.
How do I roll back to an earlier version?+
Direct re-activation of a superseded version returns 501 in v1. Fetch the old version's content via GET /v1/apps/:id/versions/:n, save it as the draft with PUT /v1/apps/:id/logic, and publish. The content hash will match the old version's, and the ledger stays linear.
Why can't my API key enable the app?+
The active version declares acts — actions with external side effects — and enabling such an app is an owner decision made by a human session. Keys and non-owner roles get 403. Apps with an empty acts list enable at the ordinary configure tier — an owner session, or a key with an operate grant.
Why did my newly enabled app not process last month's pipelines?+
The first enable stamps the trigger floor at that moment, so completions from before it are never dispatched to the app. That keeps a new subscriber from replaying its whole history. If you do want the backlog, clear the floor with POST /v1/apps/:id/trigger-floor and {"at": null}, or place it at the instant you want to start from.
What happens to in-flight runs when I disable an app?+
Disable flips the gate for new runs (409 on trigger); it does not cancel work already executing. Because rules and assisted runs complete synchronously within the trigger call, in practice the gate is the whole story for those modes; external runs already parked at awaiting_decision remain until decided or expired.