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

Source: https://opendecisio.com/docs/tasks/

Register the [question](https://opendecisio.com/docs/glossary/#question) once, with labelled examples, and every later request that asks it is answered with what the server learned. With the example [task](https://opendecisio.com/docs/glossary/#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.json
```

 

Run 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 the question, certify its answers, route on the certificate, and let the server watch. Three steps and a watch. 1, teach: your labelled examples fit a correction to your question. 2, certify: examples held back set a threshold with an error target. 3, route: certified answers act on their own and the rest go to a person. The server watches the traffic and suspends certificates when it shifts. This page is the first step.]

 

## 1 Check that it is a task

 

- A task is one question you ask again and again, with the same [options](https://opendecisio.com/docs/glossary/#option) 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

  

| 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](https://opendecisio.com/docs/glossary/#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](https://opendecisio.com/docs/glossary/#threshold) ([Certify answers](https://opendecisio.com/docs/tasks/certify/)) |

 

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

 

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](https://opendecisio.com/docs/) 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.csv
```

 

Run 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

 

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](https://opendecisio.com/docs/glossary/#yesno-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

 

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

 

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](https://opendecisio.com/docs/operating/#4-keep-tasks-across-restarts-and-replicas).

 

`/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 summary
curl -s http://127.0.0.1:8000/v1/tasks
# export, head parameters included
curl -s 'http://127.0.0.1:8000/v1/tasks?full=1' > tasks.json
curl -s http://127.0.0.1:8000/v1/tasks/import -H 'Content-Type: application/json' -d @tasks.json
curl -s -X DELETE http://127.0.0.1:8000/v1/tasks/ticket-routing
```

 

Run 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](https://opendecisio.com/docs/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](https://opendecisio.com/docs/glossary/#prompt-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

 

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

 

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

 

- [Certify answers](https://opendecisio.com/docs/tasks/certify/): get answers the server stands behind, at the [error rate](https://opendecisio.com/docs/glossary/#error-target) you choose.
- [Watch for drift](https://opendecisio.com/docs/tasks/drift/): know when traffic moves away from your examples.
- [The HTTP API](https://opendecisio.com/docs/api/): the task routes, field by field.
