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, and a mode (rules, assisted, or external). 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](app-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 operate tier (senior_member and up for sessions; 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. Mode autonomous is reserved for a later release and rejected with 422 wherever content is validated.

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](app-versions) 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.

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": "load-auto-billing",
    "display_name": "Load auto-billing",
    "mode": "rules"
  }'

Response (201)

{
  "app_id": "app_8f591d2d85c5990a",
  "id": "80afca3e-c66c-4f07-abbf-78913ee80dff",
  "slug": "load-auto-billing",
  "display_name": "Load auto-billing",
  "description": "Load auto-billing",
  "version": null,
  "content_hash": null,
  "mode": "rules",
  "enabled": false,
  "contract": null,
  "output_contract": null,
  "triggers": null,
  "thresholds": null,
  "fallback": null,
  "display": null,
  "endpoints": {
    "mcp": "apps/load-auto-billing",
    "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": "Rate Card Billing Validator",
    "slug": "rate-card-billing-validator",
    "mode": "rules",
    "summary": "The app checks every delivered load against its rate card and holds any load where the billed amount differs from the rate card amount by more than 2 percent.",
    "policy_sentences": [
      "IF the billed amount for a delivered load differs from the rate card amount by more than 2 percent THEN hold the load."
    ],
    "rules": [
      {
        "sentence": "IF the billed amount for a delivered load differs from the rate card amount by more than 2 percent THEN hold the load.",
        "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": ["delivered loads", "rate card"],
    "access_needed": [
      { "resource_type": "data_product", "hint": "delivered loads" },
      { "resource_type": "reference_table", "hint": "rate card" }
    ],
    "open_questions": [
      "How is the percentage difference calculated (relative to billed or rate card amount)?",
      "How does the rate card match to a delivered load (by route, carrier, load type)?"
    ]
  }
}
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](app-logic).

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.
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.