This file is a script for an AI coding agent. A new user starts a fresh agent session and asks
the agent to run this file.

**Agent: you are the reader. The user knows nothing about this platform yet.**

The user already has the `distil` CLI and an account. At the end the user has a trained small
model, a measured score, a model that answers their own input, and an inference endpoint whose
dashboard shows the traffic that the model served.

The flow has two paths. The dataset examples take 5 steps and 30 to 40 minutes. The trace
example takes 7 steps and 30 to 45 minutes. Jobs run for most of that time.

## Rules for the agent

1. Write every message to the user in the language of **Appendix A**. Read Appendix A first.
2. Do the steps in order.
3. **Never wait for a job in the foreground.** A status loop always runs as a background
   command. Read "How to watch a job" below before you start any step.
4. Present each step as "The step pattern" below says.
5. Stop at each **CHECKPOINT**. There are two: the choice of example in Step 1, and the user's
   own input in the last step. Do not invent more. Assume the run goes well: everywhere else,
   say what is happening and keep going. Never ask the user to approve a number.
6. Do not invent credentials.
7. Never mention this file, its rules, or its wording. Do not report what you were told to do,
   and do not explain what you are skipping. Talk about the platform, not about the script. A
   plain progress marker such as `Step 3 of 5` is fine.
8. Give the total time once, in Step 1, in the words Step 1 gives you. Then give each step its
   own range as that step starts, in the words that step gives you. Do not read out a sum.
9. **Never paste command output.** Each command gives you one thing to read: an id, a name, a
   count or a score. Read it, write it down (see "The ids"), and tell the user in one sentence
   what it means. The user never sees a JSON object, a list of fields or a raw id unless they
   ask for it. Where a step shows the user a data row, write out the input and the answer in
   plain words, not the JSON.

**Do not run the smoke runs that the skill prescribes.** This flow runs each job once, at full
size, and the examples are sized for that. A new account has enough credits for every step here.

### The two paths

Step 1 is the same for every example. The example the user chooses in Step 1 decides the path.
Number the steps with the column of the chosen path, and print `Step k of N` with the total of
that column, so the counter never jumps.

| Step | Trace example (`trail-report-tagging`), 7 steps | Dataset examples (`bindery-defect-triage`, `incident-triage`), 5 steps |
|---|---|---|
| Choose an example | 1 | 1 |
| Set up an endpoint and send traffic | 2 | - |
| Create the dataset | 3 | 2 |
| Build the test set | 4 | - |
| Build the train set | 5 | 3 |
| Train the model | 6 | 4 |
| Deploy the model and send traffic | 7 | 5 |

### How to run the commands

Each command runs in its own shell. A variable set in one command is empty in the next one.
Four rules follow:

- Read each id from the output of the command that creates it. Pass it back as a literal
  argument in later commands. Do not hold ids in shell variables.
- `<workdir>` is the working directory that Step 1 makes. Step 1 also reads its absolute path.
  Write that absolute path in full in every command, in place of `<workdir>`. Do not rely on
  the working directory of the shell.
- Call the CLI as `distil`, as every command below does. Step 1 makes that name work, and it
  gives two fallbacks for the machine where it does not. Stay on the rung Step 1 leaves you on
  for the whole run.
- Read JSON with `python3 <workdir>/read.py`, which Step 1 writes. This flow does not
  use `jq`.

`<example>` is the directory name of the chosen example, for example `bindery-defect-triage`.

### The dataset and its ids

Everything after Step 1 works on a **dataset**: one entity that holds the task description, the
settings, and up to three data files: `traces.jsonl` (recorded production traffic),
`train.jsonl` (the rows the student learns from) and `test.jsonl` (the rows every score is
measured on). Any of the three can be empty.

A job never changes a dataset. Each job that adds rows reads one dataset and writes a **new**
dataset, with the rows added and the traces it used removed. So the trace example walks a chain
of datasets, one id per job, and the dataset examples a chain of two. **Each step below says
which id it reads and which id it creates. Always pass the newest id to the next step.** If an
id is lost, `distil dataset list` shows every dataset with the operation that made it, and
`distil dataset show <id>` shows its parent.

### The ids

Every id and name you read goes into `<workdir>/ids.txt`, one per line, as `<name> <value>`, for
example `test-dataset-id 7ad51300-...`. Append it right after you read it. When a later step
needs an id, read it from that file, not from memory. The user does not see the file and does
not see the ids. If they ask where a result lives, give them the id then, in one line.

### The step pattern

Every step after Step 1 opens the same way. Do these parts, in this order:

1. Print the banner for the step. Copy the art exactly.
2. Give **one sentence**: what this step does. Add the time range the step gives you.
3. Run the commands, in the order the step lists them. If a command starts a job, read the id
   and start the background monitor.
4. Give **one paragraph**: what happens inside the step, and why it matters.
5. Offer the user the questions this step lists.
6. Read the output and show the result.

Part 3 sits between the two explanations on purpose. The job starts before you write the
paragraph, so the platform works while the user reads.

**A step with several commands runs them back to back.** Say one short line between commands,
such as "the records are on the machine, now they go up as traces". Do not open a new banner,
sentence or paragraph for each command. The step is the unit the user sees.

**Part 4 has one shape, and every step uses it.** Write three to five sentences, in this order:

1. What the platform runs in this step.
2. What it produces.
3. Why that matters for the model the user gets at the end.

Keep every step to that length. A step that runs longer does not get a longer paragraph. The
user compares the steps, and a paragraph that is twice the size of the last one reads as a
change of subject.

**Part 5 offers questions. It does not ask an open one.** Each step below lists three
questions in the user's own words. Print them as a short list and invite the user to pick one,
or to ask something else. Never ask "what do you want to know about this step, or about the
platform". A new user has no way to answer that.

Some steps run no job: their commands return in seconds, so they have no monitor and no
separate result. Each step says which kind it is.

### How to watch a job

A job takes minutes, not seconds. **Never poll for a status in the foreground.** A foreground
loop blocks you until it ends, and most agent tools kill a command at about 10 minutes. Either
way the user has a frozen session and no answer.

Run the loop as a background command instead. In Claude Code, set `run_in_background` on the
Bash tool. Every job except the deployment uses this loop:

```bash
f=<workdir>/<id>.start; [ -f "$f" ] || date +%s > "$f"; t0=$(cat "$f")
for i in $(seq 24); do
  s=$(distil <group> status --output json <id> 2>/dev/null | python3 <workdir>/read.py status 2>/dev/null)
  e=$(( $(date +%s) - t0 )); echo "elapsed $((e / 60))m$((e % 60))s ${s:-NO_ANSWER}"
  case "$s" in JOB_SUCCESS|JOB_FAILURE|JOB_STOPPED) break ;; esac
  sleep 20
done
```

The first line writes the start time of the job to a file the first time the loop runs, and
every later loop for the same id reads it back. So **every line the loop prints carries the
elapsed time of the job**, and a restarted loop keeps counting from the real start.

`<group>` is `dataset`, `teacher-evaluation`, or `slm`. A deployment reports
`deployment_status` and has no `status` field, so the last step gives a loop of its own.

`NO_ANSWER` means that the CLI could not reach the platform on that round, for example when the
machine lost its network. The job runs on regardless. If every line reads `NO_ANSWER`, tell the
user that the machine is offline and that the job continues on the platform, and start the loop
again when the network is back.

**One round is 20 seconds plus one status call, and 24 rounds are about 8 to 9 minutes.** Every
loop in this flow uses 24 rounds, because most agent tools kill a background command at about
10 minutes. A job that runs longer than that needs more than one loop, so the loop is built to
be restarted. Each step below says how many loops it expects.

