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

Source: https://opendecisio.com/docs/use-from/

Call decisio from any language with one HTTP request, and get a [probability](https://opendecisio.com/docs/glossary/#probability) for every [option](https://opendecisio.com/docs/glossary/#option) back. Each script below sends the support ticket from [Get started](https://opendecisio.com/docs/) and prints each [question](https://opendecisio.com/docs/glossary/#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&#x3C;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&#x3C;string, number> };
const { answers } = (await response.json()) as { answers: Record&#x3C;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.

 

## Keep the server private

 

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

 

## Handle errors

 

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

  

| Status | What it means |
| --- | --- |
| 422 | the server cannot read the request: a missing field, more than 255 options, a text longer than the context |
| 400 | the model cannot answer this request |
| 415 | the 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.

 

## Set a timeout

 

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

 

## Use a framework

 

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 checked | Base URL | Timeout and retries |
| --- | --- | --- |
| **LangChain** langchain-typesafe 0.0.1a3; 42 of 42 checks passed | `http://127.0.0.1:8000` | 30 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/v1` | none 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:8000` | none 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:8000` | none set (Node's fetch defaults apply); no retries |
| **Pipecat** pipecat-ai 1.12.0; 39 of 39 checks passed | `http://127.0.0.1:8000` | 10 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](https://opendecisio.com/docs/glossary/#task) and [certified answers](https://opendecisio.com/docs/glossary/#certified-answer) 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.

 

## What next

 

- [Move from System One](https://opendecisio.com/docs/migrating/): point a client you already have at decisio, and what changes.
- [Teach a task](https://opendecisio.com/docs/tasks/): teach a question you ask again and again, and certify its answers.
- [The HTTP API](https://opendecisio.com/docs/api/): every route, request field and response field.
