Skip to main content

Create an App

Create a decision app with POST /v1/apps, or draft one from a plain-language description via POST /v1/apps/proposals: name, mode, rules, and access hints.

POST /v1/apps creates an app shell: a workspace-unique slug, a display_name, a mode (rules, assisted, or external), and optionally a one-sentence description (up to 2000 chars) that the gallery card shows. The response is the app's manifest with version: null — nothing decides yet. You then save logic as a draft version (PUT /v1/apps/:id/logic), publish it, and enable the app. The slug follows the grammar [a-z0-9][a-z0-9-]{1,62}[a-z0-9] (3–64 chars) and becomes immutable at the first publish; display_name renames freely at any time.

The describe-first path is POST /v1/apps/proposals: send a plain-language description (20–2000 chars) of the decision you want automated, and the platform compiles it into a structured proposal — suggested name and slug, a mode, the policy restated as rule sentences with parameters, a drafted output contract, trigger defaults, the inputs it thinks the app needs, access_needed hints (which data products or reference tables to grant), and open_questions where your description was ambiguous. Nothing persists: the proposal is a draft on the wire, and the client turns an accepted proposal into a real app via POST /v1/apps plus PUT /:id/logic.

Requests to either endpoint need the configure tier (a workspace owner session; an operate grant or legacy write scope for keys). A duplicate slug is rejected with 409 and the message naming the taken slug, so creation is safe to retry with a new slug. Build on rules, assisted, or external: the schema also knows a fourth mode, autonomous, but its content validation admits only one narrow platform-defined logic profile, so a draft or publish declaring autonomous with any other logic is rejected with 422.

Deletion is the reverse ceremony, deliberately heavier: DELETE /v1/apps/:id requires the owner tier and is refused while the app is enabled or while other apps consume its output as an input binding (check GET /v1/apps/:id/dependents first). Deleting removes configuration only — the run journal and sealed decision records survive by design, carrying no foreign key to the app.

POST/v1/apps

Body

slug*stringWorkspace-unique, lowercase letters/digits/hyphens, 3-64 chars. Immutable after the first publish. Duplicate → 409.
display_name*stringHuman name, max 200 chars. Renames freely.
mode*stringrules, assisted, or external. Changeable later by saving and publishing a draft with a different mode.
descriptionstringOptional. One sentence about the app, max 2000 chars; shown on the gallery card and in the manifest. Change it later with PATCH /v1/apps/:id/presentation.

curl

curl -s -X POST https://api.talonic.com/v1/apps \
  -H "Authorization: Bearer tlnc_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "invoice-approval",
    "display_name": "Invoice approval",
    "mode": "rules",
    "description": "Holds supplier invoices that drift from their purchase order."
  }'

Response (201)

{
  "app_id": "app_8f591d2d85c5990a",
  "id": "80afca3e-c66c-4f07-abbf-78913ee80dff",
  "slug": "invoice-approval",
  "display_name": "Invoice approval",
  "description": "Holds supplier invoices that drift from their purchase order.",
  "version": null,
  "content_hash": null,
  "mode": "rules",
  "enabled": false,
  "contract": null,
  "output_contract": null,
  "triggers": null,
  "thresholds": null,
  "fallback": null,
  "display": null,
  "endpoints": {
    "mcp_tool": "app_invoice_approval",
    "rest": "/v1/apps/80afca3e-c66c-4f07-abbf-78913ee80dff"
  }
}
POST/v1/apps/proposals

Body

description*stringPlain-language description of the decision to automate, 20-2000 chars.

Response (201) — proposal excerpt