**When the loop exits and the last line is not `JOB_SUCCESS`, `JOB_FAILURE` or `JOB_STOPPED`,
the job still runs.** Start the same loop again, with the same id, as a new background command.
Say one short line to the user first, in this shape: the job is still running, the elapsed
time, and the range the step gave, for example "still training, 9 minutes in, out of the 15 to
22 this step takes".

Four rules hold from the moment the monitor starts until the job ends:

- Do not read the monitor on a timer, and do not start a second one at the same time. Your tool
  reports a background command when it exits, and that report is your signal.
- Tell the user that the job runs in the background, then offer the questions the step lists.
- If the user asks where the job is, read the monitor once. Then give the latest line in words:
  the elapsed time, the range the step gave, and that the job still runs. The training step
  adds a progress figure of its own, which it describes.
- If the user has no question, end your turn and let the job run. An agent that runs with no
  user cannot end its turn, because that ends the run: it waits in the foreground until the
  background command's output ends, in checks shorter than 10 minutes each.

`JOB_SUCCESS` means the outputs are readable. `JOB_FAILURE` and `JOB_STOPPED` mean stop and
read the log:

```bash
distil <group> logs --output json <id> | python3 <workdir>/read.py logs
```

Any other value means the job still runs.

The traffic replays in Step 2 and in the last step are background commands too. They are not
status loops: each one ends when the last request returns. It prints how many requests
succeeded and how many failed, and on a second line how many answers matched the label of their
row. On the trace example that label is the production model's own answer, not a corrected
one, so the matched count measures nothing there and is never given to the user. On the dataset
examples the label is the test row's answer, and the matched count is the result.

---

## Step 1: Choose an example

**Print `Step 1 of 5` before anything else.** The example decides the path, and the user has
not chosen one yet, so the counter starts with the shorter total. When the user picks the trace
example at the end of this step, change the total to 7 from Step 2 on, and say so in the same
line as the new counter.

This step has four parts: say what the flow builds, get the machine ready, get the examples,
and let the user choose one. Only the last part asks the user anything.

### Say what the flow builds

Start with these four sentences, in your own words. They name every word the diagram uses:

- Your application calls a large language model today. Call it the **production model**.
- Every call it answers can be recorded. One recorded call, the request and the answer, is a
  **trace**.
- A second large model, the **teacher**, reads the traces and writes the correct answer for
  each one. Some of those corrected rows become the **test set**, which every score is measured
  on. The rest become training rows, and the teacher writes more rows like them.
- A small model, the **student**, trains on those rows for this one task. It is scored on the
  test set before and after training, and then it takes over the traffic.

Then show the user this diagram. Copy it exactly. It runs down the page rather than across, so
no part of it falls off the right edge of a narrow window.

```
  .--------------------.
  |  PRODUCTION MODEL  |   an address in front of the model you run today
  |  BEHIND ENDPOINT   |   it records every call
  |                    |   <-- the trace example starts here
  '---------+----------'
            |
            v
  .--------------------.
  |       TRACES       |   the recorded calls: request and answer
  '---------+----------'
            |
            v
  .--------------------.
  |                    |   one entity: the traces, the test rows,
  |      DATASET       |   the training rows, the task description
  |                    |   <-- the dataset examples start here
  '---------+----------'
            |
            v
  .--------------------.
  |      TEST SET      |   the teacher corrects some traces into
  |                    |   the rows every score is measured on
  '---------+----------'
            |
            v
  .--------------------.
  |     TRAIN SET      |   the teacher corrects more traces,
  |                    |   then writes more rows like them
  '---------+----------'
            |
            v
  .--------------------.
  |                    |   the student trains on the train set
  |   TUNED STUDENT    |   and is scored on the test set,
  |                    |   before training and after it
  '---------+----------'
            |
            v
  .--------------------.
  |   STUDENT BEHIND   |   the student answers the production
  |      ENDPOINT      |   traffic, on a dashboard
  '--------------------'
```

The diagram says what each step does, so do not read it back line by line. Add only the part it
cannot show, in two sentences: the student learns one task from the teacher's answers, and a
small model that matches the teacher on one task costs far less to run, so it takes the place
of the production model behind the same kind of endpoint.

Do not read out row counts. They change with the example, and the user has not chosen one yet.
Give the total time once here: 30 to 40 minutes for a dataset example, and 30 to 45 minutes
for the trace example. Jobs run for most of that time.

**Say once that the user can leave.** The jobs run on distil labs hardware, and they run whether
the user watches or not. Tell them that they can leave the terminal open and do something else,
and that the result waits for them when they come back. Then they read a quiet session as
normal, and not as a session that froze.

### Get the machine ready

**Run this part in silence.** Say one line before it, such as "one moment while I check the
machine", and one line after it when everything passed. Do not report the single checks, and
do not install anything.

**The working directory.** Everything this flow writes goes in one directory, inside the
directory where the user started the session. Make it and read its absolute path:

```bash
mkdir -p distil-onboarding && cd distil-onboarding && pwd
```

**The path that `pwd` prints is `<workdir>`.** Every command below writes `<workdir>`. Replace
it with that absolute path, every time. Each command runs in its own shell, so the `cd` above
does not carry over, and a relative path fails in the next command.

If the user names a different directory, use that one instead and read its path the same way.

**Make the `distil` name work.** The user installs the CLI before this flow. A fresh shell does
not always read the profile that the installer wrote. Work down this ladder and stop at the
first rung that prints a version. **Stay on that rung for the whole run.**

Rung 1. Run this on its own:

```bash
distil --version
```

If it prints a version, write `distil` in every command in this file, exactly as written.

Rung 2. If rung 1 failed, add the usual install directory to `PATH` for this session:

```bash
export PATH="$HOME/.local/bin:$PATH"
```

Then run `distil --version` again, as a **separate** command. The separate command is the test:
some agent tools keep one shell for the session and hold the export, and others start a new
shell each time and lose it. If the separate command prints a version, write `distil` in every
command in this file.

Rung 3. If rung 2 failed, call the CLI by its full path, in every command in this file:

```bash
~/.local/bin/distil --version
```

This rung needs no profile and no session state.

If no rung prints a version, the CLI is not installed. Tell the user in one line to install
it first, give them https://www.distillabs.ai/docs/getting-started/ways-to-use#the-cli, and
stop.

**Check the account:**

```bash
distil whoami --output json
```

If it prints an email, the account is ready. If it prints `{"error": ...}` instead, the user has
no session on this machine. Tell them in one line to sign up and sign in first, give them
https://www.distillabs.ai/docs/getting-started/authentication, and stop.

**Check the tools:**

```bash
command -v git; command -v python3; command -v uv
```

`git`, `python3` and `uv` must all exist. This flow cannot continue without them. If one is
missing, name it, and ask the user to install it.

**The JSON reader.** It is your own tool for reading command output, and it is not a part of
the platform. Never name it to the user:

```bash
cat > <workdir>/read.py <<'PY'
import json, sys
d = json.load(sys.stdin)
for k in sys.argv[1:]:
    d = d[int(k)] if isinstance(d, list) else d[k]
print(d if isinstance(d, str) else json.dumps(d, indent=2))
PY
```

Each argument is one key. `read.py teacher_performance binary` reads
`teacher_performance.binary`. A digit is a list index. A key with hyphens needs no special
form, so `read.py teacher_performance llm-as-a-judge-reference-free` works as written.

### Get the examples

Tell the user, in one sentence, that the example tasks and the scripts that send traffic live
in a public repository of distil labs, and that it goes into the working directory. Then run
this line. Do not wait for an answer:

```bash
git -C <workdir>/examples pull 2>/dev/null || git clone https://github.com/distil-labs/distil-labs-onboarding-examples.git <workdir>/examples
```

