DocsHow-to guides
Teach a task
Teach decisio a question you ask again and again, from your own labelled examples. The model does not change; the server learns what your labels mean.
Register the question once, with labelled examples, and every later request that asks it is answered with what the server learned. With the example task from decisio’s repository, one request teaches it:
curl http://127.0.0.1:8000/v1/tasks -H 'Content-Type: application/json' -d @examples/tasks/examples.jsonRun against the CPU stand-in at v0.35.0 on 2026-10-26. It printed, trimmed to the fields this page uses:
{
"id": "ticket-routing",
"n_examples": 120,
"calibration": {
"applied": true,
"reason": "cross-validated gain"
},
"head": {
"applied": true,
"reason": "cross-validated gain"
}
}
Figure · Teach and trust
Teach the question, certify its answers, route on the certificate, and let the server watch.
This page is the first step.
1. Check that it is a task
Section titled “1. Check that it is a task”- A task is one question you ask again and again, with the same options in the same order every time: “Which support queue should handle this ticket?” over the same twelve queues.
- A question whose options change every time is not one: exam questions, each with its own four answers.
2. Gather your examples
Section titled “2. Gather your examples”| You need | For |
|---|---|
| At least 10 labelled examples | a correction per option that removes a steady lean toward some options |
| A question with 10 or more options, and at least 5 examples of each; 10 is better | the intent head, which learns what your labels mean in your data |
| Examples from your own traffic | the same kind of text, and the same mix of options, as the server will see |
| More examples, if you will certify | half of them are held back to set the threshold (Certify answers) |
Each example is one real input with the option you would want chosen, asked with the exact question and options you will use later.
3. Register
Section titled “3. Register”A task is one POST /v1/tasks: an id, and the examples, each an ordinary request with one question plus the answer you want.
{ "id": "ticket-routing", "examples": [ { "request": { "state": "My card keeps getting declined when I renew.", "questions": { "route": { "type": "choice", "instructions": "Which support queue should handle this ticket?", "criteria": { "billing": "Invoices, charges on the account, plan prices", "payment_failed": "A card or bank payment was declined or failed", "...": "..." } } } }, "answer": "payment_failed" }, ... ]}A shape to fill in, not a command to run.
With a server running (Get started starts one) and from the root of the clone, register the example’s 120 tickets, from a CSV:
uv run python examples/tasks/register.py --id ticket-routing --data examples/tasks/train.csvRun against the CPU stand-in at v0.35.0 on 2026-10-26. It printed:
120 examples, 12 labels, 10 to 10 per label
task 'ticket-routing': 120 examples, 12 options, at least 10 per option, registered in 357 s
calibration: applied (cross-validated gain); cross-validated log loss 2.42554 -> 2.35643, accuracy 0.2667 -> 0.2917
head: applied (cross-validated gain); penalty 0.0001; cross-validated log loss by penalty {"none": 2.42554, "100.0": 2.42214, "10.0": 2.39179, "1.0": 2.11252, "0.1": 1.00911, "0.01": 0.45439, "0.001": 0.28943, "0.0001": 0.23452}
Registering the same id again replaces the task.
4. Read what it kept
Section titled “4. Read what it kept”The answer has one record for each correction, calibration and head:
| Field | What it says |
|---|---|
applied | true when later questions with this option list are answered with the correction |
reason | why, in words: “cross-validated gain” when it was kept, because it helped on examples it was not fitted on, or what was missing |
The server keeps a correction only when it helps on your own examples, checked by scoring each fifth of them with a fit made on the other four. When it declines one, the question is answered exactly as before:
- Too few examples: add more, up to 20 for the correction or 10 per option for the head, and register again.
- The plain answer is already as good: common for a clear yes/no question.
- Labels that disagree: two people labelling the same kind of input differently look like noise, and the server declines.
5. Ask, and check that it applied
Section titled “5. Ask, and check that it applied”Ask the question exactly as registered:
uv run python examples/tasks/ask.py "My card keeps getting declined when I renew."Run against the CPU stand-in at v0.35.0 on 2026-10-26. It printed:
My card keeps getting declined when I renew.
choice payment_failed; x-decisio-tasks: ticket-routing; route text, 1472.9 ms
payment_failed 1.000, bug 0.000, billing 0.000
The response header x-decisio-tasks: ticket-routing says the task answered it.
No header means the question matched no task: check that its question and options are exactly the ones you registered.
6. Keep it across restarts
Section titled “6. Keep it across restarts”Start the server with --tasks-dir and every task you teach is saved to disk before the server answers, and loaded again at the next start:
python -m decisio.serve.vllm_engine --tasks-dir /data/tasks --require-tasks 3 ...Shown, not run: it is the start of a server on a card, with a directory on a volume; its other flags are the reader's.
- Put the directory on a volume that outlives the server, and add
--require-tasks N: a server that finds fewer than N usable tasks refuses to start, so a volume that failed to mount does not turn into untaught answers. - A save that fails fails the request with 500, and the task is not registered.
- One directory belongs to one server. For several replicas, see Operate.
/health says where the server saves its tasks, and how many it loaded and applies:
curl -s http://127.0.0.1:8000/health \ | python3 -c 'import json, sys; print(json.dumps(json.load(sys.stdin)["systemone"]["task_store"], indent=1))'Run against the CPU stand-in at v0.35.0 on 2026-10-26: on the stand-in started with --tasks-dir decisio-tasks, after the task above was registered. It printed:
{
"saved_to": {
"backend": "dir",
"path": "decisio-tasks"
},
"readout": {
"loaded": 1,
"applied": 1,
"other_stack": 0
},
"abstention": {
"loaded": 0,
"applied": 0,
"other_model": 0,
"other_layout": 0
},
"at_start": {
"saved_to": {
"backend": "dir",
"path": "decisio-tasks"
},
"from_dir": {
"readout": 0,
"abstention": 0
},
"from_file": {
"readout": 0,
"abstention": 0
},
"usable": 0
}
}
To move a task to another machine, export it and import it there, or start the server with --tasks-file tasks.json:
# registered tasks and each fit's summarycurl -s http://127.0.0.1:8000/v1/tasks# export, head parameters includedcurl -s 'http://127.0.0.1:8000/v1/tasks?full=1' > tasks.jsoncurl -s http://127.0.0.1:8000/v1/tasks/import -H 'Content-Type: application/json' -d @tasks.jsoncurl -s -X DELETE http://127.0.0.1:8000/v1/tasks/ticket-routingRun against the CPU stand-in at v0.35.0 on 2026-10-26. Its comments are shown above their commands.
- A task applies only under the model and the prompt layout it was taught on; on another, the server does not apply it and says why.
- A Gemma server that loads tasks taught under the plain layout keeps serving plain and says so, so the tasks keep applying.
- Renaming, adding or reordering an option makes a new task: teach it again.
- To reproduce a task exactly, export and import it rather than registering it again.
What it does
Section titled “What it does”On the default model, Gemma 4 31B, teaching with 10 examples per intent:
| Intent set | Right, untaught | Right, taught | Measured on |
|---|---|---|---|
| BANKING77, 77 intents | 78.7% | 84.4% | 10 labelled examples per intent; 150 test messages; mean of 3 draws; taught in 308 s; one RTX PRO 6000, vLLM 0.30.0, 2026-10-04 |
| CLINC150, 150 intents | 94.0% | 97.0% | 10 labelled examples per intent; 100 test messages; mean of 3 draws; taught in 699 s; one RTX PRO 6000, vLLM 0.30.0, 2026-10-04 |
These compare the taught server with the untaught one on the same messages, which none of the examples were.
- Good fit: a long list of fine-grained options, where a model reading only the option names is weakest; a question your team already labels.
- Not a good fit: a clear yes/no question the model already answers well. The server declines the correction and says so, and the answers stay as they were.
What it costs
Section titled “What it costs”- Teaching: every example is scored once, plus the fit, as in the times above.
- Each later question: a correction costs nothing extra. A question the intent head answers takes extra work on the server: in the triage demo the 31B answered in 71 ms plain and 262 ms with the head, median.
The intent head's cost: measured in decisio's triage demo on decisio 0.6.0, one RTX PRO 6000, 2026-10-05.
What next
Section titled “What next”- Certify answers: get answers the server stands behind, at the error rate you choose.
- Watch for drift: know when traffic moves away from your examples.
- The HTTP API: the task routes, field by field.