{
  "proposal": {
    "display_name": "Invoice PO Check",
    "slug": "invoice-po-check",
    "mode": "rules",
    "summary": "The app checks every supplier invoice against its purchase order and holds any invoice where the billed amount differs from the purchase order amount by more than 2 percent.",
    "policy_sentences": [
      "IF the billed amount on a supplier invoice differs from its purchase order amount by more than 2 percent THEN hold the invoice."
    ],
    "rules": [
      {
        "sentence": "IF the billed amount on a supplier invoice differs from its purchase order amount by more than 2 percent THEN hold the invoice.",
        "applies_to": null,
        "params": { "threshold": 0.02 }
      }
    ],
    "fallback": "hold",
    "output_contract": {
      "type": "object",
      "required": ["decision"],
      "properties": { "decision": { "enum": ["approve", "hold"] }, "note": { "type": "string" } }
    },
    "triggers": { "manual": true, "api": true },
    "inputs_needed": ["supplier invoices", "purchase orders"],
    "access_needed": [
      { "resource_type": "data_product", "hint": "supplier invoices" },
      { "resource_type": "reference_table", "hint": "purchase orders" }
    ],
    "open_questions": [
      "How is the percentage difference calculated (relative to the billed or the purchase order amount)?",
      "How does a purchase order match to an invoice (by PO number, supplier, or line)?"
    ]
  }
}
The proposal's access_needed hints map onto resource grants: resolve each hint against your workspace via GET /v1/apps/pickable-resources, then create the grants after the app exists. An app arrives runnable only once it can actually read its inputs.

Answer the open_questions before you publish — they are exactly the ambiguities that would otherwise surface later as indeterminate card results or unexpected holds. Feed the answers back either by refining the rule sentences before compiling, or as guidance on POST /v1/apps/:id/logic/compile.

Export, import, and duplicate

Apps are portable. GET /v1/apps/:id/export returns an environment-free manifest of the app's active version: export_version: 1, the app's slug, display_name, and mode, the version number, and the published content unmodified — no grants, keys, install state, or endpoints, so nothing in it is tied to one workspace. POST /v1/apps/import turns such a document into a new app (it creates the app and saves the content as its draft, with mode defaulting from the content), and POST /v1/apps/:id/duplicate does both in one step inside a workspace, defaulting the copy's slug to <slug>-copy and its name to <display_name> (copy). All three need the configure tier; export and duplicate answer 409 while the app has no published version, and every copy arrives as an unpublished draft whose grants you set again.

curl — move an app between workspaces

# Export from the source workspace
curl -s https://api.talonic.com/v1/apps/$APP_ID/export \
  -H "Authorization: Bearer $SOURCE_KEY" > invoice-approval.json
# → { "export_version": 1, "app": { "slug": "invoice-approval", "display_name": "Invoice approval",
#     "mode": "rules" }, "version": 3, "content": { ... } }

# Import into the target workspace (body: slug, display_name, content, export_version; mode optional)
jq '{ slug: .app.slug, display_name: .app.display_name, content, export_version }' invoice-approval.json \
  | curl -s -X POST https://api.talonic.com/v1/apps/import \
      -H "Authorization: Bearer $TARGET_KEY" -H "Content-Type: application/json" -d @-
# → { "app": { ...manifest, "version": null }, "draft": { "version": 1, "status": "draft", "content_hash": "…" } }

# Or copy within one workspace
curl -s -X POST https://api.talonic.com/v1/apps/$APP_ID/duplicate \
  -H "Authorization: Bearer tlnc_your_api_key" -H "Content-Type: application/json" \
  -d '{"slug": "invoice-approval-strict"}'

Frequently asked questions

Does POST /v1/apps/proposals create anything?+
No. The proposal is stateless: nothing exists server-side until you POST /v1/apps and save the drafted logic. You can call it repeatedly with refined descriptions, compare proposals, and only then create the app.
Can I change an app's mode after creation?+
Yes. Mode lives in the versioned content: save a draft whose content.mode differs (rules ⇄ assisted, or external), publish, and the app switches. Contract, output, and ledger are untouched — switching to external only changes who produces the decision.
Why is my delete rejected with 409?+
Deletion requires the app to be disabled and to have no dependents. Disable it first (POST /v1/apps/:id/disable), and check GET /v1/apps/:id/dependents — another app binding this app's output blocks deletion until that binding is removed.
Do grants travel with an exported or duplicated app?+
No. Export and duplicate carry the published content only — no grants, client keys, or install state. The copy arrives as an unpublished draft; grant it read access to its inputs in the target workspace, publish, and enable it there.
What happens to run history when I delete an app?+
It survives. The run journal and sealed decision records carry no foreign key to the app row, so every past decision stays readable through /v1/runs/:runId (by id) and auditable forever. Deleting removes only the configuration and versions.