Skip to content
Docs / Start with no data

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.

POST/v1/build

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, notes says so.
fieldsobject
Optional: what fields mean, keyed by a field's path (as in understood.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 from description or 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 its path and its type (number, yes/no, category or text). 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 from fields, with the path it went to and its meaning, unit and range. field_notes_not_matched: a note that matched no field (key), and why.
  • 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.

GET/v1/build/{build_id}
GET/v1/domains/{domain}/build
phasewhat happens
understandingreading your example and your questions
preparingpreparing examples of situations for your questions, and about 20 examples of how your model decides
trainingyour model is being built, on our servers
checkingyour 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.

json
{ "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.

json
"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 as refunds@latest.
  • needs_attention: the version is kept but doesn't serve. Each question under the bar has advice in 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.
  • weakest lists 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:

POST/v1/domains/{domain}/signoff
json
{ "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.

diff
- 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: str is text, int and float numbers (Field(ge=…, le=…) bounds them), bool yes/no, a Literal or an Enum a category, a pydantic model a nested object.
  • The return type, a pydantic model, is your questions: a Literal or an Enum is a Choice (option descriptions in Field(json_schema_extra={"options": {...}})), a bool a Noul, a bounded int a Score. A field's description is its question's instructions.
  • The docstring: its first paragraph is the description, and the rest are your rules in plain words.
python
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 example state to 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: the Client to 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 optionally examples (5-20 more real states), fields, rules, name, description and history rows (the local server also takes history_file).
  • get_build: a build_id, or a model for 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, with model set 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, or 402 when 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: 400 when the example or the questions can't be read (an empty object, a Choice with one option), 409 when 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.