distil labs onboarding
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.
At the end the user has an account, a trained small model, a measured score, and a model that answers their own input.
The flow has 9 steps and takes 30 to 45 minutes. Jobs run for 20 to 35 of those minutes.
Rules for the agent
Section titled “Rules for the agent”- Write every message to the user in the language of Appendix A. Read Appendix A first.
- Do the steps in order.
- 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 stage.
- Present each stage as discussed in The stage pattern below
- Stop at each CHECKPOINT. There are four, and each one waits on something only the user can do. Sometimes that is an answer, and sometimes it is an action whose result you can see for yourself. 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.
- Do not invent credentials. The user types their own email and password.
- 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 9is fine. - Give the total time once, in Step 1, in the words Step 1 gives you. Then give each stage its own range as that stage starts, in the words that stage gives you. The stage ranges add up to the job time inside the total, so the two agree. Do not read the sum out.
Do not run the smoke runs that the skill prescribes. A new account holds 2 model-training credits, and a smoke run spends one. This flow trains once. A new account has enough credits for every step here.
How to run the commands
Section titled “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 2 makes. Step 2 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 2 makes that name work, and it gives two fallbacks for the machine where it does not. Stay on the rung Step 2 leaves you on for the whole run. - Read JSON with
python3 <workdir>/read.py, which Step 2 writes. This flow does not usejq.
The stage pattern
Section titled “The stage pattern”Steps 5 to 9 are one stage each:
- Step 5, upload the seed data
- Step 6, teacher evaluation
- Step 7, synthetic data generation
- Step 8, model training
- Step 9, deployment and evaluation
Each of the five opens the same way. Do these parts, in this order:
- Print the banner for the stage. Copy the art exactly.
- Give one sentence: what this stage does. Add the time range the stage gives you.
- Run the command. For Steps 6 to 9, read the id and start the background monitor.
- Give one paragraph: what happens inside the stage, and why it matters.
- Offer the user the questions this stage lists.
- 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.
Part 4 has one shape, and every stage uses it. Write three to five sentences, in this order:
- What the platform runs in this stage.
- What it produces.
- Why that matters for the model the user gets at the end.
Keep every stage to that length. A stage that runs longer does not get a longer paragraph. The user compares the five, 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 stage 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 stage, or about the platform”. A new user has no way to answer that.
Step 5 is the one stage that runs no job. Its command returns in seconds, so it has no monitor, no wait, and no separate result. Step 5 also adds one part: it shows two rows of the data before the command. Steps 6 to 9 each start a job, and each one waits.
How to watch a job
Section titled “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. Steps 6, 7 and 8 use this loop:
for i in $(seq 24); do
s=$(distil <group> status --output json <id> | python3 <workdir>/read.py status)
echo "$(date +%H:%M:%S) $s"
case "$s" in JOB_SUCCESS|JOB_FAILURE|JOB_STOPPED) break ;; esac
sleep 20
done
The timestamps are wall clock, not elapsed time. A restarted loop cannot know when the stage started, so it prints the time of day instead. To give the user an elapsed time, subtract the first timestamp of the first monitor for this stage from the latest one. Keep that first timestamp for as long as the stage runs.
<group> is teacher-evaluation, training-dataset, or slm. A deployment reports
deployment_status and has no status field, so Step 9 gives a loop of its own.
One round is 20 seconds, and 24 rounds are 8 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 8 minutes needs more than one loop, so the loop is built to be restarted. Each stage 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: the job is still running, and here is the elapsed time.
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 stage lists.
- If the user asks where the job is, read the monitor once. Then give the latest status line.
- If the user has no question, end your turn and let the job run.
JOB_SUCCESS means the outputs are readable. JOB_FAILURE and JOB_STOPPED mean stop and
read the log:
distil <group> logs --output json <id> | python3 <workdir>/read.py logs
Any other value means the job still runs.
Step 1: Explain the plan
Section titled “Step 1: Explain the plan”Print Step 1 of 9 before anything else. This is the first line of the run, and it starts
the counter that every later step continues. A user who first sees Step 2 of 9 reads it as a
step that was skipped.
Show the user this diagram. Copy it exactly. It covers all nine steps, and it runs down the page rather than across, so no part of it falls off the right edge of a narrow window.
Step 1 the plan
| what happens, and in what order
|
Step 2 set up the machine
| the CLI, the examples, the skill
|
Step 3 create the account and sign in
| one command does both, in the browser
|
Step 4 choose an example
| one small dataset for one real task
v
.--------------------.
| PROBLEM SELECTED |
'---------+----------'
|
Step 5 | upload minimal data to the platform
v
.--------------------.
| SEED DATA |
'---------+----------'
|
Step 6 | the teacher LLM solves the problem
v
.--------------------.
| TEACHER EVAL |
'---------+----------'
|
Step 7 | the teacher writes new training rows
v
.--------------------.
| SYNTHETIC DATA |
'---------+----------'
|
Step 8 | a small student trains on those rows
v
.--------------------.
| TUNED STUDENT | ---> score: before training
'---------+----------' score: after training
|
Step 9 | deploy the model on distil inference
v
.--------------------.
| DEPLOYED MODEL |
'--------------------'
The diagram says what each step does, so do not read it back line by line. Add only the part it cannot show, in about three sentences:
- The teacher knows a little about everything. The student needs one task only.
- The training rows carry the teacher’s answers for that one task, so the student can learn it.
- A small model that matches the teacher on one task costs far less to run.
Do not read out the row counts. They change with the example, and the user has not chosen one yet. Give the total time once here: 30 to 45 minutes, and jobs run for 20 to 35 of those.
Say once that the user can leave. Four of the nine steps start a job on distil labs hardware, and those jobs 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.
Invite questions, then go straight to Step 2. Do not ask for permission to start. The user opened this flow themselves.
Step 2: Set up the machine
Section titled “Step 2: Set up the machine”This step gets the machine ready: a working directory, the CLI, the examples, and the skill.
The working directory
Section titled “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:
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.
The CLI
Section titled “The CLI”Do not run the installer yourself. Ask the user to type this line in the prompt. The ! and
the space at the front are part of it:
! curl -fsSL https://cli-assets.distillabs.ai/install.sh | sh
The ! runs the line in the user’s own shell. A piped installer that an agent runs is refused
by the permission rules of some clients, and that refusal reads as a broken install.
Do not ask the user about PATH, and do not write to a shell profile yourself. The
installer does both. When the install directory is missing from PATH, the installer adds it to
the profile of the current shell and says which file it wrote. When the directory is already
there it says nothing.
Read the install path out of the installer output. Its last line is
Installed distil to .... On most machines that path is ~/.local/bin/distil, and it is
somewhere else when XDG_BIN_HOME is set, so read it rather than assume it. Keep two values from
that line, because the ladder below uses both:
<install-path>— the whole path, ending in/distil.<install-directory>— the same path without the trailing/distil.
Make the distil name work
Section titled “Make the distil name work”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:
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 directory to PATH for this session:
export PATH="<install-directory>:$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 the path the installer printed, in every command in this file:
<install-path> --version
Write that path in full, exactly as the installer gave it. This rung needs no profile and no session state, so it works on every machine the installer supports.
A non-zero exit code from rung 1 or rung 2 is not a failed install. A bare distil goes to the
shell’s command-not-found handler, and a third-party handler can fail there with an exit code of
its own. Installed distil to ... in the installer output means the install worked.
The installer supports macOS on Apple Silicon, macOS on Intel, Linux x86_64, and Linux arm64. It rejects Windows. On Windows without WSL, use the REST API instead.
Check the tools
Section titled “Check the tools”Run this yourself. Do not report it to the user, and do not install anything.
command -v git; command -v python3; command -v uv
git and python3 must both exist. This flow cannot continue without them. uv is optional,
and Step 9 handles the machine that does not have it.
The JSON reader
Section titled “The JSON reader”Write this in silence. Never name it to the user. It is your own tool for reading command output, and it is not a part of the platform. A user who is told about a “JSON reader” one minute after the CLI arrives has one more unexplained thing to hold.
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 accuracy reads
teacher_performance.accuracy. A digit is a list index. A key with hyphens needs no special
form, so read.py base_model_performance llm-as-a-judge works as written.
The examples
Section titled “The examples”The examples are the datasets this flow trains on. Options 1 and 2 in Step 4 need this repository and nothing else. Options 3 and 4 also need the skill, and Step 4 clones it there.
CHECKPOINT. Name the repository, say that it holds the example datasets, and say where it goes. Ask the user to confirm. Then run this line:
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.
The skill
Section titled “The skill”The skill holds the full model-building procedure. This file holds the onboarding path alone, and it is enough for Options 1 and 2 in Step 4. Options 3 and 4 need the skill.
Look in this session first. If distil-cli is already available to you, you have it, and
nothing here is needed.
If it is absent, say the true state in one line: the skill is not installed, and this flow does not need it yet. Then give the user these two lines and say what they do — they install the skill for this session and for every session after it. Do not run them yourself, and do not wait for an answer:
/plugin marketplace add https://github.com/distil-labs/distil-cli-skill
/plugin install distil-cli@distil-cli-skill
Never leave the user guessing whether something was installed for them. Say who does what.
Clone nothing here. Step 4 clones the skill, and only for the two options that need it.
Step 3: Create the account and sign in
Section titled “Step 3: Create the account and sign in”One command does both jobs. distil signup opens the sign-up page and then waits for a sign-in
in the same browser, and that sign-in is what hands the session to the CLI. Tell the user the
shape before you start 3a: they create the account and sign in on the same page, and the terminal
is done when it prints Logged in as <email>.
Every line the user types in Step 3 starts with a !. Those lines need the same rung the
ladder in Step 2 left you on. On rung 3, give the user ! <install-path> signup rather than
! distil signup, and the same for the distil auth line in 3b. A user who meets
command not found on the first line they type themselves reads the whole install as broken.
Look for an existing session first. Run this yourself and do not announce it. On a machine that had no CLI a minute ago, saying that you are checking for a session reads as noise.
distil whoami --output json
On a machine with no session this prints {"error": "Not logged in ..."} and exits 1. That is
the expected answer here, not a fault. If it prints an email instead, ask the user whether to
keep that account. To create a new one, run distil logout first.
3a. Create the account and sign in
Section titled “3a. Create the account and sign in”Do not run this yourself. Ask the user to type this line in the prompt. The exclamation mark and the space at the front are part of it:
! distil signup
A browser opens at once. Give the user the command line and these four steps, and stop there. A user who reads a long block here opens a second window, runs the command, and never comes back to the rest of it:
- Fill the sign-up form. Submit it.
- If the page asks you to confirm your email address, open the confirmation email and select the link in it. The link opens a plain AWS page with no branding and no button. That page is expected. Then come back to the sign-up page.
- Sign in with the same email and password.
- If the browser asks for permission to reach your local network, accept it.
The next two paragraphs are for a user who asks. Do not volunteer them.
Point 3 is the one that surprises people. Submitting the form creates the account, but it does not sign anyone in. The sign-in is what returns the session to the terminal, and the command waits until it arrives.
The tab does not matter. The browser does. The hand-off is carried by the address this command opened, and whichever browser opens that address holds it. Any tab in that browser works. A browser that never opened the address has nothing to hand back, which is what can happen if a confirmation email opens in a different browser.
CHECKPOINT. Wait for Logged in as <email>, then go straight on to Step 4. That line is
enough on its own — when you see it, the session exists. Do not ask the user “what next”, and do
not wait for them to repeat a line you can already see. You can confirm it yourself at any time
with distil whoami.
If that line does not come. The command waits 20 minutes. After about 90 seconds it adds a line that says the sign-in went to a browser it did not open. That line is your signal to move to 3b. Do not sit through the remaining 18 minutes waiting for a timeout that tells you the same thing. Have the user press Ctrl+C before you go on, because a command left running prints its timeout in the middle of a later step, where it reads as a fresh failure.
3b. Sign in without a browser
Section titled “3b. Sign in without a browser”If 3a fails, ask the user for this line, with their own email and password:
! distil auth --email <email> --password <password>
This needs no browser at all. Never put their password in a command you run. The password does appear in the session, so offer this rung only after 3a has failed.
It needs the account to exist, which the sign-up form in 3a is enough for. Email confirmation required means this account asks for a confirmation the user has not done yet, so ask them to
open the confirmation email. Invalid email or password means either a typo or no account at
all — if the sign-up form was never submitted, go back to 3a.
Step 4: Choose an example
Section titled “Step 4: Choose an example”Say why the choice exists, before you ask for it. Give the user these three sentences in your own words:
- The next five steps train a real model, and a model is always a model of one task.
- The example supplies that task: a small dataset, and the answer the model has to learn.
- The steps are the same whichever one they pick, so the choice sets the subject and nothing else.
Without this, “choose an example” arrives with no object. The user has been installing a CLI and making an account, and nothing so far said that a task was needed.
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
Section titled “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
Section titled “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
Section titled “Option 3”Label: trail-report-tagging
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 starts from the logged traffic of a production model it replaces, not from labeled rows. 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 build from the logs of a model you already run. It is the longest run, and it ends in an honest partial success.
Preview:
**trail-report-tagging** — question answering, from traces
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. This one starts from the logged traffic of
a production model it replaces, not from labeled rows.
```
/\
/\ / \ /\
/\ / \/ \ /\ / \
/\ / \/ \___/ \/ \ /\
_____/ \/ \/ \_____
.---------------.
| TRAIL 7 |
| bridge X |
'-------+-------'
- - - - - - - - - - - - -+- - - - - - - - - - - - -
"The footbridge at the lower crossing is gone entirely."
|
v
{"tags": ["BRIDGE_OUT"]}
```
the longest run, and an honest partial success. Tuning beats the
production model it replaces, but stays well short of the teacher.
Option 4
Section titled “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
Section titled “After the choice”For bindery-defect-triage, continue with the rest of this file as written. For
incident-triage, change the directory name only. Neither one needs the skill.
Options 3 and 4 need the skill. If Step 2 found it in this session, use it. If it is absent, clone it now, and say in one line that this build needs the full procedure:
git -C <workdir>/skill pull 2>/dev/null || git clone https://github.com/distil-labs/distil-cli-skill.git <workdir>/skill
The lines below name skill files by their short path, for example
workflows/traces-to-model.md. On this clone, prefix the short path with
<workdir>/skill/skills/distil-cli/. A skill that this session already holds needs no prefix.
For trail-report-tagging, read workflows/traces-to-model.md in the skill, because a traces
build has extra stages.
For my own data, or for any answer the user typed themselves, stop following this file and
hand over to the skill:
- Ask what the user has: a set of labeled examples, or traces from a model already in production.
- For labeled examples, read
workflows/dataset-to-model.mdin the skill. - For traces, read
workflows/traces-to-model.mdin the skill. - Follow that workflow from its first stage. The account from Step 3 and the CLI from Step 2 both carry over, so nothing here is repeated.
Say plainly that you are leaving the worked examples and building their task instead. Do not run a worked example first as a warm-up, and do not treat the typed answer as an error.
Step 5: Upload the seed data
Section titled “Step 5: Upload the seed data”Banner. Print this first. Copy it exactly:
_ _ ___ _ ___ _ ___
| | | | _ \ | / _ \ /_\ | \
| |_| | _/ |_| (_) / _ \| |) |
\___/|_| |____\___/_/ \_\___/
upload the seed data
One sentence. The example data goes up to the platform, and every later step reads it from there. Time: this one returns in seconds. Say so, because it is the only step that does.
Show the data. Print two real rows, so the user sees what the platform is given:
head -2 <workdir>/examples/bindery-defect-triage/base-input/train.jsonl
Show both rows. Then name the two parts of a row, in one line each: the input that goes in, and the answer that comes out. Keep it to those two lines. Step 6 scores the answers, and the detail belongs there.
Command. Upload the directory:
distil seed-dataset create --data <workdir>/examples/bindery-defect-triage/base-input
This command runs no job, and it returns in seconds. There is no monitor and no wait here. It prints the seed dataset id in a sentence. Read that id, because Step 6 and Step 7 both need it.
One paragraph. This bundle has a name on the platform: a seed dataset. It holds four files: the labeled rows the teacher learns the task from, the test rows that every score is measured on, the task description, and the settings. The command uploads those 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 seed rows come from?
- Why are so few rows enough to train on?
- What happens if my own data looks nothing like this?
Step 6: Evaluate the teacher
Section titled “Step 6: Evaluate the teacher”Banner. Print this first. Copy it exactly:
_____ ___ _ ___ _ _ ___ ___ _____ ___ _
|_ _| __| /_\ / __| || | __| _ \ | __\ \ / /_\ | |
| | | _| / _ \ (__| __ | _|| / | _| \ V / _ \| |__
|_| |___/_/ \_\___|_||_|___|_|_\ |___| \_/_/ \_\____|
teacher evaluation
One sentence. A large teacher model answers the test set, so this run learns the highest score the task can reach. Time: 1 to 4 minutes. Give that range to the user.
Command and monitor. Start the job:
distil teacher-evaluation create-from-seed-dataset --output json <seed-dataset-id> | python3 <workdir>/read.py id
Read the id. Then start the monitor from “How to watch a job” as a background command, with
teacher-evaluation as the group. One loop of 24 rounds covers this job.
One paragraph. The teacher holds 120 billion parameters, and it answers every test row on its own. The platform compares each answer to the correct label and returns one score. That score is the ceiling for the whole run. The student learns from the teacher’s answers, so the student cannot beat the teacher it learns from. A low teacher score points at the task description or the labels, and not at the training.
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:
- What is the teacher model?
- Why measure the teacher before training anything?
- What happens if the teacher scores badly?
The result. When the monitor reports JOB_SUCCESS, read the score:
distil teacher-evaluation metrics --output json <teacher-evaluation-id> | python3 <workdir>/read.py teacher_performance accuracy
For classification the primary metric is accuracy. Give that one number 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,
here and in Step 8.
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 to Step 7. The student learns from the teacher’s answers, so the teacher’s score is the best the student can reach. Do not ask the user to approve it.
Say what the score was measured on, in the same message. A score near 1.00 with no denominator reads as a number that was chosen, not measured. Read the real counts and give them in one line, for example “measured on 50 test rows”:
grep -c '' <workdir>/examples/bindery-defect-triage/base-input/test.jsonl
This prints one number: the rows the score was measured on. grep -c '' is used rather than
wc -l, because wc -l counts one row short when the file has no final newline. Do not state
this number from memory, because it differs between examples.
Step 7: Generate the training data
Section titled “Step 7: Generate the training data”Banner. Print this first. Copy it exactly:
_____ ___ _ _____ _ _ ___ ___ _ _
/ __\ \ / / \| |_ _| || |/ __| __| \| |
\__ \\ V /| .` | | | | __ | (_ | _|| .` |
|___/ |_| |_|\_| |_| |_||_|\___|___|_|\_|
synthetic data generation
One sentence. The teacher writes the training data that the student learns from. Time: 2 to 5 minutes. Give that range to the user.
Command and monitor. Start the job:
distil training-dataset create-from-seed-dataset --output json <seed-dataset-id> | python3 <workdir>/read.py id
Read the id. Then start the monitor as a background command, with training-dataset as the
group. One loop of 24 rounds covers this job.
One paragraph. The teacher writes about 550 new training examples. The seed rows fix the
format and the style, and the teacher varies everything else. config.yaml is the settings file
inside the example directory, and it is what steers the mix. It asks for 512 rows, and the job
returns a few more. This example forces every station at every severity, so the rare
combinations appear often enough for the student to learn them.
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:
- How can data the teacher invented teach anything real?
- What stops the teacher from writing 512 rows that all look the same?
- Can I see the rows it wrote?
The result. When the monitor reports JOB_SUCCESS, read a free sample:
distil training-dataset sample --output json <training-dataset-id> | python3 <workdir>/read.py rows 0
Show the input and the label. Drop every field that is empty or null. A text task returns
"images": null on every message, and an empty field reads to a new user as something that
failed. Read the row yourself, then write out the parts that carry data.
Show one generated row, and pick one whose shape differs from the two seed rows you showed in
Step 5. That contrast is the lesson: the teacher wrote both the input line and the label, and it
varied the phrasing on its own. The sample command returns at most 128 rows and costs nothing.
A new account cannot download the full set, so sample is how you look at this data.
Step 8: Train the student and read the result
Section titled “Step 8: Train the student and read the result”Banner. Print this first. Copy it exactly:
_____ ___ _ ___ _ _ ___ _ _ ___
|_ _| _ \ /_\ |_ _| \| |_ _| \| |/ __|
| | | / / _ \ | || .` || || .` | (_ |
|_| |_|_\/_/ \_\___|_|\_|___|_|\_|\___|
model training
One sentence. A small student model trains on the data the teacher wrote, and the platform scores it before and after. Time: 11 to 20 minutes, the long wait. Give that range to the user.
Command and monitor. Start the job:
distil slm create-from-training-dataset --output json <training-dataset-id> | python3 <workdir>/read.py id
Read the id. Then start the monitor as a background command, with slm as the group. This job
needs two or three loops.
One paragraph. The platform scores the untrained student first, and that score is the baseline. The student then trains for 4 epochs on the synthetic rows. The platform scores it again at the end. The two scores show what the training did, on the same test rows the teacher answered in Step 6.
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 0.6B model match a 120B 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:
- 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.
- The size difference. The student
Qwen3-0.6Bholds 0.6 billion parameters. The teacher holds 120 billion. The student is about 200 times smaller. - Why this works. The teacher knows a little about everything. The student needs one task. The synthetic data carries the teacher’s answers for that one task.
- What the user gets. A small model answers faster and costs less to serve. It also runs on hardware that cannot hold the teacher.
- What comes after. Step 9 serves the trained model, and the user sends it their own input.
Read the result
Section titled “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.
Both numbers come out of one response, so fetch it once:
distil slm metrics --output json <slm-id> > <workdir>/metrics.json
python3 <workdir>/read.py base_model_performance accuracy < <workdir>/metrics.json
python3 <workdir>/read.py tuned_model_performance accuracy < <workdir>/metrics.json
For a question-answering task the primary metric is llm-as-a-judge, not accuracy. Change the
last argument of the two read lines, and reuse the same file.
Put the three numbers side by side, in this shape. Fill every cell from a number you read, and from nothing else. The teacher score is the one you read in Step 6:
| Score | |
|---|---|
| Student, before training | read it here |
| Student, after training | read it here |
| Teacher, from Step 6 | read it here |
Put the count you read in Step 6 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. The per-class rule from Step 6 holds here too.
Then make the argument in two sentences, from the numbers in front of you. On this example the untrained student sits near chance and training closes most of the gap to the teacher. Say what the run did, not what a run once did: a 0.6B model reaching the teacher on one narrow task is the point, and it costs far less to run.
Step 9: Use the model
Section titled “Step 9: Use the model”Banner. Print this first. Copy it exactly:
___ ___ ___ _ _____ ____ __ ___ _ _ _____
| \| __| _ \ | / _ \ \ / / \/ | __| \| |_ _|
| |) | _|| _/ |_| (_) \ V /| |\/| | _|| .` | | |
|___/|___|_| |____\___/ |_| |_| |_|___|_|\_| |_|
deployment and evaluation
One sentence. The platform serves the trained model on its own hardware, and the user sends it their own input. Time: about 6 minutes to start. Give that to the user.
Recap the task first, in two sentences. Fifteen to thirty minutes of jobs sit between Step 4
and this checkpoint, 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 stage follows the same pattern as the other four, with one difference: the checkpoint and 9a come 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 data the model was trained on, rather than describing it from memory:
head -1 <workdir>/examples/bindery-defect-triage/base-input/train.jsonl
For trail-report-tagging, read <workdir>/examples/trail-report-tagging/traces-input/traces.jsonl instead. The content of the
user message is one real input, in the one shape the model accepts.
CHECKPOINT. Show the user that line 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. Hold their input. You send it in 9c.
9a. Get the inference client
Section titled “9a. Get the inference client”This costs no credits:
Look first, as a command of its own:
ls <workdir>/model/ 2>/dev/null
If that lists files, an earlier run left them there, and the download replaces them. Say so before you download, not after. A directory that changes under a user in silence is the kind of thing they find later and cannot explain. An empty result is the normal case on a first run.
Then download:
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 system prompt the model was trained with and the decoding settings it expects, so
it is the correct way to call the model. It needs distil 0.25.2 or later. If any of the three
files is missing, run the command again.
9b. Start the deployment
Section titled “9b. Start the deployment”Command and monitor. Start the job:
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:
for i in $(seq 24); do
s=$(distil deployment status --output json <deployment-id> | python3 <workdir>/read.py deployment_status)
echo "$(date +%H:%M:%S) $s"
case "$s" in JOB_SUCCESS|JOB_FAILURE|JOB_STOPPED) break ;; esac
sleep 20
done
One loop covers this job in the normal case.
One paragraph. A deployment serves the model with vLLM on distil labs hardware, and it takes
about 6 minutes to start. JOB_SUCCESS means that the model accepts requests. Before that,
distil deployment endpoint returns null for both fields and still exits 0, so read the
status and do not probe the endpoint. This is the last job of the run, and it is the one that
turns the trained weights into something the user can send a question to.
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?
- How do I put this behind my own application?
9c. Ask the model two questions
Section titled “9c. Ask the model two questions”Wait for the monitor to report JOB_SUCCESS. Then run the canned input and the user’s input
back to back. The client takes the endpoint URL with /v1 added:
distil deployment endpoint --output json <deployment-id> > <workdir>/endpoint.json &&
url=$(python3 <workdir>/read.py url < <workdir>/endpoint.json) &&
key=$(python3 <workdir>/read.py api_key < <workdir>/endpoint.json) &&
uv run <workdir>/model/model_client.py --base-url "${url%/}/v1" --api-key "$key" \
--conversation '[{"role": "user", "content": "stn_qa defect=die-crush sev=critical lot=L-4471 op=RB shift=night press=3 units=274"}]'
Both fields come out of one response, so the command fetches it once. url and key are set and
used inside one command, which is why they work here.
That line carries all seven fields, in the order every trained row uses. die-crush is a
stamping defect at critical severity. The severity alone says rebind_unit. Rule 1 outranks
it, because stamping is never escalated. The correct answer is mark_and_pass. The other five
fields are decoration, and none of them changes the answer.
Now run the same command with the input the user gave you at the checkpoint. This is the moment the flow exists for.
If uv is absent, run the client with python3 after pip install openai pydantic.
9d. Delete the deployment
Section titled “9d. Delete the deployment”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.
A deployment is a session, not a permanent endpoint. It stops after six hours, or after one hour with no traffic. It cannot restart. A new deployment spends another credit and carries a new URL and a new key.
What comes next
Section titled “What comes next”The model weights are theirs. They are a separate download of about 1.2 GB:
distil slm download --destination <workdir>/model <slm-id>
Ask the user before you run this. It writes about 1.2 GB to their disk. The local route is at https://distillabs.ai/docs/deployment/local-deployment. Options 1 and 2 clone no skill, so name the page rather than a skill file.
Keep the skill. If the skill was never installed in this session, tell them that these two lines install it, so every future session has it:
/plugin marketplace add https://github.com/distil-labs/distil-cli-skill
/plugin install distil-cli@distil-cli-skill
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 skill asks whether they start from a labeled dataset or from production traces, and then drives the same pipeline.
Troubleshooting
Section titled “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 2 and move down one rung. Do not write to a shell profile |
| A clean install reports a non-zero exit code | A bare distil went to the shell’s command-not-found handler, and that handler failed |
Read the line above it. Installed distil to ... means the install worked. Move down one rung of the Step 2 ladder |
| The installer is refused before it runs | An agent ran a piped installer, and the client blocks that | Ask the user to run it themselves with ! curl -fsSL https://cli-assets.distillabs.ai/install.sh | sh |
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 |
| A command reports a bad id, or an empty one | The id was not carried over from the create | Read ids from the create output. Pass them as literal arguments |
| A command cannot find a local file | The working directory is not what you assumed | Write the absolute path that Step 2 read, in place of <workdir> |
distil signup or distil auth prints You are already logged in |
A session exists | Keep that account, or run distil logout and sign in again |
distil signup waits with a spinner and does not return |
Normal while the user is still on the sign-up page. It returns on the sign-in | Let it wait. If the 90-second hint appears, the sign-in went to another browser, so go to 3b |
distil signup timed out in the middle of a later step |
The command was left running after 3a | Ignore the line. It is 20 minutes old. Stop the command and read the session with distil whoami |
Invalid email or password in 3b |
A typo, or no account | Go back to 3a if the sign-up form was never submitted |
distil whoami prints no user |
The session expired | Sign in again, the same way as 3a |
whoami --output json prints {"error": ...} |
No session on this machine | Expected before Step 3. Continue with 3a |
| 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 |
| The session sits idle and the user asks what is happening | A background monitor is running and nothing was said | Read the monitor once, give the latest status line, then offer the questions this stage lists |
A job reaches JOB_FAILURE |
The log holds the cause | Read the log. Search for the first error, not the last |
download-metadata writes fewer than three files |
The download did not finish | Run the command again |
| A download exits 0 but writes nothing | The job is not finished | Read the status first |
A failed job spends its credit. Invalid input costs nothing, because the platform checks the bundle before it records the call.
Appendix A: language rules
Section titled “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.