MastriaDocs

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

Markdown
```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.

YAML
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'.
TypeLooks atPasses when
code_matchesthe editor codepattern (a regex) matches the code
stdout_containsprinted outputoutput contains text
stdout_matchesprinted outputpattern (a regex) matches the output
stdout_equalsprinted outputoutput equals text exactly (trailing whitespace ignored; set trim: false for byte-exact)
sql_result_equalsthe query resultresult rows equal rows — optional columns: (names, case-insensitive) and orderMatters: true (default compares as sets)
global_equalsa Python variablethe global named name equals value (any JSON shape)

Which to use: SQL labssql_result_equals is the strongest check — it verifies what the query actually returned. Python labsstdout_* 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 (where matches WHERE)
  • m^ and $ match per line, not per whole text
  • s. also matches newlines
  • u — 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

  • fail is 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.
  • hints escalate: the first is free, each further failed attempt unlocks one more. Order them from nudge to near-answer.
  • solution powers "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 solution must 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. starterCode seeds it, every exercise checks it, "Apply the solution" replaces it.
  • setup re-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 estimatedMinutes honest — recalibrate from real completion times in analytics.
  • Numbers in sql_result_equals compare 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.