The `pull` runs first and fails quietly when the directory is not there yet, so the `clone`
handles the clean machine and keeps its own error output. Without this form, a user who starts
the flow a second time meets a `git clone` error on the first command of the day.

**If both halves fail, the directory exists but is not a clean clone.** A half-finished download
or a local edit does that. Tell the user what is in the way and ask before you remove anything.
Never delete their directory on your own.

### Choose the example

**Say why the choice exists, before you ask for it.** Give the user these three sentences in
your own words:

- The next steps train a real model, and a model is always a model of one task.
- The example supplies that task: the data, and the answer the model has to learn.
- The trace example starts from a production model's traffic, and the dataset examples start
  from a small labeled dataset. Both end with the trained model behind an endpoint.

**CHECKPOINT.** Ask this as a single-choice question box. In Claude Code the tool is
`AskUserQuestion`. Use the header `Example` and the question
`Which example do you want to train?`.

Build four options. Each option takes a short label, a description, and a preview. Copy the
labels and the descriptions below word for word.

**The description has to carry the option on its own.** Some clients show the label and the
description and drop the preview, so a reader who never sees the art still has to know what the
task is. Each description below has the same two parts, in this order: what the task is, and who
the option is a good fit for. Do not shorten them, and do not drop the second part. The second
part is what a user picks on.

Copy the previews too, art included, for the clients that do show them. If your client has no
question box at all, print the four labels and the four descriptions, and ask the user to reply
with a number.

#### Option 1

Label: `bindery-defect-triage (fastest, clearest result)`

Description: `A book bindery is a factory that binds printed pages into books. An inspection
line typed at a quality station goes in, and one instruction for those books comes out: rebind,
rework, press, quarantine, or mark and pass. Input: stn_qa defect=panel-skew sev=major
lot=L-12083 op=RB shift=swing press=3 units=807. Good fit if you want the whole flow in the
least time, and a result you can read at a glance: one right answer per row.`

Preview:

````
**bindery-defect-triage** — classification, 5 labels

A book bindery is a factory that binds printed pages into books.
This is the defect desk on its finishing floor. One inspection line
typed at a quality station goes in. One instruction comes out:
rebind, rework, press, quarantine, or mark and pass.

```
     ___     ___     ___     ___     ___     ___
    |:::|   |:::|   |:::|   |:::|   |:::|   |:::|
    |___|   |___|   |___|   |___|   |___|   |___|
 ==================================================>
                      ^
                .-----+-----.
                |  Q A  [*] |
                '-----------'
   stn_qa defect=panel-skew sev=major lot=L-12083
                      |
                      v
                 mark_and_pass
```

the fastest run, and the clearest result
````

#### Option 2

Label: `incident-triage`

Description: `A raw log line from a server monitoring system goes in. A full incident record
comes out, not one label: how serious it is, which system it belongs to, whether to wake
somebody up, and for how long to mute the duplicates. Input: 2026-08-05T14:22:11Z WARN auth-svc
msg="token refresh failed" count=847. Good fit if your own task returns a structured record
rather than one label, or if you want the largest jump from untrained to trained.`

Preview:

````
**incident-triage** — question answering

An alert triage service. One raw log line goes in. One incident
record comes out: how serious it is, which system it belongs to,
whether to wake somebody up, and how long to suppress duplicates.

```
 2026-08-05T14:22:11Z WARN auth-svc msg="token refresh failed"
                               |
                               v
               .-------------------------------.
               |  [!]  PAGE ONCALL             |
               |-------------------------------|
               |    severity            S2     |
               |    component  authentication  |
               |    suppress        30 min     |
               |_______________________________|
               |  o o o    [===============]   |
               '-------------------------------'
```

the longest jump from an untrained model to a trained one
````

#### Option 3

Label: `trail-report-tagging (full loop, from production traffic)`

Description: `Somebody walks a section of hiking trail and writes up what they found, in their
own words. The standard condition tags come out as a JSON list. This one runs the full loop: a
production model answers through an endpoint, its traffic becomes the training data, and the
trained model takes over that traffic behind a new endpoint. Input: Trip report, 2026-July 19.
We covered 8.4 miles out and back. The footbridge at the lower crossing is gone entirely. Good
fit if you plan to replace a model you already run in production. It is the longest run.`

Preview:

````
**trail-report-tagging** — question answering, from production traffic

Somebody walks a section of hiking trail and writes up what they
found, in their own words. The standard condition tags that apply
come out as a JSON list. A production model answers through an
endpoint first, and its traffic becomes the training data.

```
                         /\
                    /\  /  \           /\
               /\  /  \/    \     /\  /  \
          /\  /  \/          \___/  \/    \  /\
    _____/  \/                               \/  \_____

                     .---------------.
                     |   TRAIL  7    |
                     |   bridge  X   |
                     '-------+-------'
    - - - - - - - - - - - - -+- - - - - - - - - - - - -

     "The footbridge at the lower crossing is gone entirely."
                             |
                             v
                   {"tags": ["BRIDGE_OUT"]}
```

the full loop, from production traffic to the trained model
````

#### Option 4

Label: `my own data`

Description: `You already have the data for a task of your own, either a set of labeled
examples or the logged traffic of a model you want to replace. This leaves the worked examples
and builds a model for that task instead. Good fit if your data is ready now and you would
rather spend the run on your own task than on a worked one.`

Preview:

````
**my own data** — your task, not a worked example

Pick this when you have your own data ready. The steps are the
same ones the examples run through, with your task in place of
theirs. The skill asks what you have and drives the pipeline.

A worked example is the faster way to see the whole flow first.
Nothing stops you doing your own task afterwards.
````

### After the choice

For `bindery-defect-triage` and `incident-triage`, continue with **Create the dataset**, as
Step 2 of 5. For `trail-report-tagging`, continue with **Set up an endpoint and send traffic**,
as Step 2 of 7, and say in the same line that this path has 7 steps.

For **`my own data`, or for any answer the user typed themselves, stop following this file.**
The distil labs skill drives that build. If `distil-cli` is available in this session, read
`workflows/build-a-model.md` in it, ask what the user has (an LLM in production, or files: a
trace file, a labeled dataset, a test set, or any mix of those), and follow the workflow from
that entry point. If the skill is absent, tell the user to install it with this command, and to
start a new session that asks for a model of their own data:

```
distil skill install
```

Say plainly that you are leaving the worked examples. Do not run a worked example first as a
warm-up, and do not treat the typed answer as an error.

---

## Set up an endpoint and send traffic

Trace example only: Step 2 of 7.

**Banner.** Print this first. Copy it exactly:

```
 ___ _  _ ___  ___  ___ ___ _  _ _____
| __| \| |   \| _ \/ _ \_ _| \| |_   _|
| _|| .` | |) |  _/ (_) | || .` | | |
|___|_|\_|___/|_|  \___/___|_|\_| |_|
 set up an endpoint and send traffic
```

**One sentence.** An inference endpoint goes in front of the production model, and the
example's 200 trip reports go through it, as an application in production would send them.
**Time: 1 to 4 minutes.** Give that range to the user.

**Commands.** Create the endpoint first. It has a fallback model and no primary, so the fallback
answers every call. Here `openai/gpt-oss-120b` stands in for the model that runs in production
today:

```bash
distil inference-endpoint create --output json --name trail-production \
  --fallback-model "openai/gpt-oss-120b" | python3 <workdir>/read.py unique_endpoint_name
```

Read the unique endpoint name. The platform adds a suffix to `trail-production`, and every later
command takes the full name. Then create an API key and link it to the endpoint. `<key-name>` is
`onboarding-<example>-<today>`, with today's date in the form `2026-10-04`. Key names are unique
in an account, so if that name exists, add `-2` and use the new name in every later command:

