Skip to main content

Apps Overview

The Talonic Apps surface: named, versioned decision apps over governed data, with an ETag-cached catalog, dual auth heads, and a read/run/operate/owner ladder.

Apps are named, versioned, publishable decision units. An app reads a governed set of inputs — published data products, reference tables, connected databases, or other apps — and produces schema-typed decisions with a replayable, provenance-carrying ledger: an append-only run journal, a queryable run projection, and an immutable sealed decision record per run. Every app runs in one of three autonomy modes: rules (deterministic policy cards decide), assisted (rules decide, a draft-only agent proposes review resolutions), or external (an outside agent decides inside Talonic's contract and ledger).

The REST surface spans six route families: /v1/apps (lifecycle, logic, versions, acceptance, grants), /v1/runs (run projection, verdict matrix, journal, sealed record), /v1/replays, /v1/reviews (the Human Review inbox), /v1/rule-candidates, and /v1/review-teams. The Talonic web UI is a client of exactly these endpoints — it holds no private capabilities — and every enabled app is additionally published as a dynamic MCP tool (apps/<slug>), so an external agent given only a token discovers and operates the same surface a person sees.

Authentication is dual-headed: tlnc_ API keys and OAuth tokens on one head, platform session JWTs on the other, both landing on the same authorization layer. Authorization is a four-tier ladder — read < run < operate < owner — mapped for humans from the workspace role (viewer → read, member → run, senior_member → operate, owner → owner). Machine callers are decided by per-app grants: a read grant admits reads, an operate grant admits read/run/operate, and owner-tier routes (grant management, deletion, pickable clients) always require a human workspace owner — they are never machine-grantable.

A compatibility rule keeps pre-Apps keys working: a key with zero app-grant rows falls back to its legacy scopes — read scope for read-tier routes, write for run/operate. The moment the first grant row lands on a key, that key switches to strict per-app enforcement: it can only touch apps it holds grants for, at the granted level. OAuth clients and routes without an app id in the path (the workspace review inbox, /v1/runs/:runId) always use the scope path, with every read tenant-scoped by the service.

The whole surface requires a concrete workspace. A key or session resolving to the cross-tenant master view is rejected with 403 and the message A concrete workspace is required for the Apps surface — there are no cross-tenant app reads or writes on /v1.

Granting a key its first per-app grant is a behavioral switch, not just an addition: the key immediately loses legacy scope-based access to every other app in the workspace. Grant deliberately — start with read, widen to operate per app.
GET/v1/apps

Query parameters & headers

enabledstringPass enabled=true to return only enabled apps (the set exposed as dynamic MCP tools).
If-None-MatchheaderThe etag from a previous response. When the catalog is unchanged the endpoint answers 304 Not Modified with an empty body.

The response carries an etag both as an ETag header and in the body: a hash across the visible set's id:content_hash:enabled triples. It changes exactly when an app is created, deleted, enabled, disabled, or gets a new active version — which makes it the cache key for tool catalogs. MCP clients re-list tools when it moves; your integration can poll GET /v1/apps with If-None-Match cheaply and rebuild only on 200.

curl

curl -s https://api.talonic.com/v1/apps \
  -H "Authorization: Bearer tlnc_your_api_key"

# Conditional re-fetch: 304 when nothing changed
curl -s -o /dev/null -w "%{http_code}" https://api.talonic.com/v1/apps \
  -H "Authorization: Bearer tlnc_your_api_key" \
  -H 'If-None-Match: "e3b0c44298fc1c149afbf4c8996fb924"'

Response (200)

{
  "apps": [
    {
      "app_id": "app_8f591d2d85c5990a",
      "id": "80afca3e-c66c-4f07-abbf-78913ee80dff",
      "slug": "load-auto-billing",
      "display_name": "Load auto-billing",
      "description": "Rules app: Load auto-billing",
      "version": 1,
      "content_hash": "9df5a562cff6d2dfda738f4bf8f88b0b...",
      "mode": "rules",
      "enabled": true,
      "created_at": "2026-08-29T11:31:36.698Z",
      "updated_at": "2026-08-29T11:32:32.724Z",
      "contract": { "reads": [], "acts": [] },
      "output_contract": {
        "type": "object",
        "required": ["decision"],
        "properties": { "decision": { "enum": ["approve", "hold"] } }
      },
      "input_schema": {
        "type": "object",
        "properties": {
          "input": { "type": "object" },
          "idempotency_key": { "type": "string", "maxLength": 64 },
          "dry_run": { "type": "boolean" }
        }
      },
      "triggers": { "api": true, "manual": true },
      "thresholds": { "auto_execute_above_confidence": 0.9 },
      "fallback": "hold",
      "display": null,
      "endpoints": {
        "mcp": "apps/load-auto-billing",
        "rest": "/v1/apps/80afca3e-c66c-4f07-abbf-78913ee80dff"
      }
    }
  ],
  "etag": "\"e51b28d3552a4f39a4c88b1e02e7a3d1\""
}
GET/v1/apps/:id
The manifest is the app as humans and agents both see it. contract.reads lists the input binding aliases of the active version, contract.acts the actions the app may execute, and display carries the app's own presentation vocabulary (subject label, headline metrics) so every surface — UI, MCP, your integration — speaks the app's words.

Webhook events

Apps emit workspace webhooks through the standard HMAC-signed delivery pipeline: app.run.completed and app.run.failed when a triggered run reaches a terminal state, app.review.raised and app.review.resolved around Human Reviews, app.version.activated when a publish lands, app.action.execute when a decision executes an action (the payload carries the decision, its evidence locators, rationale, and the action's idempotency key), and app.exception.raised for system-raised exceptions. Subscribe via [POST /v1/webhooks](create-webhook-config) exactly as for document events.

Frequently asked questions

What is the difference between the read, run, operate, and owner tiers?+
read covers every GET (manifests, runs, ledger, reviews, health). run adds triggering runs. operate adds configuration: drafts, publish, enable/disable, resolving reviews, replays, raising reviews. owner covers grant and client management, review-team writes, and app deletion — and is always a human workspace owner, never an API key.
My existing workspace key worked yesterday and gets 403 today. What changed?+
Most likely someone created the key's first per-app grant. A key with zero grants falls back to legacy read/write scopes across all apps; the first grant row flips it to strict per-app enforcement, so it now needs an explicit grant on each app it touches. Either grant it the apps it needs or remove its grants to restore legacy behavior.
How do I know when the app catalog changed?+
Use the etag. GET /v1/apps returns an ETag header (also echoed in the body) hashed over each visible app's id, active content hash, and enabled flag. Re-poll with If-None-Match: a 304 means nothing changed; a 200 means an app was created, deleted, enabled, disabled, or republished — rebuild your cached catalog or MCP tool list then.
Why do I get 403 "A concrete workspace is required"?+
Your credential resolves to the cross-tenant master view (customerId "all"), which the Apps surface rejects for reads and writes alike. Call with a key or session bound to one concrete workspace.
Can an agent manage grants over the API?+
No. Every owner-tier route — POST/DELETE grants, POST/DELETE clients, review-team writes, app deletion, pickable-clients — requires a human session with the owner role. Machine callers receive 403 with "Grant and client management requires a human workspace owner."