Skip to content

DocsGet started

12 min read · updated

Call it from your code

Ask decisio from curl, Python, JavaScript or TypeScript, or the framework you already use. Every way sends the same JSON to one route.

Call decisio from any language with one HTTP request, and get a probability for every option back. Each script below sends the support ticket from Get started and prints each question’s likeliest option.

curl

curl http://127.0.0.1:8000/v1/systemone \
-H 'Content-Type: application/json' \
-d '{
"state": "I was charged twice for October. Both charges are 49.00 EUR, same day. Please refund one.",
"questions": {
"urgent": {
"type": "noul",
"instructions": "Does this ticket need a response within the hour?"
},
"queue": {
"type": "choice",
"instructions": "Which team should handle this ticket?",
"criteria": {
"billing": "Invoices, payments, refunds, prices, VAT",
"access": "Login, passwords, two-factor, SSO, permissions, invitations, account security",
"bug": "The product behaves wrongly: errors, crashes, wrong numbers, lost data",
"feature": "A request for something the product does not do yet",
"cancellation": "Cancelling, pausing or closing an account, deleting data",
"other": "Press, partners, jobs, sales pitches, wrong address, thanks, general questions"
}
},
"impact": {
"type": "score",
"instructions": "Rate the business impact using only the reported facts.",
"criteria": [
"No function impaired",
"One user impaired, with a workaround",
"Many users blocked from a core function",
"Data loss or legal exposure"
]
}
}
}'

Run against the CPU stand-in at v0.35.0 on 2026-10-26: the answers are the stand-in's, not the default model's.

Python

Python’s standard library is all it needs.

import json
import urllib.request
body = {
"state": "I was charged twice for October. Both charges are 49.00 EUR, same day. Please refund one.",
"questions": {
"urgent": {
"type": "noul",
"instructions": "Does this ticket need a response within the hour?"
},
"queue": {
"type": "choice",
"instructions": "Which team should handle this ticket?",
"criteria": {
"billing": "Invoices, payments, refunds, prices, VAT",
"access": "Login, passwords, two-factor, SSO, permissions, invitations, account security",
"bug": "The product behaves wrongly: errors, crashes, wrong numbers, lost data",
"feature": "A request for something the product does not do yet",
"cancellation": "Cancelling, pausing or closing an account, deleting data",
"other": "Press, partners, jobs, sales pitches, wrong address, thanks, general questions"
}
},
"impact": {
"type": "score",
"instructions": "Rate the business impact using only the reported facts.",
"criteria": [
"No function impaired",
"One user impaired, with a workaround",
"Many users blocked from a core function",
"Data loss or legal exposure"
]
}
}
}
request = urllib.request.Request(
"http://127.0.0.1:8000/v1/systemone",
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request) as response:
answers = json.load(response)["answers"]
for name, answer in answers.items():
if answer["type"] == "noul":
print(name, round(answer["noul"], 3))
else:
top = max(answer["probabilities"], key=answer["probabilities"].get)
print(name, top, round(answer["probabilities"][top], 3))
# urgent 0.159
# queue billing 0.962
# impact 1 0.648

Run against the CPU stand-in at v0.35.0 on 2026-10-26: the answers are the stand-in's, not the default model's.

TypeSafe SDK

Install TypeSafe’s SDK on its own: pip install typesafe-sdk==0.7.2, the version checked. It needs a key; any string works.

from typesafe_sdk import TypeSafeClient
client = TypeSafeClient(
base_url="http://127.0.0.1:8000",
# the SDK requires one; the server ignores it
api_key="unused",
# the name GET /v1/models lists
model="decisio-gemma-4-31b-it-letters",
)
body = {
"state": "I was charged twice for October. Both charges are 49.00 EUR, same day. Please refund one.",
"questions": {
"urgent": {
"type": "noul",
"instructions": "Does this ticket need a response within the hour?"
},
"queue": {
"type": "choice",
"instructions": "Which team should handle this ticket?",
"criteria": {
"billing": "Invoices, payments, refunds, prices, VAT",
"access": "Login, passwords, two-factor, SSO, permissions, invitations, account security",
"bug": "The product behaves wrongly: errors, crashes, wrong numbers, lost data",
"feature": "A request for something the product does not do yet",
"cancellation": "Cancelling, pausing or closing an account, deleting data",
"other": "Press, partners, jobs, sales pitches, wrong address, thanks, general questions"
}
},
"impact": {
"type": "score",
"instructions": "Rate the business impact using only the reported facts.",
"criteria": [
"No function impaired",
"One user impaired, with a workaround",
"Many users blocked from a core function",
"Data loss or legal exposure"
]
}
}
}
response = client.system_one(state=body["state"], questions=body["questions"])
for name, answer in response.nouls.items():
print(name, round(answer.noul, 3))
for name, answer in response.choices.items():
print(name, answer.choice, round(answer.probabilities[answer.choice], 3))
for name, answer in response.scores.items():
top = max(answer.probabilities, key=answer.probabilities.get)
print(name, top, round(answer.probabilities[top], 3))
# The server's name for the model, and the tokens it counted:
print(response.model, response.usage.input_tokens)