```bash
distil api-keys create --output json --no-file <key-name> > <workdir>/api-key.json &&
distil inference-endpoint link-api-key <unique-endpoint-name> <key-name>
```

The secret is shown only once, which is why the command keeps it in `<workdir>/api-key.json`.
The last step uses the same key. A newly linked key takes up to a minute to start working, so
if the replay below refuses every request at once, wait a minute and start it again.

Then send the traffic. Run this as a background command. The key is read and used inside one
command, which is why it works here:

```bash
key=$(python3 <workdir>/read.py secret < <workdir>/api-key.json) &&
uv run <workdir>/examples/replay_traffic.py --base-url https://inference.distillabs.ai/v1 \
  --api-key "$key" --model <unique-endpoint-name> \
  --input <workdir>/examples/trail-report-tagging/traces-input/traces.jsonl --concurrency 32
```

**One paragraph.** An inference endpoint is an OpenAI-compatible address that an application
calls in place of its model provider. This one passes every call to the production model and
returns the answer unchanged, so the application sees no difference, and it keeps a record of
each call: the request and the answer. Each line of the input file is one request that the
production application sent, a system prompt and a trip report, and the script sends them all.
At the end the endpoint holds 200 records of real traffic, and the next step turns them into
the dataset this model is built from. A real application collects its training data the same
way, with no change other than the address.

**Questions.** Tell the user that the traffic runs in the background. Then offer these three,
and answer until the command ends:

- What changes in my application when it calls an endpoint?
- What does the endpoint record, and who can see it?
- Does the production model answer correctly every time?

**The result.** When the command ends, give the count of requests that succeeded, and nothing
else from the summary. If some failed, see Troubleshooting. A few failures do not stop the flow.

---

## Create the dataset

Step 3 of 7 on the trace example, Step 2 of 5 on the dataset examples.

**Banner.** Print this first. Copy it exactly:

```
 ___   _ _____ _   ___ ___ _____
|   \ /_\_   _/_\ / __| __|_   _|
| |) / _ \| |/ _ \\__ \ _|  | |
|___/_/ \_\_/_/ \_\___/___| |_|
 create the dataset
```

Every later step starts from a **dataset**: the task description, the settings, and up to three
data files: traces, training rows and test rows. The trace example creates it from the
endpoint's records: the traces are filled, and the training and test rows are empty until the
next steps fill them. The dataset examples upload a directory that already holds the training
and test rows and has no traces. Do the variant for the chosen example only. Both variants run
no job.

### Trace example: from the endpoint's records

**One sentence.** The endpoint's records come down to the machine, go up to the platform as
traces, and become one dataset with the task description and the settings. **Time: 1 to 2
minutes.** Give that range to the user.

**Commands.** Run these one at a time. Write today's date in place of `<today>`, in the form
`2026-10-04`:

```bash
distil inference-endpoint download-traces --all --from-start-time <today> \
  --file-name <workdir>/trail-records.jsonl <unique-endpoint-name>
```

```bash
uv run <workdir>/examples/records_to_traces.py --input <workdir>/trail-records.jsonl \
  --output <workdir>/trail-traces.jsonl
```

```bash
distil traces upload --traces <workdir>/trail-traces.jsonl
```

The upload prints `Prepared traces created. ID: <traces-id>`. Read that id and use it at once:

```bash
distil dataset create-from-traces \
  --config <workdir>/examples/trail-report-tagging/traces-input/config.yaml \
  --job-description <workdir>/examples/trail-report-tagging/traces-input/job_description.json \
  <traces-id>
```

This prints `Dataset created. ID: <dataset-id>`. Read that id: it is `<dataset-id>`, the root of
the chain, and the next step reads it.

**One paragraph.** The download holds one record per call: the request, the answer, and whether
the call succeeded. The conversion keeps the calls that succeeded and writes each one as a
conversation, the shape the platform reads, and the upload stores them on the platform. The
dataset puts the traces next to the task description and the settings. The task description is
the system prompt the production model answered with, so the trained model learns the same
instructions, and the settings say which teacher corrects the traces and how many of them each
step uses. From here on, every step reads one dataset and writes a new one, so the traffic is
never changed in place.

**Questions.** Offer these three, and answer the one the user picks:

- What is in the task description?
- Why are the training rows and the test rows empty?
- Which settings matter for the next steps?

**The result.** Continue to the test set, as Step 4 of 7.

### Dataset examples: upload the dataset

**One sentence.** The example's dataset goes up to the platform, and every later step reads it
from there. **Time:** this one returns in seconds. Say so.

**Commands.** Read two real rows first, so the user sees what the platform is given:

```bash
head -2 <workdir>/examples/<example>/base-input/train.jsonl
```

Do not show the JSON. For each row, write out the input that goes in and the answer that comes
out, in plain words, one short line each. Keep it to those four lines. Then upload the
directory:

```bash
distil dataset create --data <workdir>/examples/<example>/base-input
```

It prints `Upload successful. Dataset ID: <dataset-id>`. Read that id: it is `<dataset-id>`,
and the next two steps read it.

**One paragraph.** The directory holds the four parts of this dataset: the labeled rows the
teacher learns the task from, the test rows that every score is measured on, the task
description, and the settings. It has no traces, because this example starts from labeled data
and not from traffic. The command uploads the four files and validates them. Everything later in
this flow descends from this one upload.

**Questions.** Offer these three, and answer the one the user picks:

- Where did these rows come from?
- Why are so few rows enough to train on?
- What happens if my own data looks nothing like this?

**The result.** Continue to the train set, as Step 3 of 5.

---

## Build the test set

Trace example only: Step 4 of 7.

**Banner.** Print this first. Copy it exactly:

```
 _____ ___ ___ _____   ___ ___ _____
|_   _| __/ __|_   _| / __| __|_   _|
  | | | _|\__ \ | |   \__ \ _|  | |
  |_| |___|___/ |_|   |___/___| |_|
 build the test set
```

**One sentence.** The teacher corrects 50 of the traces into the test set, and the result is a
new dataset. **Time: 2 to 5 minutes.** Give that range to the user.

**Command and monitor.** Start the job on the root dataset:

```bash
distil dataset relabel-traces-test --output json <dataset-id> | python3 <workdir>/read.py id
```

This creates a second dataset, the one with the test set. Read its id: it is
`<test-dataset-id>`. Then start the monitor from "How to watch a job" as a background command,
with `dataset` as the group and `<test-dataset-id>` as the id. One loop covers this job.

**One paragraph.** The teacher reads 50 of the recorded requests and writes the correct answer
for each one, where the production model was sometimes wrong. Those rows become the test set of
the new dataset, and every score in this flow is measured on them. The 50 traces leave the new
dataset, so no training row is ever built from a test request. The 150 traces that remain wait
for the training rows.

**Questions.** Tell the user that the job runs in the background. Then offer these three, and
answer until the monitor reports that the job ended:

- Why not test on the production model's own answers?
- Who is the teacher, and why trust its answers?
- Where did the 50 traces go?

**The result.** When the monitor reports `JOB_SUCCESS`, read how many rows the test set holds.
The download costs nothing:

```bash
distil dataset download --destination <workdir>/data <test-dataset-id> && grep -c '' <workdir>/data/test.jsonl
```

Give that one number. Fewer than 50 is normal: the teacher drops a trace it cannot correct
into a valid row. `grep -c ''` is used rather than `wc -l`, because `wc -l` counts one row short
when the file has no final newline. Keep the number: later steps say "measured on N test rows".

---

## Build the train set

