Skip to main content

Logic & Rule Compilation

Read and write an app's versioned logic manifest, compile plain-language rule cards against bound fields, and edit apps with natural-language proposals.

An app's behavior lives in its versioned content manifest — one JSON document per version carrying the mode, the logic, the input bindings, the output contract, thresholds, triggers, fallback policy, and presentation vocabulary. GET /v1/apps/:id/logic returns both sides at once: draft (the editable next version, or null) and active (the published version runs execute, or null). PUT /v1/apps/:id/logic — and its alias PATCH /v1/apps/:id — writes the whole manifest as the draft, creating version head + 1 if no draft exists yet. Drafts change nothing live until [published](app-versions).

Manifest content fields

mode*stringrules | assisted | external. Must match the logic kind: rules/assisted require logic.kind "rules", external requires "external" (422 otherwise).
logic*objectFor rules/assisted: { kind: "rules", cards: [...], adjudication? }. For external: { kind: "external", decisionSlaSeconds, leaseSeconds, allowUnevidenced }.
input_bindings*arrayNamed sources a run reads: { alias, source: { kind: published_data_product | reference_table | source_connection | app_output, ... }, select? }. Each bound source also needs a read grant.
output_contract*objectJSON Schema (2020-12) every decision must validate against, whoever produces it.
decision_policyobject{ kind: "single" } (default when absent) folds the run into one decision; { kind: "batch_verdicts", subject: { binding, group_by, doc_type_field? }, substantive_rules, min_substantive_passes } gives every subject group its own verdict row.
displayobjectPresentation vocabulary: subject_label plus metrics [{ label, binding, field, fn: sum|count|avg|min|max }]. Validated against the version's own binding aliases at save and publish.
acts*string[]Actions the app may execute, resolved through resource grants of type "action". Empty for decide-only apps.
thresholds*objectauto_execute_above_confidence, amount_requires_human_above { field, amount }, max_runs_per_hour. Breaches route to review, never silent failure.
triggers*object{ manual, api, data_product_publications? [] } — which trigger paths may start a run.
fallback*stringrules | hold | none — what happens when an external decision never arrives or fails.
credit_cost*numberCredits charged per run (default 1), idempotently keyed on the run id.

Rules apps carry their policy as cards: each card holds the author's originalPolicy sentence plus a structured interpretation the engine executes. You rarely write interpretations by hand. Save cards with a pending_compile marker (the sentence only), then call POST /v1/apps/:id/logic/compile — the compiler grounds each pending sentence in the app's bound fields and writes executable scope/failure predicates back into the draft. A sentence that cannot be grounded stays pending, with the reason in warnings and on the card itself, so an ungrounded rule can never silently decide.

POST/v1/apps/:id/logic/compile

Body

card_idsstring[]Compile only these cards. Omitted → every pending card.
guidancestringAuthor guidance for the compiler (max 1000 chars), e.g. which field a rule's words map to.
localestringUI locale (max 10 chars); compiler reasons and readbacks come back in this language.
GET/v1/apps/:id/logic/fields

curl — save a draft with a pending rule, then compile it

curl -s -X PUT https://api.talonic.com/v1/apps/$APP_ID/logic \
  -H "Authorization: Bearer tlnc_your_api_key" \
  -H "Content-Type: application/json" \
  -d @manifest.json
# → { "version": 2, "status": "draft", "content_hash": "9df5a562cff6d2df..." }

curl -s -X POST https://api.talonic.com/v1/apps/$APP_ID/logic/compile \
  -H "Authorization: Bearer tlnc_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"guidance": "\"billed amount\" is the total_amount field on the loads binding"}'
# → { "cards": [ ...interpretations written... ], "warnings": [] }

Natural-language editing

Existing apps are edited the same way they are drafted: in language. POST /v1/apps/:id/edit-proposals takes an instruction (8–2000 chars, e.g. "Raise the quantity tolerance from 100 to 150 units") and returns typed edit ops plus a full preview of the manifest after the change, a plain-language summary, open_questions, and warnings. Nothing persists at this step. The person reviews the ops and applies them explicitly with POST /v1/apps/:id/edit-proposals/apply — a deterministic step that re-validates the ops against the live draft, saves it, and compiles the touched cards.

Edit proposal — request and response excerpt

POST /v1/apps/:id/edit-proposals
{ "instruction": "Raise the quantity tolerance from 100 to 150 units." }

{
  "ops": [
    { "op": "edit_rule", "card_id": "quantity-tolerance",
      "sentence": "Hold any load whose quantity exceeds 150 units." }
  ],
  "summary": "The quantity tolerance threshold on the \"quantity-tolerance\" rule has been raised from 100 to 150 units. All other rules and settings remain unchanged.",
  "open_questions": [],
  "warnings": [],
  "preview": { "mode": "rules", "logic": { "kind": "rules", "cards": [ ... ] }, ... }
}

The op vocabulary is closed and reviewable: add_rule, edit_rule, remove_rule, pause_rule, resume_rule, set_fallback, set_adjudication, and set_mode (rules ⇄ assisted). Because apply is a separate, deterministic call taking the exact ops you accepted, an agent can propose and a human can approve — or the same caller can chain both — without an LLM ever writing directly into the draft.

Manifest validation is strict and runs at save AND at publish: the logic kind must match the mode, output_contract must be a JSON Schema object, display metrics must name real binding aliases and a known aggregate, and adjudication's domain_hint is bounded. A draft written by an older client can therefore fail at publish even though it saved.

Frequently asked questions

What is a "pending" card?+
A card whose interpretation carries the unsupported code pending_compile: it has an author sentence but no executable predicates yet. Pending cards never decide anything. Compile grounds them in the app's bound fields; a card that cannot be grounded stays pending with the reason attached.
Can the compiler invent a field that is not in my data?+
No. The compiler is restricted to the alias → field inventory returned by GET /v1/apps/:id/logic/fields, derived from the version's input bindings. A rule naming a field outside that inventory stays pending with a warning instead of being grounded against a guess.
What is the difference between PATCH /v1/apps/:id and PUT /v1/apps/:id/logic?+
Nothing — they are aliases. Both take { content: <full manifest> }, validate it, and write it as the app's draft version. Use whichever reads better in your client.
How do rules apply to only some records?+
Two mechanisms: a card-level applies_to { field, equals } engages the card only for records whose field matches (e.g. document type "BOL"), and each input binding's select can narrow fields and rows at assembly time. Batch apps additionally scope card matching via decision_policy.subject.doc_type_field.
What does adjudication do?+
It governs fuzzy comparisons (op "similar"): pairs at or above the threshold match deterministically, and only below-threshold pairs go to the metered LLM adjudicator — and only when enabled: true. Disabled means below-threshold comparisons stay indeterminate rather than guessed. domain_hint is one sentence of your domain context interpolated into the otherwise neutral prompt.