Lab authoring
Labs are part of your lesson markdown: a fenced ```lab block
containing YAML. Because a lab is lesson content, it inherits everything
lessons already have — draft vs. published, version history, repo sync.
There is no separate lab tool to keep in step.
Learner code runs in the browser — SQL on a real DuckDB engine, Python on a real CPython runtime. Run executes and shows output; Check my work re-runs the code and evaluates your checks against the actual result of the run.
A complete lab
```lab
id: filtering-orders # stable id — progress references it forever
title: 'Lab: Filter orders with WHERE'
estimatedMinutes: 10 # optional, shown in the lab header
language: sql # sql (DuckDB) or python (Pyodide);
# anything else = code-only checks
setup: | # environment script — re-runs before EVERY
CREATE OR REPLACE TABLE orders ( # learner run, so keep it idempotent
order_id INTEGER, amount DOUBLE
);
INSERT INTO orders VALUES (1, 120.0), (2, 250.0);
starterCode: | # optional, seeds the editor
SELECT *
FROM orders;
exercises:
- id: add-where # stable id, unique within the lab
title: Filter rows with WHERE
brief: > # optional scene-setting, markdown
Narrow the orders table down to the orders over €100.
steps: # optional numbered instructions, markdown
- md: Add a `WHERE` clause to the starter query.
note: WHERE filters rows before SELECT picks columns.
checks: # at least one; ALL must pass
- type: code_matches
pattern: '\bWHERE\b'
flags: i
fail: "There's no WHERE clause yet — filtering always starts there."
- type: sql_result_equals
rows: [[2, 250.0], [1, 120.0]]
fail: "The query runs, but it's not returning only the orders over 100."
hints: # first hint is free; each failed attempt unlocks one more
- 'The clause goes after FROM: `SELECT … FROM orders WHERE <condition>`.'
solution: | # powers "Apply the solution"; always write one
SELECT * FROM orders WHERE amount > 100;
```
Exercises unlock one after another; the lab is complete when every exercise has passed.
The six check types
Checks run when the learner presses Check my work. All checks of the
exercise must pass. The learner sees them as a checklist, one row per
check, passing or failing. A failing check shows exactly one thing
beside its row: your authored fail message, never a raw error.
Every check can carry an optional label, the short line learners read
in that checklist ("Keeps only orders over €200"). Without one, the row
says what the check looks at: "Your code", "Printed output", "Query
result", or "Value of total". Labels are display copy only: adding or
changing one never resets passes learners already earned.
checks:
- type: sql_result_equals
label: Keeps only shipped orders over €200
rows: [[1041, 245.0], [1057, 612.5]]
fail: Close. Keep the amount filter and add status = 'shipped'.
| Type | Looks at | Passes when |
|---|---|---|
code_matches | the editor code | pattern (a regex) matches the code |
stdout_contains | printed output | output contains text |
stdout_matches | printed output | pattern (a regex) matches the output |
stdout_equals | printed output | output equals text exactly (trailing whitespace ignored; set trim: false for byte-exact) |
sql_result_equals | the query result | result rows equal rows — optional columns: (names, case-insensitive) and orderMatters: true (default compares as sets) |
global_equals | a Python variable | the global named name equals value (any JSON shape) |
Which to use: SQL labs → sql_result_equals is the strongest check —
it verifies what the query actually returned. Python labs →
stdout_* for printed output, global_equals for computed variables.
code_matches works in any language and is best as a shape hint
alongside an outcome check.
Tip
The robust pattern is one code_matches for the concept ("uses a
WHERE clause") plus one result/stdout check for the outcome. The shape
check catches the right idea with a wrong result; the outcome check
catches a right result reached the wrong way.
pattern and flags
pattern is a regular expression (JavaScript syntax, written without
/…/ delimiters). flags is an optional string of regex flags:
i— ignore case (wherematchesWHERE)m—^and$match per line, not per whole texts—.also matches newlinesu— full unicode matching
flags: i is the one you'll use most: learners shouldn't fail a check
over capitalization. A useful token in patterns is \b (word boundary):
'\bWHERE\b' matches the keyword WHERE but not everywhere.
Fail messages, hints, and the solution
failis required on every check — publishing without one is impossible. Write it as help, not as an error: say what to do next ("There's no WHERE clause yet…"), not what went wrong internally.hintsescalate: the first is free, each further failed attempt unlocks one more. Order them from nudge to near-answer.solutionpowers "Apply the solution": after a failed attempt the learner can put it in the editor and move on. It applies known-good code and records a checkpoint (analytics separates clean passes from skips). Always write one — it's what keeps an unattended lab from hard-sticking someone, and it's what Test lab verifies against. Checkpoints unlock the next exercise, but on a course with an assessed certificate they don't count toward it — the learner can always re-attempt for a clean pass, and the course page tells them how many exercises still need one.- Passing finishes the lesson: once every exercise of every lab in a lesson has a pass (checkpoints included), the lesson marks itself complete — learners never do the work and forget the button. Lessons without labs complete via Complete and continue.
Test your lab before publishing
Press Test lab on the lab's row under the editor (or ⌘K → "Test lab: …"). It walks the lab the way a learner would:
- the starting code must fail every check — a check that already passes before the learner does anything is vacuous (a bug in the check, not the learner);
- your
solutionmust pass every check — otherwise the check is broken and "Apply the solution" would strand people.
The results show every authored fail message exactly as a failing
learner sees it — proofread them there. A passing test marks the lab
verified; any edit flips it to "not verified since last edit" until
you test again. Typos and structural mistakes never get that far: the
editor lints the lab live as you type (unknown fields, unknown check
types, patterns that don't compile), and errors block publishing.
Good to know
- One editor buffer per lab.
starterCodeseeds it, every exercise checks it, "Apply the solution" replaces it. setupre-runs before every learner run, so your tables and variables always reset. Never write an exercise that depends on state a previous run created. Use idempotent DDL (CREATE OR REPLACE).- Python
packages(e.g.pandas) preload at startup; known packages a learner imports are also fetched automatically. - Keep
estimatedMinuteshonest — recalibrate from real completion times in analytics. - Numbers in
sql_result_equalscompare loosely (5=5.0="5"), so decimal formatting never fails an exact-value check.
During the design-partner phase, we author lab blocks with you — same-day, like all content.