Step 5 of 7 on the trace example, Step 3 of 5 on the dataset examples.

**Banner.** Print this first. Copy it exactly:

```
 _____ ___    _   ___ _  _   ___ ___ _____
|_   _| _ \  /_\ |_ _| \| | / __| __|_   _|
  | | |   / / _ \ | || .` | \__ \ _|  | |
  |_| |_|_\/_/ \_\___|_|\_| |___/___| |_|
 build the train set
```

**One sentence.** On the trace example: the teacher corrects 140 more traces into training rows,
and the result is a new dataset. **Time: 2 to 5 minutes.** On the dataset examples: the teacher
writes the training data that the student learns from, and the result is a new dataset.
**Time: 3 to 8 minutes.** Give the range for the chosen example to the user.

### Start the teacher evaluation alongside

This step also starts a job the user does not wait for: a **teacher evaluation**, in which the
teacher answers the test set on its own and the platform scores it. It runs on the platform
while the train set is built, and its score appears in the final table of the training step.
**Start it, write down its id, and do not monitor it.** Do not announce it as a step. If the
user asks what the teacher scored, say that the score comes with the training result.

The dataset examples read `<dataset-id>`; the trace example reads `<test-dataset-id>`:

```bash
distil teacher-evaluation create-from-dataset --output json <the dataset id> | python3 <workdir>/read.py id
```

Read the id: it is `<teacher-evaluation-id>`.

**Trace example only: score the production model too.** The production model behind the
endpoint is `openai/gpt-oss-120b`, and the platform knows it as the teacher
`openai.gpt-oss-120b`. Run a second evaluation with it as the teacher. The settings of the
dataset are read back, the teacher line is changed, and the whole file is sent back:

```bash
distil dataset download-metadata --destination <workdir>/baseline <test-dataset-id> &&
sed -i.bak 's/^  teacher_model_name: zai.glm-5.3-low-thinking$/  teacher_model_name: openai.gpt-oss-120b/' <workdir>/baseline/config.yaml &&
grep -n "teacher_model_name" <workdir>/baseline/config.yaml
```

The `grep` prints two lines, one per section of the file, and both must show
`openai.gpt-oss-120b`. Then start the second evaluation:

```bash
distil teacher-evaluation create-from-dataset --output json \
  --config <workdir>/baseline/config.yaml <test-dataset-id> | python3 <workdir>/read.py id
```

Read the id: it is `<baseline-evaluation-id>`. Do not monitor this one either.

### Trace example: relabel

**Command and monitor.** Start the relabelling job on the dataset that holds the test set:

```bash
distil dataset relabel-traces-train --output json <test-dataset-id> | python3 <workdir>/read.py id
```

Read the id: it is `<final-dataset-id>`, the dataset the student trains on. Start the monitor
as a background command, with `dataset` as the group. One loop covers this job.

### Dataset examples: generate

**Command and monitor.** Start the generation job on the uploaded dataset:

```bash
distil dataset generate-synthetic-data-train --output json <dataset-id> | python3 <workdir>/read.py id
```

Read the id: it is `<final-dataset-id>`, the dataset the student trains on. Start the monitor
as a background command, with `dataset` as the group. One loop covers this job.

### The paragraph, the questions and the result

**One paragraph.** Give the version for the chosen example.

On the trace example: the 150 traces that the test set did not use go through the same
correction as the test set, and 140 of them become the training rows: real production requests
with the teacher's answers. They are written the way the production traffic is written, so they
teach the student exactly the requests it will serve. The test set is copied through unchanged,
so every score stays comparable.

On the dataset examples: the teacher writes a few hundred new training rows. The rows already
in the dataset fix the format and the style, and the teacher varies everything else, so the
student sees many more cases of the task than the upload held. The test set is copied through
unchanged, so every score stays comparable.

**Questions.** Tell the user that the job runs in the background. Then offer these three, and
answer until the monitor reports that the job ended. On the trace example:

- Why correct the traces instead of using the production answers?
- Are 140 rows enough to train on?
- Can I see the rows it wrote?

On the dataset examples:

- How can data the teacher invented teach anything real?
- What stops the teacher from writing rows that all look the same?
- Can I see the rows it wrote?

**The result.** When the monitor reports `JOB_SUCCESS`, download the dataset and read one of
the new rows. The download costs nothing. The new rows come after the rows the dataset held
before, so the last line is always one the teacher wrote: a corrected trace on the trace
example, a generated row on the dataset examples:

```bash
distil dataset download --destination <workdir>/final <final-dataset-id> &&
grep -c '' <workdir>/final/train.jsonl && tail -1 <workdir>/final/train.jsonl
```

Give the row count as the size of the train set. **Show the input and the label of the row.
Drop every field that is empty or `null`.** An empty field reads to a new user as something that
failed. Read the row yourself, then write out the parts that carry data.

On the dataset examples, also read the size of the test set now, so the next step can say what
its scores were measured on:

```bash
grep -c '' <workdir>/final/test.jsonl
```

Keep the number. Do not state it from memory, because it differs between examples.

---

## Train the model

Step 6 of 7 on the trace example, Step 4 of 5 on the dataset examples.

**Banner.** Print this first. Copy it exactly:

```
 _____ ___    _   ___ _  _ ___ _  _  ___