Run against the CPU stand-in at v0.35.0 on 2026-10-26: with TypeSafe's Python SDK 0.7.2, the version the page names. Its comments are shown above their commands.

JavaScript

Save it as ask.mjs and run node ask.mjs (Node 22 or later; no package).

const body = {
"state": "I was charged twice for October. Both charges are 49.00 EUR, same day. Please refund one.",
"questions": {
"urgent": {
"type": "noul",
"instructions": "Does this ticket need a response within the hour?"
},
"queue": {
"type": "choice",
"instructions": "Which team should handle this ticket?",
"criteria": {
"billing": "Invoices, payments, refunds, prices, VAT",
"access": "Login, passwords, two-factor, SSO, permissions, invitations, account security",
"bug": "The product behaves wrongly: errors, crashes, wrong numbers, lost data",
"feature": "A request for something the product does not do yet",
"cancellation": "Cancelling, pausing or closing an account, deleting data",
"other": "Press, partners, jobs, sales pitches, wrong address, thanks, general questions"
}
},
"impact": {
"type": "score",
"instructions": "Rate the business impact using only the reported facts.",
"criteria": [
"No function impaired",
"One user impaired, with a workaround",
"Many users blocked from a core function",
"Data loss or legal exposure"
]
}
}
};
const response = await fetch("http://127.0.0.1:8000/v1/systemone", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
const { answers } = await response.json();
for (const [name, answer] of Object.entries(answers)) {
if (answer.type === "noul") {
console.log(name, Number(answer.noul.toFixed(3)));
} else {
const [top, p] = Object.entries(answer.probabilities).sort((a, b) => b[1] - a[1])[0];
console.log(name, top, Number(p.toFixed(3)));
}
}
// urgent 0.159
// queue billing 0.962
// impact 1 0.648

Run against the CPU stand-in at v0.35.0 on 2026-10-26: the answers are the stand-in's, not the default model's.

TypeScript

Save it as ask.ts and run node --experimental-strip-types ask.ts (Node 22.6 or later; no package).

const body: { state: string; questions: Record<string, unknown> } = {
"state": "I was charged twice for October. Both charges are 49.00 EUR, same day. Please refund one.",
"questions": {
"urgent": {
"type": "noul",
"instructions": "Does this ticket need a response within the hour?"
},
"queue": {
"type": "choice",
"instructions": "Which team should handle this ticket?",
"criteria": {
"billing": "Invoices, payments, refunds, prices, VAT",
"access": "Login, passwords, two-factor, SSO, permissions, invitations, account security",
"bug": "The product behaves wrongly: errors, crashes, wrong numbers, lost data",
"feature": "A request for something the product does not do yet",
"cancellation": "Cancelling, pausing or closing an account, deleting data",
"other": "Press, partners, jobs, sales pitches, wrong address, thanks, general questions"
}
},
"impact": {
"type": "score",
"instructions": "Rate the business impact using only the reported facts.",
"criteria": [
"No function impaired",
"One user impaired, with a workaround",
"Many users blocked from a core function",
"Data loss or legal exposure"
]
}
}
};
const response = await fetch("http://127.0.0.1:8000/v1/systemone", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
type Answer = { type: string; noul?: number; probabilities?: Record<string, number> };
const { answers } = (await response.json()) as { answers: Record<string, Answer> };
for (const [name, answer] of Object.entries(answers)) {
if (answer.type === "noul") {
console.log(name, Number(answer.noul!.toFixed(3)));
} else {
const [top, p] = Object.entries(answer.probabilities!).sort((a, b) => b[1] - a[1])[0];
console.log(name, top, Number(p.toFixed(3)));
}
}
// urgent 0.159
// queue billing 0.962
// impact 1 0.648

Run against the CPU stand-in at v0.35.0 on 2026-10-26: the answers are the stand-in's, not the default model's.

In the Python, JavaScript and TypeScript tabs, the comment lines are what the default model answered.

Recorded decision: Gemma 4 31B, decisio 0.17.0, RTX PRO 6000 at 600 W, 2026-10-19, on the plain layout; record 1 of 60 of decisio-examples' support-routing run, sent again on that release; the printed probabilities are rounded to three places.

  • The server has no authentication: it accepts the Authorization header and any API key, and ignores them.
  • Run it on 127.0.0.1, or behind a reverse proxy that authenticates the caller.
  • Call it from your backend, not from a web page: the server sets no CORS headers, so a browser cannot call it.

Every error comes back with a detail that names the problem.

StatusWhat it means
422the server cannot read the request: a missing field, more than 255 options, a text longer than the context
400the model cannot answer this request
415the body is not sent as Content-Type: application/json

With the standard library, an error is an HTTPError whose body holds the detail:

import json
import urllib.error
import urllib.request
request = urllib.request.Request(
"http://127.0.0.1:8000/v1/systemone",
data=json.dumps({"state": "No questions asked.", "questions": {}}).encode(),
headers={"Content-Type": "application/json"},
)
try:
urllib.request.urlopen(request)
except urllib.error.HTTPError as error:
print(error.code, "detail" in json.load(error))
# 422 True

Run against the CPU stand-in at v0.35.0 on 2026-10-26.

  • In JavaScript, check response.ok before reading the answer, as the script does.
  • TypeSafe’s SDK raises TypeSafeUnprocessableEntityError for a 422 and TypeSafeBadRequestError for a 400.
  • The first request after a server starts warms the model and takes longer than the rest; allow for it.
  • TypeSafe’s SDK gives up after 10 s by default; raise its timeout.
  • fetch has no timeout of its own; pass signal: AbortSignal.timeout(ms) to stop sooner.
  • The server sets no rate limit, so the SDK’s retries on 429 never fire; it still retries 408 and 5xx.

Five frameworks that speak TypeSafe’s format work with decisio once they point at the server. Each was checked against a running server, and every check passed.

Framework, as checkedBase URLTimeout and retries
LangChain
langchain-typesafe 0.0.1a3; 42 of 42 checks passed
http://127.0.0.1:800030 seconds, no retries
Vercel AI SDK
ai 7.0.131, @ai-sdk/typesafe-ai 3.0.15; 42 of 42 checks passed
http://127.0.0.1:8000/v1none set (Node's fetch defaults apply); maxRetries 2 for retryable failures
n8n
n8n 2.35.7, n8n-nodes-jev 0.2.3; 44 of 44 checks passed
http://127.0.0.1:8000none unless added under Options; retries only on 429 and 529
TanStack AI
@tanstack/ai 0.65.1, @tanstack/ai-typesafe 0.1.8; 39 of 39 checks passed
http://127.0.0.1:8000none set (Node's fetch defaults apply); no retries
Pipecat
pipecat-ai 1.12.0; 39 of 39 checks passed
http://127.0.0.1:800010 seconds; 3 retries, on 429 and 529 only

Checked on the CPU stand-in of decisio 0.35.0 on 2026-10-26 (decisio-examples at 3436915), and against Gemma 4 31B on decisio 0.9.0, 2026-10-08. The stand-in's answers are not the default model's: the checks test the paths, fields, parsing and errors. The programs below ran on the stand-in on 2026-10-14.

  • Every one needs an API key; any string works.
  • Only the Vercel AI SDK wants /v1 at the end of the base URL; the others add it themselves.
  • Tasks and certified answers are decisio’s own routes: none of these frameworks calls them, so call those routes directly.

Each program below asks two questions about one ticket and prints the answers.

LangChain

pip install langchain-typesafe==0.0.1a3 (checked at that version; it prints a beta warning).

from langchain_typesafe import Choice, Noul, NoulCriteria, TypeSafeClassifier
clf = TypeSafeClassifier(base_url="http://127.0.0.1:8000", api_key="not-used")
result = clf.invoke(
{
"state": {"message": "My card was charged twice."},
"questions": {
"queue": Choice(instructions="Which queue?", criteria={"billing": "Payments", "access": "Login problems"}),
"urgent": Noul(instructions="Is it urgent?", criteria=NoulCriteria(true="needs an answer today", false="can wait")),
},
}
)
print(result.answers["queue"].choice, result.answers["urgent"].noul)

Run against the CPU stand-in at v0.35.0 on 2026-10-26: with langchain-typesafe 0.0.1a3, the version the page names.

Output: billing 0.43047947436151557 (the stand-in's numbers). result.answers[name] is a ChoiceAnswer (choice, probabilities, confidence) or a NoulAnswer (noul); a malformed question is refused by the client before it reaches the server, and a 422 from the server is raised as TypeSafeUnprocessableEntityError.

Vercel AI SDK

npm install ai@7.0.131 @ai-sdk/typesafe-ai@3.0.15 (Node 22). experimental_decide is the current name; experimental_evaluate is a deprecated alias of it.

import { experimental_decide as decide } from "ai";
import { createTypeSafeAi } from "@ai-sdk/typesafe-ai";
const typesafe = createTypeSafeAi({ baseURL: "http://127.0.0.1:8000/v1", apiKey: "not-used" });
const result = await decide({
model: typesafe.decisionModel("decisio"),
state: { message: "My card was charged twice." },
questions: {
queue: { type: "choice", instructions: "Which queue?", criteria: { billing: "Payments", access: "Login problems" } },
urgent: { type: "boolean", instructions: "Is it urgent?", criteria: { true: "needs an answer today", false: "can wait" } },
},
});
console.log(result.answers.queue, result.answers.urgent);

Needs Node 22 with ai 7.0.131 and @ai-sdk/typesafe-ai 3.0.15: taken as it is from decisio's checked docs.

Output: the choice answer { type: 'choice', choice: 'billing', probabilities: {...} } and { type: 'boolean', probability: 0.43047947436151557 }. confidence is under result.providerMetadata.typesafe.confidence.

n8n

n8n's built-in HTTP Request node posts the request to the server; no community node is needed. Import this workflow (Workflows, Import from File; or n8n import:workflow --input=workflow.json with the workflow wrapped in a list and given an id), change the URL if the server is not on port 8000, and press Execute. Checked in n8n 2.35.7: the workflow ran to success and the node's output held queue.choice: "billing" and urgent.noul: 0.43....

{
"name": "decisio: ask a question about a text",
"nodes": [
{
"parameters": {},
"id": "11111111-1111-4111-8111-111111111111",
"name": "Manual trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [0, 0]
},
{
"parameters": {
"method": "POST",
"url": "http://127.0.0.1:8000/v1/systemone",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\n \"model\": \"decisio\",\n \"state\": {\"message\": \"My card was charged twice.\"},\n \"questions\": {\n \"queue\": {\"type\": \"choice\", \"instructions\": \"Which queue?\", \"criteria\": {\"billing\": \"Payments\", \"access\": \"Login problems\"}},\n \"urgent\": {\"type\": \"noul\", \"instructions\": \"Is it urgent?\", \"criteria\": {\"true\": \"needs an answer today\", \"false\": \"can wait\"}}\n }\n}",
"options": {}
},
"id": "22222222-2222-4222-8222-222222222222",
"name": "Ask decisio",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [240, 0]
}
],
"connections": {
"Manual trigger": {"main": [[{"node": "Ask decisio", "type": "main", "index": 0}]]}
},
"active": false,
"settings": {}
}

Needs n8n 2.35.7: taken as it is from decisio's checked docs.

The community node n8n-nodes-jev 0.2.3 also works against decisio (its credential's Base URL is the server's, without /v1); the HTTP Request node is shown because it needs nothing installed and its request body is the wire format itself.

TanStack AI

npm install @tanstack/ai@0.65.1 @tanstack/ai-typesafe@0.1.8 (Node 22).

import { boolean, choice, decide } from "@tanstack/ai";
import { createTypesafeDecider } from "@tanstack/ai-typesafe";
const adapter = createTypesafeDecider("decisio", "not-used", { baseURL: "http://127.0.0.1:8000" });
const result = await decide({
adapter,
state: { message: "My card was charged twice." },
questions: {
queue: choice({ instructions: "Which queue?", options: { billing: "Payments", access: "Login problems" } }),
urgent: boolean({ instructions: "Is it urgent?", criteria: { true: "needs an answer today", false: "can wait" } }),
},
});
console.log(result.queue.value, result.urgent.probability);

Needs Node 22 with @tanstack/ai 0.65.1 and @tanstack/ai-typesafe 0.1.8: taken as it is from decisio's checked docs.

Output: billing 0.43047947436151557. Each result carries value, probability, confidence and probabilities; meta.usage maps the server's usage. typesafeDecider(model) reads the key from TYPESAFE_API_KEY and has no base-URL variable, so use createTypesafeDecider with baseURL as above.

Pipecat

pip install "pipecat-ai[jev]==1.12.0" (it installs without the audio extras).

import asyncio
from pipecat.classifiers.base_classifier import ChoiceQuestion, YesNoQuestion
from pipecat.classifiers.jev.classifier import JevClassifier
async def main():
clf = JevClassifier(api_key="not-used", base_url="http://127.0.0.1:8000")
state = {"message": "My card was charged twice."}
queue = await clf.choice(state, {"queue": ChoiceQuestion(instructions="Which queue?", options={"billing": "Payments", "access": "Login problems"})})
urgent = await clf.yes_no(state, {"urgent": YesNoQuestion(instructions="Is it urgent?", yes="needs an answer today", no="can wait")})
print(queue["queue"], urgent["urgent"])
await clf.cleanup()
asyncio.run(main())

Run against the CPU stand-in at v0.35.0 on 2026-10-26: with pipecat-ai 1.12.0, the version the page names.

Output: choice='billing' probabilities={...} confidence=... and the yes/no result. A 422 from the server is reported as HTTP 422 and the server's detail is dropped by the client; the default timeout is 10 seconds, which the first question after a start can exceed on a slow card.

  • Move from System One: point a client you already have at decisio, and what changes.
  • Teach a task: teach a question you ask again and again, and certify its answers.
  • The HTTP API: every route, request field and response field.