Building with aityx
Every AI decision needs an answer you can explain and a rule you can change. aityx returns the choice, the amount and the date as typed answers, from a JSON model your team owns, with a receipt for each one. Here is how to call it, write the model and handle errors.
Key takeaways
- One endpoint, one contract. Send
decisionandstate, plus Jevquestionswhen a fact has to be graded from text, toPOST /v1/systemone. Every call returnsanswers,receiptandusage.- Models are readable JSON you own. Inputs, decision tables with a
# whyon every row, expressions for amounts and dates, and functions. Your team defines the model; aityx validates and runs it.- Answers are typed, and the receipt explains them. Your code reads
answers.refund_amount.number. The receipt records where each input came from and which rule produced each decision.- Jev grades the evidence; your decision model does the math. Jev values arrive raw, so write the cutoff in your model, where it is readable and testable.
- Plan for errors, including HTTP 200 with a
decision_error. A combined call can complete Jev and still fail the decision. Do not act on missing native answers.- A rule change is an edit to your JSON. Replay the cases you already understand against the revised model before your application switches.
aityx (say EYE-tyx) is a decision API for AI apps and agents that does the math. AI Decision APIs answer classification questions, such as whether a customer should get a refund. The real decision also needs numbers and dates: how much, and by when. Your team defines that decision as a JSON model. Your app sends the model and the facts of each case to one endpoint and gets back typed answers: the choice, the calculated amount and the date, with a receipt that shows how each one was reached. When a fact lives only inside a message, Jev grades it and your decision model calculates from its answer, in the same call.
The contract: one endpoint, three shapes
Everything goes to POST /v1/systemone with a bearer API key and a JSON body. What you include decides what runs:
- Decision only (
decision+state). Your model on matching structured facts. Runs without an AI provider call. - Jev only (
questions+state). Jev grades the evidence; its answers are the result. - Jev + Decision (
decision+questions+state). Jev supplies the uncertain facts; your decision model returns the final answer.
Every shape returns the same three fields: answers, receipt and usage. The response header X-aityx-Mode tells you which shape ran. There is no stored model and no receipt history: your application keeps the model, the original state and the receipt, and sends the model with every case. That keeps the API stateless and keeps the evidence where your case lives.
Your first call
Take the example that runs on the aityx homepage. A customer writes: “Please cancel my subscription and refund the days I have not used.” For this decision-only call, assume your application has already classified the request as cancel_refund and knows the account facts: the plan price, the billing period, the unused days, the date the request was accepted, and whether the account is refundable.
Include the required customer_intent input with those account facts:
{
"customer_intent": "cancel_refund",
"plan_amount": 60,
"period_days": 30,
"unused_days": 10,
"accepted_on": "2026-10-21",
"refundable": true
}
Send this state and the model to the endpoint:
async function getRefund(decision, state, apiKey) {
const res = await fetch("https://api.aityx.ai/v1/systemone", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ decision, state })
});
if (!res.ok) throw new Error(`aityx ${res.status}`);
const { answers } = await res.json();
return {
amount: answers.refund_amount.number,
due: answers.processing_deadline.date
};
}
The recorded response, simplified to show the answer values that matter (the actual response contains typed answer objects):
{
"request_type": "cancel_refund",
"refund_eligible": 1,
"refund_amount": 20,
"processing_deadline": "2026-10-28"
}
Cancel and refund. $20. October 28. Each answer is typed, so your code reads answers.refund_amount.number and moves on. No prompt engineering, no regular expressions over model output, no “please respond in JSON”. The JavaScript and Python guide has the full client for both languages.
Writing the model
A model is one JSON document with a name, inputs, decisions, and optional types and functions. Answer names and types come from the decisions, so there is no separate output schema to keep in sync.
Inputs are observations. Each one has a type and a desc, the question it answers. Types are compact strings: built-ins such as number, boolean and date, constrained forms such as number >= 0, choices such as cancel_refund|cancel_only|other, ordered levels such as low<medium<high, structs and lists.
"customer_intent": {
"type": "cancel_refund|cancel_only|other",
"desc": "What is the customer's request type?",
"options": {
"cancel_refund": "Cancellation with a refund requested.",
"cancel_only": "Cancellation without a refund requested.",
"other": "Any other request."
}
}
Decisions hold the policy. A decision is an expression or a decision table. Tables are first-match: rows are tried top to bottom, the last row is the else row, and every row carries a # why that a reviewer would accept.
"refund_eligible": {
"desc": "Is the request eligible for a refund?",
"table": [
["customer_intent", "refundable", "unused_days > 0", "=> boolean", "# why"],
["cancel_refund", "true", "true", "true", "A refundable account with unused days qualifies."],
["-", "-", "-", "false", "At least one refund condition is not met."]
]
}
Expressions do the arithmetic and the calendar. Amounts, dates and durations are first-class, so a deadline is an expression over a date, and rounding is explicit.
"processing_deadline": {
"type": "date",
"desc": "The processing deadline after the acceptance date.",
"expr": "accepted_on + duration(\"P\" + string(deadline_days) + \"D\")"
}
Functions keep calculations in one place. They use only their parameters and other functions, and the receipt records their evaluation.
"prorated_refund": {
"params": { "amount": "number", "unused": "number", "period": "number" },
"returns": "number",
"body": "decimal(amount * (if unused < period then unused else period) / period, 2)"
}
The model format reference covers the rest: let bindings, object-form rules for wide tables, named types, and the expression language. The console’s Check is free and reports whether a definition is ready to run before you send it from code. aityx validates and runs models; it does not create them.
Reading answers and receipts
Every named decision, including helpers, appears in answers with a type and a value field:
- Boolean decisions return
noul, with.noulas1for true and0for false. - Named choices return
choice; ordered levels returnscorewith.scoreand.level. - Numbers return
number, dates returndateas an ISO string, and anything else returnsvaluewith the JSON preserved.
The receipt is the part auditors and on-call engineers will thank you for. It records which inputs Jev read, the provenance of every input, and the rule or function that produced each decision:
{
"read": {
"customer_intent": { "value": "cancel_refund", "source": "jev" }
},
"provenance": {
"plan_amount": "state",
"period_days": "state",
"unused_days": "state",
"accepted_on": "state",
"refundable": "state",
"customer_intent": "jev"
},
"decisions": {
"refund_eligible": { "value": true },
"refund_amount": { "value": 20 },
"processing_deadline": { "value": "2026-10-28" }
}
}
One fact came from Jev grading the message. The rest came from your systems. The refund amount and the deadline came from your rules. Keep three things together per case: the model you sent, the original state including any message, and the returned answers and receipt. Together, they let you explain an answer later without making another API call.
Jev grades the evidence, your decision model does the math
Jev put AI Decision APIs on the map: ask it a named question about evidence and it answers with a probability (noul), a choice or a score. We’re extending that beyond classification to numbers and dates. aityx accepts Jev questions at the same endpoint, and in a combined call Jev runs first, its named answers merge with your state, and your decision model calculates from the result.
Three details matter when you wire this up:
- Jev values arrive raw. A
noulbecomes a probability between zero and one, a choice becomes its string, a score becomes a number. The API does not threshold or round them. Write the cutoff in your model, where it is readable and testable, for exampleapproval_probability >= 0.8. - Names collide on purpose. A Jev answer overrides a state field with the same name. Use distinct names for graded evidence and trusted business facts, and read
receipt.provenanceto see which won. - Inputs can bind any field of the Jev response.
from: "jev:<JSON Pointer>"on an input pulls, say, a confidence value, andthresholdmakes the input missing below it, so a low-confidence answer never passes as a fact. Mark the inputblank: trueand give the missing case its own review row, or leave it required and send the call’sdecision_errorto review.
The Jev guide has the complete combined request and the response shapes.
Handling errors
Failed requests return problem details with a stable code. The ones you will design around:
422 invalid_state: the facts do not match the model’s inputs. Nothing extracts facts for you. Supply matching facts, or add a Jev question that establishes the missing one. Theerrorsarray names each path.422 invalid_modeland422 evaluation_failed: the definition did not validate, or could not evaluate these inputs. Fix the JSON, or run the console’s free Check.402 insufficient_balance: Decision-only calls reserve funds before running; Jev calls need a positive balance.502,503,504with ajev_code: Jev failed, was unavailable, or timed out. Your decision model did not run. Retry with backoff.
One case needs special care. HTTP 200 can still mean partial failure. If Jev completes but your decision model then fails, the response keeps Jev’s answers and receipt and adds decision_error. Native answers are not there; do not act on them.
const result = await res.json();
if (result.decision_error) {
// Jev completed; the decision did not. Route to review, keep the receipt.
return { outcome: "needs_facts", error: result.decision_error, answers: result.answers };
}
The errors reference lists every code and its next step.
Changing rules without surprises
There is no revision endpoint: a rule change is an edit to your JSON. The safe pattern is to keep cases. Save the states you already understand, with the answers you expect, and run them against both the current and the revised model before your application switches. The console’s Replay does this interactively; from code, it is a loop over your cases and two files. Compare revised rules walks through it with a worked model.
Because you send the model with every request, a rollout is also under your control: ship the revised JSON with your application, or keep both and select per tenant, per region or per experiment.
Get started
- Sign up in the console with your email. No approval needed. See pricing and limits for current credit and capacity information.
- Run the recorded example in the console: get the request type, the refund and the deadline. Change the unused days, then change the deadline rule, and watch the answers follow.
- Create an API key and run the quickstart from your terminal with
curlandjq. The same guide adds the Jev question that reads a customer message. - Call it from your code with the JavaScript and Python guide, and handle the error codes above.
- Write your own decision. Start from Define a model by hand, then keep the model format open. For larger worked examples, read the models of the demos in Playing with aityx.
Frequently asked questions
Is aityx a language model?
No. aityx executes a decision model your team supplies. It only calls an AI provider when you include Jev questions, and then only to answer those questions. Amounts, dates and conditions are calculated by your rules.
Do I need Jev to use aityx?
No. Send your decision model with matching structured facts and it runs alone, with no provider involved. Add Jev questions when a fact has to be graded from a message or other unstructured evidence.
Are the calculated answers repeatable?
Yes, for the same model and the same validated facts: the same inputs always produce the same result and the same rule trace. Jev’s grading can vary between calls, so the receipt labels every input as coming from state or from jev, and any probability threshold is written explicitly in your model.
Can aityx create a decision model from a prompt or a document?
No. Your team defines the model, by hand in the console or with your own tools, and aityx validates and runs it. Documents are not uploaded to create a model; include relevant text as evidence in state and ask Jev questions about it.
Where do models and receipts live?
In your application. aityx has no stored-model or receipt-history lookup. You send the model with each request and keep the receipt with the case. The demos keep their models as JSON files next to the API that sends them.
Who is behind aityx, and what if we need this on our own infrastructure?
aityx is built by Aletyx, a team with a long record of decision technology in production. The tech preview is hosted. Teams that need decisions on their own infrastructure, with governance and workflows, can talk to the Aletyx Platform team.
Want to see the pattern at work before you build? Play with the samples in Playing with aityx: open-source games, from the Game of Life to a poker table, each with its decision models in JSON you can read, edit and run.
Run the refund example at aityx.ai, see the pattern at work in Playing with aityx, and read the aityx decision API docs to build your own. Questions, capacity needs or feedback: [email protected].
Jev and TypeSafe are trademarks of their respective owners. aityx is not affiliated with TypeSafe AI.