|_   _| _ \  /_\ |_ _| \| |_ _| \| |/ __|
  | | |   / / _ \ | || .` || || .` | (_ |
  |_| |_|_\/_/ \_\___|_|\_|___|_|\_|\___|
 model training
```

**One sentence.** A small student model trains on the train set, and the platform scores it on
the test set before and after. **Time: 15 to 22 minutes: the long wait.** Give that range to the
user.

**Command and monitor.** Start the job:

```bash
distil slm create-from-dataset --output json <final-dataset-id> | python3 <workdir>/read.py id
```

Read the id: it is `<slm-id>`. Then start the monitor as a background command, with `slm` as
the group. This job needs two or three loops.

**Progress.** The job has three parts, in this order: the training itself, which writes its
progress to the job log, then the scoring of the untrained student, then the scoring of the
trained student. Scoring the untrained student takes about 4 minutes and scoring the trained
student about 8, almost all of it starting the server that answers the test rows. When the user asks where the training is, or when you restart
the monitor, read the latest epoch figure:

```bash
distil slm logs --output json <slm-id> | python3 <workdir>/read.py logs | grep -oE "'epoch': [0-9.]+" | tail -1
```

It prints a line such as `'epoch': 0.8`. The student trains for 1 epoch on
`bindery-defect-triage` and for 2 epochs on the other examples, so give the user that figure as
progress, in words: "0.8 of 2 epochs done, 7 minutes in". Before the first such line appears, the
platform is loading the model. The training itself takes a few minutes, so the figure moves fast.
Once it reaches the number of epochs, training is done and the two scoring runs follow: 12 to 13
minutes together. Say which part is happening.

**One paragraph.** The student trains on the training rows: one pass over all of them (1 epoch)
on `bindery-defect-triage`, two passes on the other examples. The platform then scores the untrained student on the test set, which is the starting
point, and scores the trained student on the same rows. The two scores show what the training
did, on the same test rows the teacher answered.

**Questions.** Tell the user that the job runs in the background. This wait is the longest one,
and it is where the user learns the most. Offer these three first, and answer the one the user
picks:

- How can a small model match a large one?
- What is actually changing inside the model right now?
- What do I do with this model after the run?

Keep the five points below in reserve. Give one at a time, after you answer a question, or when
the user has nothing to ask:

1. **What an epoch is.** One epoch is one pass over all the training rows. More passes let the
   model learn the rules. Too many passes make it memorize.
2. **The size difference.** The student is `Qwen3-0.6B`: 0.6 billion parameters. The teacher
   is many times larger.
3. **Why this works.** The teacher knows a little about everything. The student needs one task.
   The training rows carry the teacher's answers for that one task.
4. **What the user gets.** A small model answers faster and costs less to serve. It also runs
   on hardware that cannot hold the teacher.
5. **What comes after.** The last step puts the trained model behind an endpoint, and the user
   sends it traffic.

### Read the result

Wait for the monitor to report `JOB_SUCCESS` before you show anything here. This is the payoff
of the whole run, and it is lost if it arrives underneath a status feed. Before that, the
metrics command exits with an error that says the job has not finished.

Both student numbers come out of one response, so fetch it once:

```bash
distil slm metrics --output json <slm-id> > <workdir>/metrics.json
python3 <workdir>/read.py base_model_performance binary < <workdir>/metrics.json
python3 <workdir>/read.py tuned_model_performance binary < <workdir>/metrics.json
```

For classification (`bindery-defect-triage`) the primary metric is `binary`, the exact match on
the label. For the other examples it is `llm-as-a-judge-reference-free`, so change the last
argument of the two read lines, and reuse the same file.

Then read the teacher's score, from the evaluation that the train set step started. It ended
long ago:

```bash
distil teacher-evaluation metrics --output json <teacher-evaluation-id> | python3 <workdir>/read.py teacher_performance binary
```

Use the same metric name as above. If the command says that the job is still running, read its
status, start a monitor for it, and show the table when it ends. If the evaluation failed, show
the table without the teacher row and say in one line that the teacher's score is missing.

On the trace example, read the production model's score the same way, with
`<baseline-evaluation-id>`.

**Give one number per model and one summary line. Never paste the per-class block.** The metrics
object holds one dictionary per class label, and near-identical dictionaries teach a new user
nothing. One line such as "every class scored between 0.94 and 1.00" carries the same fact. This
holds every time you read metrics.

**Read the number the job returns. Never state a score before you have read it.** Generation and
judging both run at a non-zero temperature, so no two runs land on the same decimal. State the
number you read, say what it bounds, then continue. Do not ask the user to approve it.

Put the numbers side by side, in this shape. Fill every cell from a number you read, and from
nothing else. On the trace example, add the production model's score as a fourth row:

| | Score |
|---|---|
| Student, before training | *read it here* |
| Student, after training | *read it here* |
| Teacher | *read it here* |
| Production model (trace example only) | *read it here* |

Say what each row is, in one line each: the teacher writes the answers the student learns from,
so the student rarely goes far past it, and the production model is the score the student
replaces. If the production model scores at or above the teacher, say so plainly: on this test
set the production model is as good as the teacher, and the student's goal is to come close to
it at a fraction of the cost. If the student scores at or above the teacher, say that both are at the top of what
this test set can measure, and that a difference of a few hundredths is run-to-run variation,
not a ranking.

**Put the test row count under the table, in one line**, for example "measured on 50 test rows".
Two scores of 1.00 with no denominator read as a demo, and the run is not a demo. You read the
count in the test set step on the trace example and in the train set step on the dataset
examples.

Then make the argument in two sentences, from the numbers in front of you: how far training moved
the student toward the teacher, and, on the trace example, whether it beats the production model
it replaces. Say what this run did, not what a run usually does. If the student lands below the
production model, say so plainly, and say that this is what the next round of training exists
for: more training rows, or more passes over them, on the same test set. Do not soften the
number, and do not skip the last step because of it.

---

## Deploy the model and send traffic

Step 7 of 7 on the trace example, Step 5 of 5 on the dataset examples.

**Banner.** Print this first. Copy it exactly:

```
 ___  ___ ___ _    _____   __
|   \| __| _ \ |  / _ \ \ / /
| |) | _||  _/ |_| (_) \ V /
|___/|___|_| |____\___/ |_|
 deploy the model and send traffic
```

**One sentence.** The platform serves the trained model, a new endpoint puts it in front of the
traffic, and the dashboard shows the calls it answers. **Time: 4 to 10 minutes to start, then a
few minutes of traffic.** Give that to the user.

**Recap the task first, in two sentences.** A long run sits between the choice of example and
this step, and by now the user has lost the thread of what the model is for. Say what goes in
and what comes out, in the words of the example they chose, before you ask them for anything.
For `bindery-defect-triage`: one inspection line from a quality station goes in, and one of five
instructions comes out. Without this recap the request for "an input in the same shape" has no
subject.

This step follows the same pattern as the others, with one difference: the checkpoint comes
before the command. A deployment stops after one hour with no traffic, and it cannot restart, so
**collect the user's input before you create the deployment, not after.**

**Each example trains on exactly one input format, and the model reads that format alone.** Do
not tell the user that other shapes work. Read the real format out of the test rows you
downloaded in the train set step, rather than describing it from memory:

```bash
head -1 <workdir>/final/test.jsonl | python3 -c 'import json, sys; m = json.load(sys.stdin)["messages"]; print("input:", m[-2]["content"]); print("expected:", m[-1]["content"])'
```

It prints the user message, which is one real input in the one shape the model accepts, and the
expected answer. The row also starts with the long system message the model was trained with;
the command leaves it out.

**CHECKPOINT.** Show the user that input, and only the input, not the JSON around it, as the
pattern to follow, and ask for an input of their own in the same shape. Name the fields that decide the answer, so they change those and not
only the decoration:

- `bindery-defect-triage`: the defect alias (`defect=`) and the severity (`sev=`). The other
  fields do not change the answer.
- `incident-triage`: the service alias and the level (`ERROR`, `WARN`, ...). The message and the
  count do not change the answer. The model knows only the service aliases that the task
  description lists. Read them from `<workdir>/examples/incident-triage/base-input/job_description.json`
  and give the user five of them to choose from, with the one on the line you showed.
- `trail-report-tagging`: the conditions the report describes. The date and the distance do not
  change the answer.

Hold their input. You send it after the endpoint exists.

### The API key

The student's endpoint needs an API key, and the key is created before the deployment, so that
nothing waits on the user while the deployment counts down its idle hour.

On the trace example, the key from Step 2 already exists. Skip to "Deploy the model".

On the dataset examples, create the key. `<key-name>` is `onboarding-<example>-<today>`, with
today's date in the form `2026-10-04`. Key names are unique in an account, so if that name
exists, add `-2` and use the new name in every later command:

```bash
distil api-keys create --output json --no-file <key-name> > <workdir>/api-key.json || cat <workdir>/api-key.json
```

The secret is shown only once, which is why the command keeps it in `<workdir>/api-key.json`.
When the command fails, the error is in that file, and the `cat` prints it. An account holds at
most 5 API keys. If the error says so, this is the one place where a third question to the user
is allowed: run `distil api-keys list`, show the names, ask which unused key to remove, run
`distil api-keys delete --force <name>` (`--force` because there is no terminal to confirm in),
and create the key again.

### Deploy the model

**Command and monitor.** Start the job:

```bash
distil deployment create-from-slm --output json <slm-id> | python3 <workdir>/read.py id
```

Read the id. Then start this loop as a background command. **A deployment reports
`deployment_status`, and it has no `status` field:**

```bash
f=<workdir>/<deployment-id>.start; [ -f "$f" ] || date +%s > "$f"; t0=$(cat "$f")
for i in $(seq 24); do
  s=$(distil deployment status --output json <deployment-id> | python3 <workdir>/read.py deployment_status)
  e=$(( $(date +%s) - t0 )); echo "elapsed $((e / 60))m$((e % 60))s $s"
  case "$s" in JOB_SUCCESS|JOB_FAILURE|JOB_STOPPED) break ;; esac
  sleep 20
