Start with no data
Send us the same JSON you send Jev. Get your own model back in minutes. No past decisions or answers needed; a few examples of what you send Jev make it better: one real example of your state, 5-20 more in examples, your questions exactly as they are, and, if you like, notes on your fields and your rules in plain words.
Alias: POST /v1/systemone/build, same body. A pasted Jev request works as is: model and anything else Jev takes are ignored.
curl https://api.canonopylabs.com/v1/build \
-H "Authorization: Bearer $CANONOPY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"state": { "order": { "amount": 42.5 }, "customer": { "tier": "pro", "fraud_flag": false } },
"questions": {
"action": { "type": "choice", "instructions": "What should we do with this refund request?",
"criteria": { "approve": "refund the customer", "escalate": "a person decides",
"decline": "politely decline" } },
"urgent": { "type": "noul", "instructions": "Does this need a person within the hour?" }
},
"examples": [
{ "order": { "amount": 820 }, "customer": { "tier": "free", "fraud_flag": false } },
{ "order": { "amount": 12.99 }, "customer": { "tier": "pro", "fraud_flag": true } }
],
"fields": { "order.amount": "the refund asked for, in USD (0-5,000)" },
"rules": "Never approve when amount is over 500. Always escalate when fraud_flag is true.",
"name": "refunds",
"description": "Refund requests"
}'statestring | objectrequired- ONE real example: a text, or the JSON object your program sends. Every value is read as a field with a type and a
path: numbers,true/false(yes/no), short codes (categories) and longer writing (text). examples(string | object)[]- Optional, recommended: 5-20 more real states, each like
state(up to 50; states only, no answers). Pick different ones: a quiet moment and a busy one, a small order and a large one. Your model's situations are made from all of them and checked against them, and your fields' ranges and units are read from them. With one state only,notessays so. fieldsobject- Optional: what fields mean, keyed by a field's
path(as inunderstood.fields, e.g.player.angle) or its name. A note is a sentence, e.g."turn angle in engine units (0-4,294,967,295)", or{"meaning", "unit", "range": [lowest, highest]}. Your model's situations follow them. questionsobjectrequired- Your questions, exactly as you send them to Jev: Choice, Noul and Score, with instructions and criteria. They're stored as they are, so you keep sending the same JSON.
rulesstring- Your rules in plain words, e.g. "Never approve when amount is over 500. Always escalate when fraud_flag is true."
namestring- The model's name, what you'll pass as
model. Default: one made fromdescriptionor your question ids. Building again under the same name makes the next version of that model. descriptionstring- What it decides, in a sentence.
historyobject[]- Optional: your past cases with their answers. See Make it better with your history.
What it reads from your example
The answer comes at once, with what was read in understood. Check it before anything else:
fields: every field your model reads, with itspathand its type (number,yes/no,categoryortext). Lists of objects are read at their first 8 positions ([0]to[7]); lists of plain values one position per item.not_read: what isn't read, and why. Identifiers (id,*_id, uuids, emails, phone numbers, links, timestamps), empty values and empty lists are left out on purpose.field_notes: each note fromfields, with thepathit went to and itsmeaning,unitandrange.field_notes_not_matched: a note that matched no field (key), andwhy.questions: your questions, as stored.decision_question: the Choice question your hard rules apply to.rules: the rules read and enforced on every decision.rules_not_understood: what couldn't be read as a rule that blocks answers, each with the reason. Your model still follows it in its answers; it just isn't enforced as a hard rule.
Rules in plain words
Write them the way you'd say them, one per sentence: "Never approve when amount is over 500." "Always escalate when fraud_flag is true." "When tier is free, only decline or escalate." Name a field by its path (order.amount), its name (order_amount) or, when it's unique, its last key (amount).
Rules that block answers of your decision question are enforced on every decision, in code, whatever the model thinks, and reported in each answer. A rule on another question's answer ("Always block when topic is lost_card") is enforced whenever there's a real chance it applies. See Decisions and rules.
The phases
Follow the build with GET /v1/build/{build_id}, or the model's latest build with GET /v1/domains/{domain}/build. The console shows the same progress.
| phase | what happens |
|---|---|
understanding | reading your example and your questions |
preparing | preparing examples of situations for your questions, and about 20 examples of how your model decides |
training | your model is being built, on our servers |
checking | your model against your instructions and rules, on fresh situations |
status goes queued → building → training → checking → ready, or needs_attention (built, not serving yet: see below), or failed (errors says why, in plain words). Nothing waits for you in between. message and next_step always say where it is and what to do.
{ "build_id": "bld_…", "domain": "refunds", "model": "refunds@latest",
"status": "training", "phase": "training",
"message": "Your model is being built.",
"next_step": "Your model is being built. Check this build again in a minute or two.",
"understood": { "state": "object",
"fields": [{ "name": "order_amount", "path": "order.amount", "type": "number" }, "…"],
"decision_question": "action",
"rules": ["Never approve when amount is over 500.", "Always escalate when fraud_flag is true."],
"rules_not_understood": [] },
"examples": [], "quality": null,
"stats": { "seconds": 94.2, "situations": 30000, "examples": 20 } }The quality check: 95% on fresh situations
Once trained, your model is checked against your instructions and rules on fresh situations it never learned from. Each question needs to agree at least 95% of the time (quality.bar) before the model serves.
"quality": { "bar": 0.95, "passed": true, "situations": 4000,
"questions": [{ "question": "action", "agreement": 0.978, "passed": true, "cases": 4000,
"weakest": [{ "where": "when the right answer is 'escalate'", "agreement": 0.93, "cases": 240 }] }],
"statement": "…" }ready: it serves asrefunds@latest.needs_attention: the version is kept but doesn't serve. Each question under the bar hasadvicein plain words: where it's weakest ("… weakest when the right answer is 'escalate' (72%) and when amount is under 150 (80%) …") and what to change. Clarify your instructions or rules and build again under the same name, or retrain it.weakestlists where each question matches least, even when it passes: what a retrain sharpens.
stats has how long it took, the number of situations your model learned from, and once trained the download's size (download_bytes) and the unzipped model's (model_file_bytes). The version's report has the same check, with its questions measured_on: "situations". Once you have enough answered cases of your own (about 200: answer the review queue or report outcomes), a new version is also compared with the serving one on your own held-back cases, and it serves only if it isn't worse there (quality.measured_on: "your_cases", with the numbers in quality.compared).
Review how it decides (optional)
Your model is ready in minutes. Review how it decides any time (optional): about 20 situations, each with the answer your model gives and why. They're at GET /v1/domains/{domain}/examples, and in the build's examples once it's ready or needs_attention. Nothing waits on them.
To correct one, send only the ones you correct. With "retrain": true the retrain starts in the same call:
{ "examples": [
{ "id": "ex_02", "ok": false, "correct": { "action": "escalate" }, "note": "pro accounts under 30 days go to a person" } ],
"retrain": true }The answer has a message and the retrain's build_id. Your corrections also go to your model's instructions, so the retrained model gives those answers. Without retrain, they apply at your next retrain (POST /v1/domains/{domain}/train). The review is kept as a record. In the CLI: canonopy examples refunds, then canonopy signoff refunds --correct ex_02:action=escalate --retrain.
Switch over and keep sending the same JSON
When it's ready, change two things in the code that calls Jev today: the base URL and the model name. The request and the answer keep their shape.
- POST https://api.typesafe.ai/v1/systemone "model": "jev-latest"
+ POST https://api.canonopylabs.com/v1/systemone "model": "refunds@latest"/v1/systemone is an alias of /v1/decide. Every answer also says who answered (source) and whether to act on it (route), and your rules are applied in decision. See Quick start and Decisions and rules.
From a Python function: @canonopy.fn
If you describe the decision as a typed Python function, put @canonopy.fn on it (swap @jev.fn for @canonopy.fn). It needs canonopy-decisions 0.2.0 or later and pydantic 2. Only the function's definition is read; its body is never run.
- The parameters are your state's fields:
stris text,intandfloatnumbers (Field(ge=…, le=…)bounds them),boolyes/no, aLiteralor anEnuma category, a pydantic model a nested object. - The return type, a pydantic model, is your questions: a
Literalor anEnumis a Choice (option descriptions inField(json_schema_extra={"options": {...}})), aboola Noul, a boundedinta Score. A field'sdescriptionis its question's instructions. - The docstring: its first paragraph is the description, and the rest are your rules in plain words.
from typing import Literal
from pydantic import BaseModel, Field
import canonopy
class Refund(BaseModel):
action: Literal["approve", "escalate", "decline"] = Field(
description="What should we do with this refund request?",
json_schema_extra={"options": {"approve": "refund the customer", "escalate": "a person decides",
"decline": "politely decline"}})
urgent: bool = Field(description="Does this need a person within the hour?")
@canonopy.fn(name="refunds")
def refund(message: str, amount: float, tier: Literal["free", "pro", "enterprise"], fraud_flag: bool) -> Refund:
"""Refund requests from our shop.
Never approve when amount is over 500. Always escalate when fraud_flag is true.
"""
refund.request # the JSON it sends to POST /v1/build: name, state, questions, rules, description
build = refund.build(wait=True) # waits until the model is ready (or needs attention, or failed)
answer = refund("Charged twice, please refund", amount=820, tier="pro", fraud_flag=False)
answer.action # "escalate": a Refund, with your rules applied@canonopy.fn(name=None, *, rules=None, example=None, description=None, decision=None, client=None, examples=None, fields=None):
name: the model's name (default: the function's);rules: more rules in plain words, before the docstring's;description: instead of the docstring's first paragraph.example: the examplestateto build from (default: one made from the parameter types and their defaults).examples: 5-20 more real states;fields: notes on what fields mean.decision: the question that gets the final decision's action (default: the one your rules apply to);client: theClientto use.
.build(history=[...]) sends your past cases too. .bind(model="refunds@3") points the function at a fixed version, or a model you already have. Calling the function decides with refunds@latest and returns the typed answer: the decision question gets the action with your rules applied, a Noul True or False, a Score its level.
From a coding agent
The MCP tools (the hosted connector and canonopy mcp, see Use with coding agents):
build_model:state,questions, and optionallyexamples(5-20 more real states),fields,rules,name,descriptionandhistoryrows (the local server also takeshistory_file).get_build: abuild_id, or amodelfor its latest build.get_examples,correct_examples: optional, once it's ready: how your model decides on about 20 situations, and corrections (with a retrain).decide: the same JSON, withmodelset to the new name.
A prompt that works: "Take the Jev request in src/refunds.py, build a Canonopy model from it with our refund rules, and switch the code over once it's ready." Then, if you like: "Show me how it decides on the examples."
In the console
New model, then Paste your Jev request: paste the JSON, paste a few more examples (one state per line) and notes on fields if you like, add your rules, and follow the build. Once it's ready, the page gives the model to call, and Review examples shows how it decides (optional).
Good to know
- In minutes: preparing the examples takes a few minutes, training about a minute. Your model is small enough to download and run yourself.
- Limits: 5 builds per workspace per UTC day, 3 during the free trial, 30 a month with a subscription (
429, or402when the trial's builds are used up). A build is a new model, a new version, or a rule change; retrains don't count. A build counts as training for your plan: after the free trial it needs a subscription (402). You're never charged per build. See Build limits. - Errors:
400when the example or the questions can't be read (an empty object, a Choice with one option),409when the name belongs to a model not made this way, or a build of it is still in progress. - Past cases later: your uploads, outcomes and unsure-queue answers improve the same model. See Make it better with your history and Improve your model.