done
```

One or two loops cover this job.

**One paragraph.** A deployment serves the trained model with vLLM on distil labs hardware: the
student's base model, with the trained LoRA adapter on top. `JOB_SUCCESS` means that the model
accepts requests. An application never calls the deployment itself. It calls an inference
endpoint, which sends each request to the deployment and to a fallback model only when the
deployment fails. This is the step that puts the student where the production model was.

**Questions.** Tell the user that the deployment starts in the background, and that their input
goes to the model as soon as it is ready. Then offer these three, and answer until the monitor
reports that the job ended:

- Where does this model run, and can I run it myself?
- What does it cost to keep serving?
- What happens when the deployment cannot answer?

### Put the endpoint in front of it

**Wait for the monitor to report `JOB_SUCCESS`. Create the endpoint only after that.** Before it,
the command answers `Deployment is not ready`.

```bash
distil inference-endpoint create-from-deployment --output json --name <example>-student \
  --fallback-model "openai/gpt-oss-120b" <deployment-id> | python3 <workdir>/read.py unique_endpoint_name
```

Read the unique endpoint name. This is `<student-endpoint-name>`. Link the key to it:

```bash
distil inference-endpoint link-api-key <student-endpoint-name> <key-name>
```

A newly linked key takes up to a minute to start working, so if the first call below is
refused, wait a minute and run it again.

### Ask the model two questions

Download the model's client first. It costs no credits:

```bash
distil slm download-metadata --destination <workdir>/model <slm-id>
ls <workdir>/model/
```

This writes `config.yaml`, `job_description.json` and `model_client.py`. `model_client.py`
carries the decoding settings the model expects, so it is the correct way to call the model. It
adds no system prompt: the conversation it sends must start with the system message the model was
trained with, which is the first message of its `DEFAULT_CONVERSATION`. If any of the three files
is missing, run the command again.

Now send the canned input. The second line builds the conversation: the trained system message
from the client, then the input as the user message.

```bash
key=$(python3 <workdir>/read.py secret < <workdir>/api-key.json) &&
conversation=$(uv run --with openai python -c 'import json, sys; sys.path.insert(0, sys.argv[1]); from model_client import DEFAULT_CONVERSATION; print(json.dumps([DEFAULT_CONVERSATION[0], {"role": "user", "content": sys.argv[2]}]))' \
  <workdir>/model '<the input from the line you showed>') &&
uv run --with openai <workdir>/model/model_client.py --base-url https://inference.distillabs.ai/v1 \
  --api-key "$key" --model <student-endpoint-name> --conversation "$conversation"
```

The client needs the `openai` package, and `--with openai` gives it one. It prints the assistant
message as JSON, for example `{"content":"quarantine_lot","role":"assistant"}`. The answer is
its `content`. Give the user the answer, not the JSON. If the message also carries `reasoning`
fields, the fallback model answered, because the endpoint was created seconds ago and the
student was not reachable yet. Wait a minute and send the same input again.

**Write the input exactly.** It is one argument in single quotes, so double quotes stay as they
are, and each apostrophe of the input becomes `'\''`. For example, the first `incident-triage`
test row, `2026-08-05T04:11:09Z ERROR authsvc msg="password grant failures" count=12400`, goes in
as:

```
<workdir>/model '2026-08-05T04:11:09Z ERROR authsvc msg="password grant failures" count=12400'
```

For `bindery-defect-triage`, use this input, and give the explanation below it in the user's
words: `stn_qa defect=hinge-gap sev=major lot=L-9032 op=KT shift=day press=4 units=731`. That
line carries all seven fields, in the order every trained row uses. A hinge gap is a defect of
the book's case, and it is marked major. On severity alone the instruction is a rework. The
task description has a rule with higher priority: a case defect at major severity holds the
whole lot. So the correct answer is `quarantine_lot`. The other five fields are decoration, and
none of them changes the answer. The user has not seen the rules, so name the rule in those
words and do not number it.

For the other examples, use the input of the line you showed, and compare the answer with the
expected answer on that line.

Now run the same command with the input the user gave you at the checkpoint. This is the moment
the flow exists for.

### Send traffic through the student

Run this as a background command. On the trace example, the production inputs go through the
student's endpoint as raw requests. Their system prompt is the job description's
`task_description`, which is the system prompt the model was trained with, so a raw request
gets the same answer as the client:

```bash
key=$(python3 <workdir>/read.py secret < <workdir>/api-key.json) &&
uv run <workdir>/examples/replay_traffic.py --base-url https://inference.distillabs.ai/v1 \
  --api-key "$key" --model <student-endpoint-name> \
  --input <workdir>/examples/trail-report-tagging/traces-input/traces.jsonl --limit 50 --concurrency 32
```

On the dataset examples, the test rows go through the model's client. Each row already starts
with the system message the model was trained with, and the script puts the one from the
client's `DEFAULT_CONVERSATION` first on any row without it:

```bash
key=$(python3 <workdir>/read.py secret < <workdir>/api-key.json) &&
uv run <workdir>/examples/replay_traffic.py --base-url https://inference.distillabs.ai/v1 \
  --api-key "$key" --model <student-endpoint-name> \
  --input <workdir>/final/test.jsonl --client <workdir>/model/model_client.py --limit 50 --concurrency 16
```

When the command ends, it prints `sent N, ok M, failed K`, and on the next line `matched X of N`:
the number of answers that equal the label of their row. On the dataset examples, give the user
the matched count as the result of the replay, with the number of requests it was measured on.
On the trace example, give the count that succeeded only: the labels in that file are the
production model's answers, so a match says nothing about the student. The score in the table
is its measurement.

### Show the dashboard

The dashboard is a web page, and the CLI session does not sign the browser in. Ask the user to
sign in at https://app.distillabs.ai with the same account they use for the CLI. Then give them
the page of the student's endpoint, and ask them to open it:

```
https://app.distillabs.ai/inference-endpoints/<student-endpoint-name>
```

The page shows the usage of the endpoint: the requests the user just sent, over time. Say so in
one sentence. You cannot see the page, so do not describe it beyond that. On the trace example, also give them the page of the first endpoint,
`https://app.distillabs.ai/inference-endpoints/<unique-endpoint-name>`, which holds the 200 calls
that the production model answered. The two pages side by side show the same traffic, first on
the production model and then on the student.

### Delete the deployment

```bash
distil deployment delete <deployment-id>
distil deployment status --output json <deployment-id> | python3 <workdir>/read.py endpoint_status
```

`deployment_status` stays `JOB_SUCCESS` after the delete, which is why the check reads
`endpoint_status` instead. A value of `stopped` confirms that the model is down. If it still
reads `running`, run `sleep 30` and the same status read together as one background command,
and read the result when it ends.

A deployment is a session for trying the model, not a permanent endpoint. It stops after six
hours, or after one hour with no traffic. It cannot restart. After it stops, the endpoint answers
every call from the fallback model, and the endpoint and its dashboard stay. For a permanent
deployment, the user emails contact@distillabs.ai.

### What comes next

**The trained model is theirs.** The download is a tarball that holds the trained LoRA adapter
under `model-adapter/`. It goes in a directory of its own, so it does not replace the files of
the client:

```bash
distil slm download --destination <workdir>/model-download <slm-id>
```

**Ask the user before you run this. It writes the tarball to their disk.** To serve it locally,
vLLM loads the student's base model from HuggingFace and puts the adapter on top. The local route
is at https://www.distillabs.ai/docs/deployment/local-deployment.

**The chain of datasets is theirs too.** Every dataset of this run is still on the platform, and
`distil dataset list` shows them with the operation that made each one. A later model starts
from the last one: more training rows are one more `generate-synthetic-data-train` run on it,
and the trained model's own traffic is the next round of traces. The loop is described at
https://www.distillabs.ai/docs/build/improving.

**Build a model for their own task.** Tell the user to start a new session and ask for a model
for their own data. The distil labs skill asks what they start from: an LLM in production, or
files on disk (a trace file, a labeled dataset, a test set, or any mix). Then it drives the same
loop. If the skill is not installed, `distil skill install` installs it.

---

## Troubleshooting

| Symptom | Cause | Action |
|---|---|---|
| `distil: command not found` | `PATH` does not hold the install directory in this shell | Go back to the ladder in Step 1 and move down one rung. Do not write to a shell profile |
| `git clone` fails with `already exists and is not an empty directory` | The directory is there but is not a clean clone, so the `pull` half failed too | Say what is in the way. Ask the user before you remove the directory |
| A version notice appears on stderr | The CLI is out of date | Not an error. `--output json` still parses |
| `ValueError: The conversation must start with a system message` | A conversation was sent to `model_client.py` without the trained system message | Build the conversation with the command in "Ask the model two questions", which puts the first message of `DEFAULT_CONVERSATION` first. For the replay, run `git -C <workdir>/examples pull` and run it again |
| A command prints that it was removed and names another command | The CLI is older than this flow, or the command belongs to the previous platform | Run `distil update`, then use the `distil dataset` command this file gives for the step |
| `whoami --output json` prints `{"error": ...}` | No session on this machine | Send the user to sign in first, as Step 1 says, and stop |
| A command reports a bad id, or an empty one | The id was not carried over from the create | Read ids from `<workdir>/ids.txt`. Pass them as literal arguments |
| A step reads the wrong dataset | An older id of the chain was passed | Each job writes a new dataset. Pass the id the previous job created, and check the chain with `distil dataset show <id>` |
| A command cannot find a local file | The working directory is not what you assumed | Write the absolute path that Step 1 read, in place of `<workdir>` |
| `api-keys create` fails because the name exists | An earlier run created a key with that name | Add `-2` to the name, and use the new name in every later command |
| `api-keys create` is refused because the account holds 5 keys | An account holds at most 5 API keys | Follow "The API key" in the last step: list the keys, ask which to remove, delete it with `--force`, create the key again |
| `traces upload` prints nothing for minutes, or ends with `Failed to upload file: Bad Request` | The upload to storage is too slow on this network, and storage closed the connection | Nothing was created. Run the same command again. If it fails again, tell the user that the network is too slow for a 3 MB upload right now, and try again later |
| The first calls through a new key are refused | A newly linked key takes up to a minute to start working | Wait a minute, then run the same command again |
| The replay summary shows failed requests | Some calls timed out or were refused | A few failures do not matter. If most failed, check the key and the endpoint name, then run the command again |
| `download-traces` writes no file, or prints `No traces found` | The endpoint has no records in the time window yet | Check that `<today>` is today's date and that the replay finished. Run the command again |
| `relabel-traces-test` or `relabel-traces-train` fails at once and says the dataset has fewer traces than it needs | The replay sent too few requests, the download missed a few of the newest records, or the conversion dropped many | Read the count in the error. Run the replay again, or lower `trace_processing.num_test_relabelled` or `num_train_relabelled` in a copy of the config and pass it with `--config` |
| A `grep` line after `download-metadata` does not show `openai.gpt-oss-120b` | The teacher line in the config differs from the one the `sed` expects | Open `<workdir>/baseline/config.yaml`, set every `teacher_model_name` to `openai.gpt-oss-120b` by hand, and continue |
| `teacher-evaluation metrics` says the job is still running | The evaluation started in the train set step did not finish before training did | Start a monitor for it, with `teacher-evaluation` as the group, and show the table when it ends |
| `Deployment is not ready` | The deployment has not reported `JOB_SUCCESS` yet | Let the deployment loop finish, then create the endpoint |
| A create exits 1 with a validation error | The data or the config is wrong | Read the error. It names the file and the row |
| A create is refused | The credit balance for that route is 0 | Read `distil credits-balance` and ask distil labs for a grant |
| A wait loop returns a tool timeout | The loop ran in the foreground | Run every status loop as a background command. Read "How to watch a job" |
| A background monitor exits and the last status is `JOB_RUNNING` | The loop ran out of rounds. The job did not stop | Start the same loop again with the same id. Tell the user the elapsed time and the range the step gave |
| The monitor prints `NO_ANSWER` | The CLI could not reach the platform. The machine is offline, or asleep | The job continues on the platform. Start the loop again when the network is back |
| `slm metrics` or `teacher-evaluation metrics` says the job has not finished | The job still runs | Wait for the monitor to report `JOB_SUCCESS`, then read the metrics again |
| The session sits idle and the user asks what is happening | A background command is running and nothing was said | Read it once, give the latest line in words, then offer the questions this step lists |
| A job reaches `JOB_FAILURE` | The log holds the cause | Read the log. Search for the **first** error, not the last |
| The log command prints `Server error: Internal Server Error`, or `read.py` stops with `KeyError: 'logs'` | The platform could not return the log of the failed job | Run the command that started the job once more. It creates a new job with a new id: use that id from then on. If the new job fails too, tell the user and stop |
| `download-metadata` writes fewer than three files | The download did not finish | Run the command again |
| A download exits 1 and says the job is still running | The job is not finished | Read the status first |

A failed job spends its credit.

---

## Appendix A: language rules

**Agent: write every message to the user in this style.** The rules come from ASD-STE100
Simplified Technical English. They make technical text short and clear for readers who are new
to a subject, and for readers whose first language is not English.

Apply the rules to your own messages. Do not apply them to command output, and do not rewrite
the user's words.

---

When you write technical text (documentation, READMEs, runbooks, procedures, error messages,
release notes, reports), obey these rules from ASD-STE100 Simplified Technical English:

CLASSIFY FIRST. Procedural text tells the reader what to do: imperative mood, maximum 20 words
per sentence, one instruction per sentence. Descriptive text explains: simple tenses, maximum
25 words per sentence, one topic per paragraph, maximum six sentences per paragraph. Never mix
the two in one passage.

VERBS. Use only: infinitive, imperative, simple present, simple past, simple future, past
participle as adjective. No present perfect ("has completed" → "completed"). No "-ing" verb
forms ("making it easy" → new sentence). Active voice; passive only in descriptions when the
agent is unknown. Approved modals: can, will, must. Banned: should, would, may, might, could.
For "should": write "must" if required, delete if optional.

SENTENCES. Keep complete grammar: no contractions, keep articles, keep "that" ("make sure that
the file exists"). Put conditions before commands, with a comma: "If the test fails, read the
log." No semicolons — write two sentences. Use a vertical list for more than two items or
steps.

WORDS. One word, one meaning, for the whole document: pick one of check/verify/confirm and keep
it. Noun chains of maximum three words; break longer ones with prepositions ("the timeout value
for the connection pool"). Delete words that carry no fact: simply, seamlessly, robust,
powerful, comprehensive, leverage, "in order to", "it is worth noting". Replace: utilize → use,
prior to → before, in the event that → if, e.g. → for example. American spelling.

WARNINGS. Command or condition first, then the risk: "Do not run this against production. The
command deletes rows."

NEVER TOUCH. Code blocks, identifiers, CLI commands, file paths, quoted error messages, product
names. Each counts as one word toward sentence limits.

SELF-CHECK before returning: scan for contractions, "has been", "should", ", making",
semicolons. Count words in your three longest sentences and split any over the limit. Collapse
synonym rotation.

Do not apply these rules to marketing copy or brand writing.

---

Source: https://github.com/AminBlg/SimpleEnglish