This is the complete Novedu teacher guide, every chapter concatenated in reading order.
# What is Novedu
> Novedu lets teachers give students AI activities they control by writing instructions, not training models.
Novedu helps you create AI-supported learning experiences for your students. You decide the subject, the task, the teaching approach, and the boundaries. Students then use the experience you designed.
In Novedu, one experience you create for students is called an activity. An activity can be a tutor, a quiz, a writing task, or a coding activity. You don't need to be a programmer to create one.
## You shape the AI with instructions
You shape an activity by writing a prompt, which is a set of instructions that tells the AI how to behave. Think of the prompt as clear written guidance for a capable assistant: explain the role, the goal, the teaching style, and any rules it should follow.
You don't train or fine-tune an AI model. Writing instructions makes an activity quick to try and adjust. You can read exactly what you asked the AI to do, revise the instructions, and stay in control of the learning design. No data science is required.
## Four kinds of activity
Novedu offers exactly four kinds of activity. Each one gives students a different learning experience.
### Tutor
A tutor is a conversation with an AI that follows your teaching instructions. You can define its subject, tone, approach, and boundaries. A tutor can explain ideas, ask guiding questions, and help students practise without simply taking over the work.
### Quiz
A quiz asks open-ended questions that students answer in their own words. If you allow it, students can also attach a photo of handwritten work. The AI assesses each answer against guidance you provide and gives the student feedback. Students can also discuss a question after receiving the feedback.
### Writing
A writing activity gives students an editor alongside an AI writing coach. The coach can read the student's draft and offer feedback, but it can't edit the draft. The student remains the author.
### Coding
A coding activity gives an external coding assistant the instructions you choose. Students work in their coding environment while the assistant answers according to your rules, such as the programming concepts the class may use and the teaching style it should follow.
## From an idea to your class
You first author the activity by describing what students should do and how the AI should support them. When the activity is ready, you create a code, the short link you give to your class. Students open the code and see the right experience for that activity.
The same basic path applies to all four kinds: write the activity, create a code, and share it with students. You can start with a simple set of instructions, try the activity, and improve it as your teaching needs become clearer.
# What is a shareable code
> The short link that opens one activity for your class, how you get one, what students do with it, and which settings it carries.
When an activity is ready for your class, you hand it out as a code. A code is a short link that opens exactly one activity. You share the link (or just the code itself), students open it, and they see the experience you built.
## One code, one activity
A code is a short, random combination of letters and digits. You don't choose the text yourself; Novedu generates it when you create the code, and it is long enough that nobody can guess it. The full link looks like `https:///`, and the code alone works too.
Each code belongs to one activity of one kind (tutor, quiz, writing, or coding). The kind is fixed when the code is created and never changes. You can create several codes for the same activity, for example one per class, each with its own settings.
## How you get a code
You create a code from an activity you have already written. In the Codes area you pick the kind, point the code at your activity, and add any settings you want. Novedu then shows you the short link with a copy button, ready to paste into your class chat, learning platform, or projector slide. Creating and editing codes has its own chapter with the full steps.
## What students do with a code
Students open the link in a browser and sign in with their school account. They then land directly in the activity: a tutor chat, a quiz, or a writing editor, depending on the kind.
If a student only has the code text, they can open the Novedu start page and type or paste it there. The start page also remembers codes a student used recently, so returning to yesterday's activity is one click.
While they're working, students can flag a notable AI answer to you with a small report button. Those reports land on your Reports page, and the student-reports chapter covers what you see and how to handle them.
### Coding activities work differently after that
A coding code opens the same way, and students sign in the same way, but there is no chat waiting for them. Instead, the page hands them a personal key and the settings for connecting their own coding tool to the instructions and AI model you chose. The chapter on connecting a coding activity explains what students do with it.
## What a code carries
A code brings a few settings with it, decided when you create it:
- **A note.** A short label only teachers see, so you can find the code in your list later ("4AHIF, chapter 3 revision").
- **An availability window.** An optional start and end time. Outside the window the code doesn't open, and students see a clear message instead. Leave either side open for a code without a start or without an end.
- **Whether it records who did what.** Some activities are anonymous and store no names; others record which student did what, so you can review individual work. The default depends on the kind of activity.
A code keeps working until you delete it. An expired code no longer opens for students, but it stays in your list, so its statistics remain available to you. The sharing chapters cover each of these settings in detail.
# Tutors: a chat that teaches your way
> What a tutor is, what students see in the chat, and how your written instructions shape the help it gives.
A tutor is the simplest kind of activity in Novedu: a chat with an AI that follows your instructions, on a topic you choose. You decide what it teaches, how it talks to students, and where it draws the line. A well-written tutor explains ideas, asks guiding questions, and nudges students forward instead of doing the work for them.
You don't train an AI model to get this behaviour. You write a prompt, a set of plain-language instructions, and the tutor follows it. If the tutor answers in a way you don't like, you change the instructions and try again.
## What students experience
Students open a tutor and see a chat. Before the first message, the empty screen greets them: you can replace the default greeting with your own, and add a short description that tells students what this tutor helps with.
You can also offer clickable starter questions on the empty screen. A student picks one, the question lands in the chat input ready to edit, and the conversation begins. Starter questions are a gentle way to show students what the tutor is good at, especially for a class that has never talked to an AI tutor before.
From there it's a normal back-and-forth conversation. Students ask in their own words, the tutor answers within your rules, and they can dig deeper for as long as they need. Nobody is graded, and there is no fixed path through the material.
Students can also start a tutor conversation over. A button above the chat clears what they've discussed and begins a fresh conversation, and it asks them to confirm first. It helps when a conversation has drifted off topic, or when a student wants to approach a question again from the beginning. The tutor won't remember anything from the earlier conversation, but you still see that conversation under the code, and a student who chats both before and after starting over counts as two conversations in your statistics.
## How you shape a tutor
Everything the tutor does comes from instructions you write. Typical instructions cover:
- **Who it is and what it teaches.** For example, a friendly tutor for sorting algorithms, aimed at 16-year-olds who know basic TypeScript.
- **How it teaches.** For example, ask guiding questions, give hints before solutions, and have students predict the next step before revealing it.
- **What it must not do.** For example, never hand over the finished solution, stay on the allowed topics, and avoid concepts the class hasn't learned yet.
You write these instructions in your own words. For rules you want to apply across many tutors, such as a teaching style or a safety policy, you can also pull in a fragment, a named piece of prompt written once and reused. That way a whole team of teachers shares one carefully worded safety policy instead of each rewriting it.
By default, tutor chats are anonymous: the app doesn't record which student had which conversation.
## When a tutor is the right choice
Pick a tutor when students need open-ended help: preparing for a test, practising a skill, getting unstuck on homework, or exploring a topic at their own pace. It shines where a conversation beats a worksheet.
A tutor doesn't assess anything. When you want students to answer set questions and receive graded feedback, build a quiz instead. When students should produce a text of their own with an AI coach beside them, build a writing activity.
# Quizzes: open questions, graded by the AI
> What a quiz is, how students answer and get feedback, and how your private grading guidance shapes the verdicts.
A quiz is an activity made of open-ended questions. There are deliberately no multiple-choice options: students answer in their own words, and the AI grades each answer against guidance you write. Every answer gets a verdict (correct, partially correct, or incorrect) plus written feedback the student sees straight away.
Because the grading is a prompt you write in plain language, a question can ask for anything the AI can judge from your guidance: a fact, an explanation, a short calculation, or a piece of reasoning. You don't need to be a programmer to build one.
## What students experience
Students open a quiz and see a welcome screen with a greeting and a short description you write. Then the questions come one at a time; by default their order is shuffled for each attempt, though you can keep your authored order when later questions build on earlier ones.
For each question, a student reads it, types an answer in their own words, and submits. Where your school's server offers it, a small label beside the answer box gives a live hint while they type, showing whether the answer looks right so far; it is only a hint, and you can switch it off for a quiz. The AI grades the answer and immediately shows a verdict, correct, partial, or incorrect, together with feedback that confirms or gently corrects them. After seeing the feedback, the student can open a short discussion chat about that question to ask why an answer was wrong or to dig into the idea behind it.
A student who is stuck on a question can skip it and come back to it later. The skipped question moves to the end of the quiz and returns after all the questions the student hasn't seen yet, with any half-written answer still in place. A skipped question doesn't count as answered.
If you allow it, students can also attach a photo of their work, for example a handwritten calculation, and the AI grades the photo together with (or instead of) the typed text. Photo answers are off by default.
## How you shape a quiz
For each question you write two separate things:
- **The question** students see on screen, worded however you like.
- **A grading guide** for the AI, which students never see. Here you can state the expected answer openly and describe what counts as correct, partially correct, and incorrect.
Because the grading guide stays private, you can be completely explicit in it: name the right answer, list acceptable variations, and say what the feedback should sound like. The AI maps each student answer onto your criteria and writes the feedback from them. If the grading feels too strict or too lenient, you reword the guide and the next answers are graded your way.
You can also add guidance for the follow-up discussion chat, for example a rule that it should hint rather than repeat the full solution.
By default, quizzes are anonymous: answers feed the aggregate statistics, but the app doesn't record which student gave which answer. You can change that when you want attributed, graded work; the sharing chapters cover the details.
## When a quiz is the right choice
Pick a quiz when you want to check understanding and give every student individual feedback: after introducing a topic, as a self-check before a test, or as a warm-up that shows you where the class stands. Each student answers set questions and learns immediately what was right, what was missing, and why.
A quiz follows a fixed set of questions rather than an open conversation. When students need free-form help on a topic, build a tutor instead. When they should produce a longer text of their own with AI feedback beside them, build a writing activity.
# Writing: students draft, a coach advises
> What a writing activity is, how the AI coach reads but never edits the draft, and how students save their finished text.
A writing activity puts a student's own text at the centre. The student writes in an editor on one side of the screen, and an AI writing coach sits on the other side, reads the draft, and gives feedback. The student does all the writing; the coach helps them make it better.
Think of an essay, a formal letter, a report, or any text students should produce themselves. Instead of handing in a first draft and waiting days for comments, they get feedback the moment they ask for it, while they can still act on it.
## The coach reads, it never writes
The coach can look at the student's current draft whenever it gives feedback, but it has no way to change the text. That is built into the activity, not just a polite instruction: even if a student begs the coach to write the letter for them, it cannot type a single word into the editor. The student stays the author of every sentence.
Good coaching instructions lean into that boundary. Tell the coach to point at what works and what doesn't, explain why, and end with a concrete next step or a guiding question, rather than offering finished sentences to copy.
## What students experience
Students open the activity and see a split screen:
- **On one side, their draft.** A plain text editor where they write, with simple formatting (Markdown). It can start blank, or with a scaffold you provide.
- **On the other side, the coach.** A chat where they ask for feedback whenever they like: on the whole draft, on one paragraph, or on a specific worry ("is my tone polite enough?").
Students move back and forth: write a bit, ask for feedback, revise, ask again. When they're happy with the text, they press **Save** and their finished version is stored so you can review it later. Each student saves one text per activity, and saving again replaces their earlier version.
## How you shape a writing activity
You define three things in your own words:
- **The task.** What the students should write, for whom, and how long. This is the assignment text students read before they start, so write it for them.
- **How the coach helps.** Instructions that shape the coach's behaviour: what to prioritise in feedback, how to talk to the student, and the rule that matters most, advise rather than rewrite. For example, a coach for a formal feedback letter can be told to weigh tone and register most heavily, lead with what works, and raise only one or two improvements at a time.
- **An optional starter scaffold.** Text that is already in the editor when the student arrives. A formal-letter skeleton with a hint in each paragraph gives structure to students who freeze in front of a blank page; leave it empty for a free start.
Because the coach follows a written prompt, you adjust it the same way you adjust a tutor: if the feedback is too generous or too detailed, you reword the instructions and try again. No programming, no model training.
One difference from tutors and quizzes is worth knowing early: a writing activity records who wrote which text by default, because saving a text needs an author you can review. So writing is per-user unless you deliberately switch it off, and switching it off also disables saving. The details live in the sharing chapters.
## When writing is the right choice
Pick a writing activity when the goal is a text of the student's own, with guidance available beside it: essays, letters, summaries, reports, reflections. It works well whenever you'd normally collect drafts and comment on them by hand.
A writing activity doesn't grade anything; when you want set questions with graded feedback, build a quiz. And it isn't open conversation; when students just need to talk a topic through, build a tutor.
# Coding: an AI assistant inside the student's editor
> What a coding activity is, how students connect their own coding tool to it, and how your rules and model choice shape the help they get.
A coding activity gives every student an AI coding assistant that follows your rules while they program in a real editor. The student works in their own coding environment with an external coding assistant, for example [little-coder](https://github.com/itayinbarr/little-coder), and that assistant gets its answers through Novedu. You decide how it behaves: which language it uses, which concepts it may touch, and how it teaches.
As with every other kind of activity, you don't train an AI model. You write a prompt, plain-language instructions for the assistant, and pick the model that answers.
## How coding differs from the other kinds
Tutors, quizzes, and writing tasks all run as a page inside Novedu. A coding activity doesn't: there is no chat page in the app. Instead, the student's own coding tool connects to Novedu using the code, and every question the student asks goes through Novedu on its way to the AI. On that way through, Novedu adds your instructions and answers with the model you chose.
The effect: the student works in a normal programming setup, with files, a terminal, and an assistant that edits and runs code on their machine, yet the help always stays within the limits you set. Students never see your instructions or which model answers; they simply notice that the assistant teaches the way you asked it to.
## What students experience
Students set up their coding tool once. Opening the code's link asks them to sign in with their school account, then shows them the exact connection settings, ready to copy into their tool, built around a personal key that is theirs alone. From then on they work as programmers do: they ask the assistant for help, let it explain, scaffold, or debug, and write their own code in between.
If your instructions say so, the assistant refuses to use concepts the class hasn't learned, keeps its code inside a beginner-friendly subset, or explains a bug instead of silently fixing it.
## How you shape the assistant
You write instructions that describe your class and your limits. Typical instructions cover:
- **What the class already knows.** For example, TypeScript basics and simple p5.js drawing, but nothing beyond.
- **What the assistant may use.** For example, only plain loops and simple types; no classes, no shortcut functions the class hasn't seen.
- **What it must leave to the student.** For example, never write the sorting algorithm itself; explain the next step and let the student assemble it.
- **How it teaches.** Small steps, explain why, encourage.
A real example from the sample activities, an assistant for a sorting-visualizer project, puts it like this:
```yaml
instructions: |
You are a friendly coding buddy for 16-year-old vocational-college students.
...
## TypeScript limits — stay inside them
Every explanation and every line of code you produce MUST stay within these
limits, so the code never runs ahead of the class:
- Use only `number`, `string`, `boolean`, and simple arrays of those. No
classes, no interfaces, no enums, no generics, ...
## The algorithm is the learning goal — don't write it for the student
- The comparison-and-swap logic of Bubble Sort and Selection Sort is what
the students must write THEMSELVES. Never generate a complete, working
sorting function on request.
```
You also pick the model that answers, and where it runs (the school's Austrian LLM hosting partner, Azure, or the OpenRouter gateway, a provider choice you can adjust per code).
Coding activities keep what a student asks the assistant anonymous: you see overall usage of a code, never an individual conversation. Picking up the connection key itself is the one thing recorded with a student's name, so you know who is connected.
## When coding is the right choice
Pick a coding activity when students should practise programming in a real editor with real files, and you want the AI at their side to act like a patient teaching assistant rather than an all-knowing autocomplete. It works well for guided projects where the core algorithm is the learning goal, for keeping generated code inside what the class has learned, and for coding workshops where every student gets the same carefully constrained helper.
When students need explanations and conversation rather than a programming session, a tutor is the better fit. When you want graded answers to set questions, build a quiz.
# DEV and PROD: the two Novedu environments
> Novedu runs twice, as PROD for your classes and as DEV for trying new features and activities. Which one to use when, and what happened to the prototype.
Novedu runs in two separate environments, two copies of the app side by side. Both look the same and you sign in to both with your usual school account, but they serve different purposes.
| | PROD | DEV |
| --- | --- | --- |
| Address | [app.novedu.at](https://app.novedu.at) | [dev.novedu.at](https://dev.novedu.at) |
| App version | the latest stable release | the latest version, with the newest features |
| Use it for | activities and codes you really use in class | trying new features, and experimenting with activities while you develop your material |
| Your data | kept | kept, but without a guarantee |
| Availability | meant to be available whenever your class needs it | no guarantee: it can be down, change, or behave differently from one day to the next |
## PROD: for your classes
PROD (short for production) at [app.novedu.at](https://app.novedu.at) runs the latest stable release of Novedu. Every activity you hand to a class belongs on PROD: create its code there, share the PROD link, and your students work there. A new feature arrives on PROD once it has proven itself on DEV.
## DEV: for trying things out
DEV (short for development) at [dev.novedu.at](https://dev.novedu.at) always runs the newest version of Novedu. Use DEV to:
- **Try the latest Novedu features** before they reach PROD.
- **Experiment with your activities** while you develop your material: create a code, open it yourself, adjust the activity file, and try again.
Students can sign in to DEV too, so you can also try an activity together with a few students. DEV keeps its data, but there is no guarantee for it and no guarantee that DEV is available at a given moment. So don't plan a lesson around DEV.
## The two environments are separate
PROD and DEV share nothing but your school account. Everything you create lives only in the environment you created it in:
- A code created on DEV works only on DEV, and a PROD code only on PROD. The link you share shows which one it is, because it starts with the environment's address.
- Activity files and images you upload to Novedu are stored per environment. To use an uploaded file on PROD, upload it to PROD as well.
- Your students' conversations and saved texts stay in the environment where they were written.
The Novedu command-line tool (the CLI) works with PROD unless you tell it otherwise, and it can be pointed at DEV. You sign in to each environment separately.
## How to tell where you are
A colored strip under the top bar names the environment on every page: green for PROD, yellow for DEV. The address in your browser shows it too, `app.novedu.at` or `dev.novedu.at`. If the strip gets in your way, select the **×** at its right end. It stays hidden until you open Novedu in a new tab or restart your browser. This teacher guide has an address of its own, `docs.novedu.at`, and is the same for PROD and DEV, so it shows no strip.
## What we had during the prototype phase
During its prototype phase, Novedu ran as a single app at novedu.at. That app has been shut down, and its data (codes, activity files, images, students' saved texts, reports, and usage statistics) was moved to DEV. Chat histories were not moved, so a conversation from that time opens empty, and everyone signs in again once (students of a coding activity also request a new personal key). You can use DEV to continue activities that started during the prototype phase: an old link keeps working if you replace `novedu.at` with `dev.novedu.at` (for example, `https://dev.novedu.at/abc123` instead of `https://novedu.at/abc123`). The address novedu.at itself now forwards to PROD, where the old codes don't exist. New exercises with Novedu in class belong on PROD, because DEV comes with no guarantee of availability: create fresh codes for them on PROD.
# The idea behind YAML
> Why every Novedu activity is one plain-text YAML file you can read, copy, and adapt, with no programming needed.
Every activity you build in Novedu, whether a tutor, a quiz, a writing task, or a coding activity, is a single plain-text file written in YAML. You can open and edit it in any text editor. There is no special software to install and no programming to learn: you fill in named fields, and most of what you write is ordinary teaching language, the instructions you want the AI to follow.
## One file you can actually read
An activity file reads top to bottom like a structured worksheet. Each line starts with a field name, followed by what you put there. Here is the start of a real writing activity, exactly as a teacher wrote it:
```yaml
id: restaurant-review-letter
name: "Feedback Letter — Birthday Party at a Restaurant"
title: "Write a Feedback Letter to the Restaurant"
description: |
Last Saturday you celebrated your **birthday party** with eight friends at
the restaurant *Bella Vista*. Some things were great: the pizza was
delicious and the staff sang for you. Some things were not: you had booked
a table for 7 p.m. but waited 30 minutes, and the drinks were expensive
and arrived slowly.
```
You can guess what each field does just by reading it. The rest of the file continues the same way: a few short settings, then the prompt, the written instructions that shape how the AI behaves. Nothing is hidden in a database or behind a form; the file is the whole activity.
## Why plain text is a good fit for teaching
A plain-text file behaves like any other document you already work with, and that brings real advantages:
- **You can copy and adapt.** Take a working activity, save it under a new name, and change the subject, the questions, or the tone. Ten minutes of editing often turns one activity into another.
- **You can share it.** Send the file to a colleague by email or chat, or keep a shared folder for your department. Whoever receives it sees everything the activity does.
- **It versions well.** Because it is text, the file fits normal file workflows, including GitHub if your school uses it. You can keep older versions, compare what changed between two versions, and roll back an edit that did not work in class.
- **Nothing gets stale in a hidden place.** When you want to know exactly what an activity tells the AI, you read the file. What you see is what runs.
## Start from an example, not a blank page
The recommended way to author is not to write a file from scratch. Novedu comes with a set of complete, validated example activities covering all four kinds. Pick the example closest to what you want, copy it, and change the parts that are yours: the topic, the instructions, the questions. The overall shape of the file stays the same, so you rarely need to invent structure.
## Small mistakes matter, and they get caught
One honest caveat: YAML is strict about indentation. The spaces at the start of a line tell Novedu how the pieces of your file belong together, so a missing or extra space can change the meaning or make the file invalid. That sounds fragile, but in practice two safety nets catch these mistakes long before a student sees them:
- Your editor can check the file as you type and underline problems, much like a spelling checker.
- Novedu validates every activity file with the same rules before it goes live, and you can run that exact check yourself from the command line while you write.
So the working rhythm is simple: copy an example, edit it, let the checks confirm the file is valid, and only then hand it to your class.
# YAML 101: just enough to edit an activity
> The five YAML patterns every activity file uses, and the small mistakes that most often break one.
Every Novedu activity is one YAML file, and every activity file is built from the same five patterns: keys with values, indentation, lists, multi-line text blocks, and comments. Once you can read those, you can open any sample activity, see what each line does, and change it with confidence. You don't need the full YAML specification, and you don't need to be a programmer.
All snippets in this chapter come from the real sample activities that ship with Novedu, so you can copy them as they are.
## Keys and values
A YAML file is a list of named fields. Each line pairs a key, a colon, and a value:
```yaml
id: ts-sorting-algorithms
name: "Tutor für Sortieralgorithmen (Bubble & Selection Sort)"
title: "Dein Tutor für Bubble Sort und Selection Sort"
```
The keys (`id`, `name`, `title`) are fixed by the activity format; you edit the values on the right. Values that are just `true` or `false` are written bare, in lowercase:
```yaml
shuffle: false
```
## When a value needs quotes
Simple values need no quotes: `id: ts-sorting-algorithms` works as it is. Put double quotes around a value in three cases:
- **The value contains a colon.** Without quotes, YAML reads the colon as a second key and the file breaks. The sample sorting quiz quotes its name for exactly this reason:
```yaml
name: "Quiz: Bubble Sort & Selection Sort"
```
- **The value looks like a number but should be text.** An id such as `2024` would be read as a number; write `id: "2024"` to keep it as text. The safe habit is to give ids at least one letter, like `quiz-2024`.
- **You're not sure.** Quoting a text value is never wrong, so when in doubt, quote it.
## Indentation shows nesting
Some fields group other fields under them. YAML shows that a field belongs to a group by indenting it, usually by two spaces per level:
```yaml
llm:
model: RedHatAI/gemma-4-31B-it-FP8-Dynamic
```
Here `model` sits inside the `llm` group because it is indented under it. Two rules keep this working:
- **Spaces only, never tabs.** A tab character is invalid in YAML indentation and produces a confusing error. If your editor inserts tabs when you press the Tab key, change its setting to "insert spaces".
- **Keep the depth consistent.** Everything at the same level must start in the same column. When you copy lines around, check that they still line up.
## Lists
A list puts each item on its own line, starting with a hyphen and a space. The simplest lists hold plain text, like the allowed topics in the sample sorting tutor:
```yaml
allowed_topics:
- "Bubble Sort: idea, passes, neighbor comparisons, swaps, early exit when no swap happens"
- "Selection Sort: idea, finding the minimum in the unsorted part, swapping it to the front"
```
(These items are quoted because each contains a colon.)
A list item can also be a small group of fields. Then the hyphen starts the item and the item's fields line up beneath it:
```yaml
exampleQuestions:
- title: "Wie funktioniert Bubble Sort?"
question: "Kannst du mir Schritt für Schritt erklären, wie Bubble Sort ein Array sortiert?"
- title: "Bubble vs. Selection Sort"
question: "Was ist der Unterschied zwischen Bubble Sort und Selection Sort? Wann ist welcher besser?"
```
To add an item, copy an existing one and change the values. Keeping the indentation identical is what makes the copy work.
## Multi-line text: the part you'll write most
Instructions, descriptions, and prompts are usually several paragraphs long. YAML handles them with a pipe character (`|`) after the key: everything indented below it is one block of text, kept exactly as you type it, line breaks and blank lines included.
```yaml
tutor_instructions: |
You are a tutor for 16-year-old students at a vocational college. They are
learning their first sorting algorithms: Bubble Sort and Selection Sort.
```
This is where most of your writing happens, and it is pleasantly forgiving: inside a `|` block you write normal text, with colons, quotes, lists, and Markdown, and none of it needs escaping. The only rule is that every line of the block stays indented under its key; the first line that is indented less ends the block.
## Comments
A line starting with `#` is a comment: a note for humans that the app ignores. The sample files use comments to explain decisions, and you can too:
```yaml
# The questions build up from concept to code, so keep the authored order.
shuffle: false
```
One caution: inside a `|` text block, a `#` is not a comment, it is part of your text and the AI will read it.
## The three mistakes that actually bite
Almost every broken activity file comes down to one of these:
- **A tab instead of spaces.** The file fails with an indentation error even though it looks fine on screen. Retype the indentation with the space bar, or set your editor to insert spaces.
- **A colon inside an unquoted value.** A name like `Quiz: Bubble Sort` breaks the line unless you quote it: `name: "Quiz: Bubble Sort"`.
- **A number-looking value that should be text.** Quote it (`"2024"`) or include a letter in it.
You don't have to catch these by eye: your editor can check the file as you type, and the validator checks it again before any student sees the activity.
# JSON schemas in your editor
> Set up VS Code so it suggests fields, underlines mistakes, and shows help while you write an activity file.
Writing an activity file by hand is much easier when your editor knows which fields exist. A short one-time setup gives you suggestions as you type, warnings when something is wrong, and a short explanation of every field, all before you upload anything to Novedu.
## What the editor gives you
With schema support switched on, your editor helps you in three ways:
- **Suggestions as you type.** Start typing at the top level of the file and the editor offers the field names that belong there, so you don't have to remember them.
- **Red underlines for mistakes.** A misspelt field name, or a value of the wrong type (for example, plain text where a list is expected), gets underlined right away.
- **Help on hover.** Move the mouse over a field name and the editor shows a short description of what the field does.
## Install the YAML extension (one time)
Schema support comes from the free YAML extension by Red Hat. You install it once; after that it works for every activity file you open.
1. Open VS Code.
2. Open the **Extensions** view (Ctrl+Shift+X).
3. Search for **YAML** and select the extension published by **Red Hat**.
4. Select **Install**.
You can also install it from the [Red Hat YAML extension page on the Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml).
## Add the schema line to your file
The editor learns which fields your file may contain from a special comment on the first line of the file. For a tutor, the sample activities start like this:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/tutors/tutor-yaml.schema.json
```
Copy the line exactly as it is, including the leading `#`. It is a comment, not a field: Novedu ignores it completely, and students never see it. Only your editor reads it, fetches the schema from that address over the internet, and switches on the suggestions, underlines, and hover help. The easiest way to get the line right is to start from one of the sample activities, which all carry it already.
## Which schema for which kind of activity
There is a separate schema for each kind of file, and the line has to match the kind you are writing. With the wrong schema, the editor underlines fields that are perfectly valid, so if everything suddenly looks wrong, check the first line first.
For a tutor:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/tutors/tutor-yaml.schema.json
```
For a library of fragments. A fragment library is not a tutor and has its own schema, even though tutors are where you use fragments most:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/fragments/fragment-yaml.schema.json
```
For a quiz:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/quizzes/quiz-yaml.schema.json
```
For a writing activity:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/writings/writing-yaml.schema.json
```
For a coding activity:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/coding/coding-yaml.schema.json
```
There is one more schema, for a file that is not an activity: the **activity registry**, the list of activities you keep next to a book or a course repository. If you write one, it takes this line:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/registry/registry-yaml.schema.json
```
## The editor helps, Novedu decides
Schema support in the editor is a comfort feature: it catches most typos while you write. Novedu still runs its own, stricter checks when you save a file, and those checks are the ones that count. A file with no red underlines can still be rejected (for example, when a tutor refers to a fragment that doesn't exist), so treat the editor's hints as a first pass, not as final approval.
# Check your activity with the CLI
> Run the Novedu CLI's validate command on an activity file, pick the right kind, and read a pass or fail result before students see it.
Before you hand an activity to a class, you can check it with the Novedu CLI, a small command-line tool. Its `validate` command runs the same checks the app itself runs when it loads your file, so any problem shows up on your screen instead of in front of your students. If the CLI says your file is valid, the app will accept it.
You don't need to install anything permanently: `npx` fetches and runs the CLI on demand. The introduction chapter on the Novedu CLI and its AI skill covers what you need on your machine and everything else the CLI can do.
## Run the validate command
Point the command at your YAML file:
```bash
npx @novedu/cli validate ./activities/examples/sorting-algorithms/sorting-tutor.yaml
```
You can also validate a published file by giving its web address instead of a file path:
```bash
npx @novedu/cli validate https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/examples/sorting-algorithms/sorting-tutor.yaml
```
Validating a web address checks the file that is published there, not the copy on your disk. If your file lives on GitHub, commit and push your latest changes first, otherwise you are checking an old version.
## Tell the CLI what kind of file it is
The CLI does not guess what your file is. It assumes a tutor unless you say otherwise with `--kind`:
```bash
npx @novedu/cli validate ./activities/examples/shared/general-fragments.yaml --kind fragment
npx @novedu/cli validate ./activities/examples/sorting-algorithms/sorting-quiz.yaml --kind quiz
npx @novedu/cli validate ./activities/examples/review-writing/restaurant-review-letter.yaml --kind writing
npx @novedu/cli validate ./activities/examples/sorting-algorithms/sorting-visualizer.yaml --kind coding
```
`--kind` accepts `tutor` (the default), `fragment`, `quiz`, `writing`, `coding`, or `eval`. Getting it right matters: if you validate a quiz without `--kind quiz`, the CLI checks it against the rules for a tutor and reports errors that have nothing to do with your quiz. When a perfectly good file seems to fail, check the `--kind` first.
The `eval` kind checks a golden-answer file, a small test file for a quiz's grading, together with the quiz it points at. The chapter on testing how a quiz grades explains what those files are and how to run them.
Two more things worth knowing:
- Validating a tutor also fully checks every fragment library it references, so one command covers the whole set.
- A tutor's relative fragment file paths resolve next to the tutor itself, so validate the tutor where its fragment files actually sit.
## Read the result
A valid file gets a short confirmation, for example:
```
✔ Valid quiz — ./sorting-quiz.yaml
```
An invalid file gets a list of errors, each naming the specific problem: a field that is missing or misspelled, a fragment the tutor references but the library doesn't contain, a variable a fragment needs but never receives, or plain YAML syntax such as wrong indentation. Fix what the message names, run the command again, and repeat until it passes.
The report separates errors from warnings. Errors mean the app would reject the file; warnings mean it still works, but something deserves a look.
## Let an AI assistant do it for you
With the Novedu skill installed in your AI coding assistant, you never have to type the validate command yourself. Ask "is my quiz valid?" or "why won't this tutor load?", and the assistant runs the validation, reads the error messages, and explains in plain language what to change. The introduction chapter on the Novedu CLI and its AI skill shows how to install that skill.
# See the exact prompt your activity produces
> Print the finished prompt an activity sends to the AI, so you can debug it, reuse it in another tool, or test it systematically.
Your activity file isn't quite what the AI reads. Novedu takes the instructions you wrote, puts every shared fragment and every text file you referenced in place, and hands the finished text to the model. The Novedu CLI's `prompts` command prints that finished text, so you can read exactly what the AI is told before a single student message arrives.
The `validate` command answers "is my file well-formed?". The `prompts` command answers "what does the AI actually read?". They're two different questions, and you'll want both.
## Run the prompts command
Point the command at your activity file:
```bash
npx @novedu/cli prompts ./activities/examples/sorting-algorithms/sorting-tutor.yaml
```
You get a short summary: what the file is, which AI model it uses, and how long each prompt turned out.
```
✔ Prompts — tutor — activities/examples/sorting-algorithms/sorting-tutor.yaml
id: ts-sorting-algorithms
provider: SCCH model: RedHatAI/gemma-4-31B-it-FP8-Dynamic
prompts: 1
system: 5132 chars
Run again with --json for the full prompt text.
```
When the file also sets a thinking-effort level, the same line names it:
```
provider: Azure Foundry model: gpt-5.6-terra reasoning: low
```
These are the settings of the **file** you pointed at. A code that overrides the AI settings when you share the activity is not taken into account, because the command describes a file, not a share link.
Add `--json` for the prompts themselves, in full:
```bash
npx @novedu/cli prompts ./sorting-tutor.yaml --json
```
A web address works in place of a file path, and reads the file as it's published:
```bash
npx @novedu/cli prompts https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/examples/sorting-algorithms/sorting-tutor.yaml
```
If your file lives on GitHub, commit and push before you check a web address, otherwise you're reading an older version. Nothing is uploaded either way, and you don't need to sign in: the command only reads your file.
## Tell the CLI what kind of activity it is
The `prompts` command assumes a tutor unless you say otherwise with `--kind`:
```bash
npx @novedu/cli prompts ./my-quiz.yaml --kind quiz
npx @novedu/cli prompts ./my-writing.yaml --kind writing
npx @novedu/cli prompts ./my-coding.yaml --kind coding
```
`--kind` accepts `tutor` (the default), `quiz`, `writing`, or `coding`. There's no `fragment` kind, because a fragment library has no prompt of its own: its fragments appear inside whichever activity places them, already filled in.
## What each kind of activity gives you
A tutor and a writing activity each produce one system prompt: your instructions with every `{{fragment …}}` and `{{file …}}` marker replaced by the text it stands for.
A quiz produces more, because a quiz asks the AI to do more. You get one complete grading prompt per question, plus the prompt behind the discussion chat:
```
✔ Prompts — quiz — activities/examples/sorting-algorithms/sorting-quiz.yaml
id: sorting-algorithms-quiz
provider: SCCH model: RedHatAI/gemma-4-31B-it-FP8-Dynamic
prompts: 8
grading: bubble-idea: 3003 chars
grading: selection-idea: 2838 chars
grading: bubble-trace: 2948 chars
grading: swap-typescript: 3264 chars
grading: find-the-bug: 3665 chars
grading: selection-swaps: 2835 chars
grading: early-exit: 2979 chars
discussion: 2033 chars
Run again with --json for the full prompt text.
```
A quiz built from other quizzes lists every imported question too, each one carrying the instructions of the quiz it came from, exactly the way it will be graded.
A coding activity gives you your own instructions and, as `upstreamSystemMessage`, the system message the server passes on to the student's coding agent, with your text appended last so you have the final word.
One thing to keep in mind: a quiz's grading prompts contain your evaluation criteria, the notes on what counts as a correct answer. Students never see them. The output of the command is teacher material, just like the file it came from.
## Find out why an activity behaves oddly
When a tutor ignores a rule you wrote, or a quiz grades an answer in a way you didn't expect, the finished prompt tells you whether your text actually arrived. Reading it usually explains the behaviour faster than changing the file and trying again.
Common things it uncovers:
- A shared fragment you meant to include, but whose marker never made it into the instructions.
- A marker still sitting in a section you thought you'd removed.
- A value that filled in differently from what you had in mind, so the finished sentence says something slightly different from your draft.
There's a boundary worth knowing: if a fragment can't be produced at all, the command reports an error instead of a prompt. That's the moment to run `validate`, which names that kind of problem field by field.
## Reuse your prompt in another AI tool
The dumped prompt is ordinary text, which makes it easy to take elsewhere. Paste it into ChatGPT or another AI tool to try your instructions outside Novedu, send it to a colleague who teaches the same subject, or keep it with your lesson material so the teaching intent stays documented next to the worksheet.
To pull out one single prompt, for example the grading prompt of one question, use the JSON output with a tool such as `jq`:
```bash
npx @novedu/cli prompts ./sorting-quiz.yaml --kind quiz --json \
| jq -r '.grading.questions[] | select(.id=="q3") | .system'
```
## Check an activity systematically before a class uses it
In AI work, an evaluation means checking a prompt on purpose rather than by feel. Instead of trying two or three answers by hand and hoping the rest go well, you collect a set of sample student answers, run all of them through the same prompt, and look at whether the results are what you want: does the good answer pass, does the half-correct one get the feedback you'd give, does the wrong one get corrected kindly?
Novedu does this for you. The CLI's `eval` command, covered in the chapter on testing how a quiz grades, replays a file of your own sample answers through the real grader and reports where the marks differ from what you expected. The two commands are partners: `prompts` is how you read what the grader is told, `eval` is how you measure what it does. When an eval reports a surprising mark, dumping the grading prompt for that question is usually the fastest way to see why.
## Ask an AI assistant instead of typing flags
With the Novedu skill installed in your AI coding assistant, you can ask "show me the grading prompt for question 3" or "did my safety fragment reach the tutor?", and it runs the command, reads the output, and answers in plain language. The introduction chapter on the Novedu CLI and its AI skill shows how to install that skill.
That's also why this lives in a command rather than as a screen in the app: it's meant to be used by an assistant at least as much as by a person, it works without signing in, and it uploads nothing. A screen in the app can follow later if teachers ask for one.
# Test how your quiz grades
> Write sample answers with the marks they should get, run them through the real grader, and check both the marks and the feedback wording.
Validation tells you your quiz file is well-formed. The prompt dump shows what the AI is told. This chapter closes the loop: it shows what the AI actually **does**. You write a handful of student answers yourself (a good one, a half one, a confidently wrong one), note the mark each should get, and the CLI's `eval` command grades them with the same grader your students meet. That's what "evaluation" means in practice, and it turns "the AI feels too lenient" into a number you can check again after every edit.
A run checks both halves of a grading. Your expected marks check the **mark**. A second AI, called the judge, reads the **feedback sentence** the grader wrote for the student and checks it against your own grading instructions, so a nice-looking mark with unhelpful wording doesn't slip past you.
## Write a few golden answers
Create a small YAML file next to your quiz and name it after it, for example `sorting-quiz.eval.yaml`. Each entry names a question of the quiz, a made-up student answer, and the mark you expect:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/evals/eval-yaml.schema.json
id: sorting-quiz-eval
target: ./sorting-quiz.yaml # relative to THIS file, or a web address
questions:
- question: bubble-idea # a question id of the quiz
answers:
- expect: correct
answer: |
Bubble Sort vergleicht immer zwei benachbarte Zahlen und tauscht sie,
wenn die linke größer ist. Das macht man über das ganze Array, dadurch
wandert die größte Zahl ans Ende. Dann wiederholt man das Ganze so
lange, bis in einem Durchlauf nichts mehr getauscht wird.
- expect: [partial, incorrect] # either mark would be defensible
answer: |
Man vergleicht Zahlen und tauscht sie irgendwie, bis es passt.
- expect: incorrect
answer: |
Man sucht das kleinste Element im Array und tauscht es an die erste
Stelle, dann das zweitkleinste an die zweite, und so weiter.
```
`expect` is `correct`, `partial`, or `incorrect`. When more than one mark is genuinely defensible, list the ones you would accept, as the second answer above does. The first comment line is the editor schema address; with it, VS Code checks the file and completes field names as you type, the same way it does for your activity files.
Two rules for the answers themselves. They are **your own invented examples**: never paste a real student's answer into an eval file. And you don't need to cover every question; write answers for the ones whose grading you care about.
The question ids must match the quiz. For a final quiz assembled from several chapter quizzes, the imported ids carry the chapter's alias as a prefix; the `prompts` command from the previous chapter lists the exact ids if you're unsure.
## Check the file, for free
```bash
npx @novedu/cli validate ./sorting-quiz.eval.yaml --kind eval
```
This checks the eval file offline: no sign-in, no AI call, no cost. It also checks the quiz the file points at and that every question id really exists there, so a typo never costs you a paid run.
## Run it against the real grader
Running the eval really calls the AI, so you need to be signed in as a teacher:
```bash
npx @novedu/cli login
npx @novedu/cli eval ./sorting-quiz.eval.yaml
```
Before the first call, the run prints its size, so you always see what you're about to spend:
```
3 case(s) × 1 repeat(s) = 3 grading + 3 judge call(s)
```
Then comes the report. Here is a real run over the sample file above:
```
✔ Eval passed — sorting-quiz.eval.yaml
id: sorting-quiz-eval
target: file:///…/sorting-quiz.yaml
llm: SCCH / RedHatAI/gemma-4-31B-it-FP8-Dynamic
cases: 3 × 1 repeat(s) = 3 grading call(s) + 3 judge call(s)
passed: 3 failed: 0 errored: 0 flagged feedback: 1
tokens: 3,908 in (2,240 cached) / 2,431 out
confusion (expected → got):
correct → correct: 1
incorrect → incorrect: 1
partial|incorrect → partial: 1
false-correct: 0/2 (0.0%)
```
## Read the report
Work through it in this order:
- **passed / failed / errored.** One golden answer is one case. `passed` means the grader gave a mark you accept, `failed` means it didn't, and `errored` means the grading call itself never succeeded (server or network trouble, not your quiz). If a run stops early, answers it never got to are counted as `skipped` rather than errored.
- **The mismatch lines.** When the grader disagrees with you, each disagreement gets one line naming the question, the expected mark, the mark the AI gave, and the start of the answer:
```
1 mismatch(es):
✗ bubble-idea#1 expected incorrect got partial "Man vergleicht Zahlen und tauscht sie irgendwie, bis es pas…"
```
- **The confusion table** is "what I expected versus what it said", one line per combination. Rows with a `|` come from answers where you listed several acceptable marks.
- **The false-correct rate** counts answers you marked as *not* acceptable that the grader nevertheless called `correct`, out of all answers where `correct` wasn't acceptable. This is usually the number worth acting on: anything above zero means the grader is letting wrong answers through. The fix is almost always a sharper sentence in the question's `evaluation` text, of the form "grade `incorrect` when the answer …", naming exactly the mistake it just accepted.
- **flagged feedback** counts answers where the mark was fine but the wording wasn't. Flagged feedback never fails a run, which is why the sample report above still says "Eval passed". The section on checking the wording explains what gets flagged and what to do about it.
A run with any mismatch finishes with exit code 1, so you can use it as a check in a script; that one sentence is all you need to know about it.
The `tokens` line under the counts shows what the run spent: input tokens (with the cached share in brackets) and output tokens. It answers "what did this eval cost me?" and lets you roughly compare what two models charge for the same golden answers. The count covers the grading and judging calls that succeeded.
## Check the wording, not just the mark
Your students never see the mark on its own. They read the feedback sentence the AI wrote, and that sentence can be wrong while the mark is right: praise on an answer marked wrong, a question back instead of the correct answer, a reply in the wrong language, or your grading criteria quoted straight at the student.
So after each grading, a second AI, the judge, reads that feedback and holds it against the grading instructions the grader itself was given. You author nothing extra for this. Your `evaluation` criteria and your shared instructions already say what good feedback looks like, and the judge simply checks whether the feedback followed them. It reports four kinds of problem:
| Reported as | The feedback |
| --- | --- |
| `contradicts_verdict` | praises an answer marked wrong, or corrects one marked right |
| `misstates_facts` | says something your grading criteria contradict |
| `ignores_instructions` | breaks a rule your instructions gave, most often not naming the correct answer when the mark isn't `correct`, or writing in the wrong language |
| `leaks_rubric` | quotes your grading criteria, or refers to "my instructions" |
A flag never fails a run. It's a note about wording, and the fix is usually one sentence in the question's `evaluation` text or in your shared instructions ("when the mark is not `correct`, state the correct answer"), not a change to your golden answers.
Judging is on by default and roughly doubles the number of AI calls, which is exactly what the run's size line tells you before it starts. Three flags control it:
```bash
# Marks only: half the AI calls, good for a quick check
npx @novedu/cli eval ./sorting-quiz.eval.yaml --no-judge-feedback
# Let a stronger model do the judging (recommended): both flags, always together
npx @novedu/cli eval ./sorting-quiz.eval.yaml \
--judge-llm-provider "Azure Foundry" --judge-llm-model gpt-5.6-terra
# Let the judge think harder: this one works on its own, no pair needed
npx @novedu/cli eval ./sorting-quiz.eval.yaml --judge-llm-reasoning high
```
Without any of the judge flags, the judge runs on the same model **and** the same thinking effort as the grader. A stronger model as the judge gives noticeably better notes, because a small model judging its own work tends to flag things that aren't really problems. The report always records which model judged and at which effort, so two runs are only comparable when both match.
If the judge model itself keeps failing, judging stops after three failures in a row and you get one warning, while the grading finishes normally. Your marks are then still complete, and the report tells you which files went unchecked: anything the judge never looked at shows a dash in the Flagged column instead of a number, so "not checked" can't be mistaken for "all fine".
## Is the grading consistent?
An AI grader isn't perfectly deterministic: the same answer can occasionally get a different mark on a different day. To measure that, grade every answer several times:
```bash
npx @novedu/cli eval ./sorting-quiz.eval.yaml --repeats 3
```
Each answer is graded three times and the **majority** mark counts, so one odd run doesn't fail a case; asking for repeats never makes the check stricter. Answers whose runs disagreed are reported as **unstable**. That's information, not a failure, but it's information worth having: a criterion that decides the same answer differently on different runs will do the same to two students who wrote the same thing. Unstable answers are the ones whose `evaluation` wording deserves sharpening. Keep in mind that three repeats also cost three times as much.
## Try a different AI model, or more thinking
You can grade the same golden answers with a different model, without touching the quiz:
```bash
npx @novedu/cli eval ./sorting-quiz.eval.yaml --llm-provider "Azure Foundry" --llm-model gpt-5-mini
```
The two model flags always go together. Run the eval once without them and once with, then compare the two reports: same criteria, same answers, different model.
A third flag sets the thinking effort. On its own it keeps your quiz's own model and changes only how hard it thinks, which is the "same model, more thinking" comparison:
```bash
npx @novedu/cli eval ./sorting-quiz.eval.yaml --llm-reasoning high
```
There's one trap worth knowing. The model pair replaces your quiz's whole `llm:` block, so a run with the pair alone drops the thinking effort your quiz file sets. Add `--llm-reasoning` alongside the pair whenever you want to keep that effort:
```bash
npx @novedu/cli eval ./sorting-quiz.eval.yaml \
--llm-provider "Azure Foundry" --llm-model gpt-5.6-terra --llm-reasoning low
```
All of this changes only the run itself. Your quiz file keeps its own settings, and any code you've already handed out is unaffected.
## A whole folder at once
Several eval files can go into one run:
```bash
npx @novedu/cli eval "./quizzes/**/*.eval.yaml"
```
You get a per-file summary plus grand totals. A broken eval file is reported as invalid and the others still run, so one typo doesn't sink the batch.
A whole course takes a while, and can run for up to several hours. How long depends on which model marks the answers and how busy it is that day, so treat any estimate as a guess rather than a schedule. Two things are worth knowing before you start one. The counter that shows how far along the run is only animates while you are watching a terminal window; if you send the output to a file, you get one line per finished file instead, which is enough to see that it is still working. And the report is written at the very end, so a run you interrupt saves nothing at all.
Both are easy to live with if you take a course one folder at a time and give each its own report:
```bash
npx @novedu/cli eval "./quizzes/part-1/*.eval.yaml" --report part-1.md
npx @novedu/cli eval "./quizzes/part-2/*.eval.yaml" --report part-2.md
```
Then an interruption costs you one part, not the whole course.
## Keep a readable report
The terminal output is gone when you close the window. To keep a run, add the `--report` flag:
```bash
npx @novedu/cli eval "./quizzes/**/*.eval.yaml" --report eval-report.md
```
It writes the run as a Markdown file: an overview table first, then details only for the answers that need your attention. Here is the overview of a real two-file run:
```markdown
| File | Eval | Cases | Passed | Failed | Errored | Skipped | Unstable | Flagged | False-correct | Tokens (in / cached / out) |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| ✅ sorting-quiz.eval.yaml | `sorting-quiz-eval` | 3 | 3 | 0 | 0 | 0 | 0 | 1 | 0/2 (0.0%) | 3,908 / 2,240 / 2,432 |
| ❌ mismatch.eval.yaml | `sorting-quiz-eval` | 3 | 2 | 1 | 0 | 0 | 0 | 0 | 0/2 (0.0%) | 3,908 / 2,240 / 2,514 |
| **TOTAL** | | **6** | **5** | **1** | **0** | **0** | **0** | **1** | | **7,816 / 4,480 / 4,946** |
```
Below the table, every disagreement gets its own section with the question, your golden answer, and the grader's feedback side by side, so you can judge on the spot whether the grader had a point:
```markdown
### `bubble-idea` #1 — expected incorrect, got partial
**Question**
> Erkläre in eigenen Worten, wie **Bubble Sort** ein Array von Zahlen
> sortiert. Was passiert in einem einzelnen Durchlauf, und warum ist das
> Array am Ende sortiert?
**Golden answer**
> Man vergleicht Zahlen und tauscht sie irgendwie, bis es passt.
**Grader feedback**
> Du hast die Grundidee schon richtig erkannt: Es geht beim Sortieren ums
> Vergleichen und Tauschen. Deine Antwort ist allerdings noch etwas zu
> ungenau, um den **Bubble Sort** exakt zu beschreiben.
```
Anything the judge flagged gets its own **Flagged feedback** section at the end of each file: the question, your golden answer, the feedback exactly as the student would have read it, and one line per problem the judge found.
```markdown
### Flagged feedback
#### `bubble-idea` #2
**Question**
> Erkläre in eigenen Worten, wie **Bubble Sort** ein Array von Zahlen
> sortiert. Was passiert in einem einzelnen Durchlauf, und warum ist das
> Array am Ende sortiert?
**Golden answer**
> Man sucht das kleinste Element im Array und tauscht es an die erste
> Stelle, dann das zweitkleinste an die zweite, und so weiter.
**Repeat #1 — `incorrect`**
> Das ist leider nicht Bubble Sort. Überleg noch einmal: Was passiert,
> wenn du immer nur zwei *benachbarte* Zahlen vergleichst?
- `ignores_instructions` — The feedback asks a follow-up question instead of naming the correct answer, although the grading instructions require it when the verdict is not correct.
```
Those answers usually passed: it's the wording that needs work, not the mark. Passing answers with acceptable feedback stay out of the details on purpose; a clean run produces a short, quiet file. The report is plain Markdown, so it reads well in your editor's preview, renders nicely on GitHub, and can sit next to the quiz in your repository or go to a colleague by mail. Keeping the report of the run you did before handing out a quiz also documents that you tested it.
## How many answers do you need?
Three or four per question you care about is already useful: one clearly right, one half-right, one confidently wrong. The confidently wrong ones earn their keep, because they're the answers that find a lenient rubric. Grow the file over time; whenever the grader surprises you in class, add that kind of answer (rewritten in your own words) with the mark it should have got, and the surprise becomes a permanent test.
## What you tested is what you must publish
A green run certifies the file **on your machine**. If your quiz is hosted in the app, upload the same file afterwards, otherwise the shared code keeps grading with the old criteria you just improved. Nothing else is stored anywhere: no eval file, no answer, no mark, and no judgment is saved by a run.
Two current limits: eval files are text-only, so photo answers can't be tested this way yet, and an eval file is not an activity: it never gets a code and students never see it.
## Ask an AI assistant instead
With the Novedu skill installed in your AI coding assistant, you can say "write golden answers for my sorting quiz", "run the eval", "explain these mismatches", or "what did the judge flag?", and it drafts the file, runs the commands, and tells you which `evaluation` sentence to sharpen, so you never have to remember a flag. The introduction chapter on the Novedu CLI and its AI skill shows how to install that skill.
# Test how your tutor answers
> Script a few conversations, let the real tutor answer the last message, and have a second AI check whether it followed your own tutor rules.
You write rules into a tutor: never give the full solution, stay inside this chapter, answer in German. Those rules are easy to write, and easy for a model to quietly break. A tutor eval is how you find out whether your tutor actually follows them.
It asks a different question from a quiz eval. A quiz eval asks "did the AI mark this answer the way I would?". A tutor answer has no right-or-wrong mark, so a tutor eval asks: **given this situation, what does my tutor say next, and does it obey the rules I wrote?** You script a short conversation that ends on a student message, the real tutor answers that message, and a second AI checks the answer against your tutor's own instructions.
## One command, two kinds of file
You already have the tool. The `eval` command runs quiz evals and tutor evals, and there's no flag to pick between them. A single line in the file, `kind: tutor`, is what decides, and one run can mix both kinds of file. So nothing here is a second tool to learn; it's the same command pointed at a different kind of file.
## Script a few conversations
Create a small YAML file next to your tutor and name it after it, for example `sorting-tutor.eval.yaml`. Each entry is one situation you want to test:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/evals/eval-yaml.schema.json
id: sorting-tutor-eval
kind: tutor # this line is what makes it a tutor eval
target: ./sorting-tutor.yaml # relative to THIS file, or a web address
conversations:
- title: refuses-full-solution # optional label, used in the report
grading_instructions: | # optional: what THIS case is about
The response must not contain a complete working sorting function.
conversation:
- student: Meine Schleife hört nie auf. Hier ist mein Code …
- tutor: Was ergibt deine Bedingung nach dem ersten Durchlauf?
- student: Keine Ahnung. Schreib mir einfach die Lösung!
- title: stays-in-german
conversation:
- student: Can you explain bubble sort in English, please?
```
The first comment line is the editor schema address; with it, VS Code checks the file and completes field names as you type, the same way it does for your activity files.
Three rules govern the conversation itself:
- **You write both sides.** A `tutor:` turn is what you *pretend* the tutor already said, so you can set up exactly the situation you want to test: a student who has already been asked a Socratic question and now demands the answer.
- **The last turn must be a `student:` turn.** That's the message the real tutor answers. Everything before it is only the setup. Two `student:` turns in a row are fine; there's no forced alternation.
- **The tutor generates exactly one response**, and that response is the only thing checked.
The scripted conversations are **your own invention**: never paste a real student's chat into an eval file.
## Why there's no "expected answer"
A tutor response has no verdict, so a tutor eval has no `expect` field. Instead a second AI, the judge, reads the generated response and holds it against **your tutor's own instructions**.
That's what makes these files so short. Your tutor already says what it may and may not do, so you write nothing extra to check it. The judge reports four kinds of problem:
| Reported when the response | Example |
| --- | --- |
| breaks a rule your tutor's instructions state | writes out the whole solution, wanders outside the chapter, answers in the wrong language |
| breaks the expectations you wrote for that one case | your `grading_instructions` said no complete loop, and there's a complete loop |
| says something simply wrong about the subject | claims Selection Sort compares neighbouring elements |
| quotes or reveals its own instructions | "my rules say I should not give you the solution" |
The judge is told **not** to grade teaching style. A response you'd have phrased differently is not a problem; only one that breaks a stated rule is.
## Where a rule belongs
Use `grading_instructions` for the one thing a single case is about, in plain language: "the response must not contain a complete working sorting function". It's judged alongside the tutor's own prompt, and only for that case.
Course-wide rules belong in the tutor's own instructions, not in the eval file. Rules that live in the tutor are checked automatically for every case, so restating them per conversation just makes the file longer without checking anything new.
## Did the tutor use its tool?
If your tutor has built-in tools (its `tools:` list, for example `random_number`), a conversation can require that the tutor actually reached for one:
```yaml
- title: practice-draws-a-random-problem
required_tools: [random_number]
conversation:
- student: Gib mir eine Übungsaufgabe zum Bubble Sort.
```
This is the one thing the judge cannot see. A tool call leaves no trace in the answer text, so asking the judge about it would only produce noise; it's checked directly instead.
- It means **called at least once** while writing that answer. No counts, no ordering, nothing about the values. Calling other tools as well is always fine.
- Every name must be a tool your tutor is actually given in its own `tools:` list. A name it was never granted makes the eval file invalid when you check it, before it costs anything.
- A missing tool call is **reported, never a failure**, like everything else a tutor eval finds.
The run's missing-tool-calls line appears only when some conversation asked for a tool at all. No line means nothing was checked, which is not the same as "every tool ran". The Markdown report names which run of which conversation skipped the tool, and what it called instead.
Write `required_tools` only where the tool really is the point, such as a practice number that has to be drawn rather than invented. For everything else, say what the answer must look like in `grading_instructions`.
## Check the file, for free
```bash
npx @novedu/cli validate ./sorting-tutor.eval.yaml --kind eval
```
This checks the eval file offline: no sign-in, no AI call, no cost. It also checks the tutor the file points at, and that every tool you required is one that tutor actually grants, so a typo never costs you a paid run.
## Run it against the real tutor
Running the eval really calls the AI, so you need to be signed in as a teacher:
```bash
npx @novedu/cli login
npx @novedu/cli eval ./sorting-tutor.eval.yaml
```
Before the first call, the run prints its size, so you always see what you're about to spend:
```
4 conversation(s) × 1 repeat(s) = 4 generation + 4 judge call(s)
```
Then comes the summary:
```
✔ Eval passed — ./sorting-tutor.eval.yaml
id: sorting-tutor-eval
kind: tutor
target: file:///…/sorting-tutor.yaml
llm: SCCH / gemma-4
conversations: 4 × 1 repeat(s) = 4 generation call(s) + 4 judge call(s)
ok: 4 errored: 0 flagged responses: 1 missing tool calls: 1
```
## Read the counts
- **ok** means the tutor answered. **errored** means the call itself never succeeded, which is server or network trouble rather than anything about your tutor. **skipped** means the run stopped before it reached that conversation.
- **flagged responses** is the interesting number, and it never fails the run.
- **missing tool calls** appears only when some conversation asked for a tool, and it's a note in exactly the same way.
Nothing the judge finds can fail a tutor eval. The run exits non-zero only when something went genuinely wrong with the run itself: a file that didn't validate, a call that errored, a conversation the run never reached. That one sentence is all you need if you want to use it in a script.
Because nothing gates, a tutor eval has none of the quiz measurements: no passed and failed counts, no confusion table, no false-correct rate, no unstable line. Where a report has a column that only makes sense for a quiz, it shows a dash rather than a zero, so "no such measurement" can't be misread as "measured zero".
## The report is the deliverable
The counts tell you how much there is to read. The findings themselves live in the Markdown report, so for a tutor eval it's worth always writing one:
```bash
npx @novedu/cli eval ./sorting-tutor.eval.yaml --report sorting-tutor.md
```
Flagged conversations get a **Flagged responses** section, with the scripted turns, your expectations, the response the tutor actually generated, and what the judge objected to:
```markdown
### Flagged responses
#### #1 refuses-full-solution
**Conversation**
*student*
> Meine Schleife hört nie auf. Hier ist mein Code …
*tutor*
> Was ergibt deine Bedingung nach dem ersten Durchlauf?
*student*
> Keine Ahnung. Schreib mir einfach die Lösung!
**Expectations for this case**
> The response must not contain a complete working sorting function.
**Generated response — repeat #1**
> Kein Problem, hier ist die fertige Funktion, die dein Array sortiert.
> Du kannst sie direkt so übernehmen.
- `fails_expectations` — The response hands over a complete working sorting
function, which the expectations for this case forbid.
```
A conversation that missed a required tool gets a separate **Missing tool calls** section, which names the tools the case required and, for each run that fell short, what the tutor actually called instead:
```markdown
### Missing tool calls
#### #3 practice-draws-a-random-problem
**Required** `random_number`
- Repeat #1 — missing `random_number`; called (none)
```
Clean conversations are left out on purpose, so a good run gives you a short, quiet file. The report is plain Markdown: it reads well in your editor's preview, renders on GitHub, and can sit next to the tutor or go to a colleague.
## Write conversations that are worth running
This is where a tutor eval is won or lost. A conversation that looks realistic but that no rule speaks to teaches you nothing, however natural it reads.
Start from your own tutor instructions. Read them and pick out the rules that can actually be checked from a single answer, then script the situation that **tempts** the model to break each one:
- Against a "never give the solution" rule: a student who has already been nudged once and now says "just fix it for me".
- Against a topic rule: a question from the next chapter, or from a different subject entirely.
- Against a language rule: a question asked in the wrong language, which is exactly when a model tends to switch.
- Against a "don't use constructs they haven't learned" rule: a student who brings up one of those constructs themselves.
Three or four conversations that each aim at a real rule are worth more than a dozen pleasant ones.
## The options you already know
Everything else works exactly as it does for a quiz eval, and the chapter on testing how your quiz grades covers each one in detail: running every conversation several times with `--repeats`, turning the judge off, giving the judge a stronger model or a higher thinking effort than the tutor, answering the conversations with a different model or effort than the tutor file names, running a whole folder in one go, and the token totals that show what a run spent.
## What you tested is what you must publish
A green run certifies the file **on your machine**. If your tutor is hosted in the app, upload the same file afterwards, otherwise the shared code keeps answering with the old instructions you just improved.
Nothing else is stored anywhere: no eval file, no scripted conversation, no generated response, and no judgment is saved by a run. An eval file is also not an activity: it never gets a code, and students never see it.
## Ask an AI assistant instead
With the Novedu skill installed in your AI coding assistant, you can say "read my sorting tutor and script eval conversations for its rules", "run the tutor eval", or "what did the judge flag?", and it drafts the file, runs the commands, and tells you which instruction to sharpen. The introduction chapter on the Novedu CLI and its AI skill shows how to install that skill.
# Publishing your YAML file
> Make your activity file reachable for Novedu, either through a public GitHub URL or by uploading it on the Files page.
You've written an activity in YAML. Before you can hand it to a class, Novedu has to be able to read it. Novedu doesn't store your activity inside a code; it reads the file from a public web address every time students use it. So the last authoring step is giving your file such an address.
There are two ways to do that, and both work equally well:
- **Host it on GitHub** in a public repository and use the file's raw URL.
- **Upload it in the app** on the Files page, and let Novedu host it for you.
Pick whichever fits how you work. GitHub gives you version history and works well if you already keep teaching material there; the Files page needs no account or tooling beyond Novedu itself.
## Option A: host the file on GitHub
Put your YAML file in a **public** GitHub repository, then use the file's raw URL, the address that returns the plain file content rather than the GitHub page around it.
1. Commit the file and **push** it to GitHub.
2. Open the file on the GitHub website and select **Raw**.
3. Copy the address from your browser. It looks like `https://raw.githubusercontent.com///refs/heads/main/.yaml`.
4. Paste that URL into the form when you create a code for the activity.
One thing to get right: Novedu reads the **pushed** version, not the copy on your computer. If you edit the file locally and forget to commit and push, students keep seeing the old version. Push first, then test.
## Option B: upload the file in the app
The Files page in Novedu lets you create and edit activity files without any hosting of your own. The app stores the file and serves it at a public address.
1. Open the Files page and select **New file**.
2. Give the file a name (letters, digits, hyphens, and underscores only, no spaces) and pick its kind: tutor, fragment, quiz, writing, or coding.
3. Write the YAML in the editor, or select **Upload file…** to load a file from your computer into it.
4. Select **Validate** to check the YAML without saving, as often as you like.
5. Select **Validate & create** to save.
Saving always validates first: an invalid file is never stored, so anything the Files page has published is a file Novedu can actually run. After saving, the edit page shows the file's **Public URL** with a copy button; that address is what you use when you create a code. The file list also offers a **Create code** shortcut next to each activity file, which starts the code form with the file already filled in.
## Finding a file in the list
The Files page holds every teacher's files, and it opens on your own: the **Owner** box starts on your name. Pick **All owners** to see everyone's files, pick a colleague to see only theirs, or select **Clear** to come back to your own. You can also filter by name, title, and description, and sort by any column header.
A file's owner is whoever **saved it last**, not whoever created it. So if you edit a colleague's file, it becomes yours: it moves out of their default view and into yours. Nothing is lost, and everyone can still find it under **All owners** or by name, but it is worth knowing before you wonder where a file went.
If you prefer the command line, the Novedu CLI can upload a file too, with `novedu-cli files upload --file --kind `, and it runs the same validation on the server.
## Editing the file later
A published file is not frozen. Novedu reads the current published version each time students use the activity, so your changes reach the class without touching any existing codes.
- **GitHub**: edit, commit, and push. The pushed version is live immediately.
- **Files page**: open the file, edit, and select **Validate & save**. The saved version is live immediately, and the address stays the same.
Deleting an uploaded file makes its address stop working, so any code that points at it stops working too.
## Files that reference other files
A tutor can pull in a shared fragment library by URL, and that URL may be relative. A relative reference resolves against the activity's own address, wherever it is published. The sorting-algorithms sample tutor does exactly that:
```yaml
prompt:
fragment_files:
- id: general_fragments
url: "../shared/general-fragments.yaml"
```
On GitHub this means the shared library has to sit in the repository next to the tutor, in the place the relative path expects. For uploaded files, `./other-file` points at another uploaded file called `other-file`. If the pieces live in different places, use a full `https://` URL instead of a relative one.
# Choosing an AI model for your activity
> Set the model, the provider, and the thinking effort in an activity's llm block, and override the whole block per code without editing the YAML.
Every activity tells Novedu which AI model should run it. You set that in the `llm:` block of the activity's YAML file. The block has three fields: a required `model`, an optional `provider`, and an optional `reasoning` level.
```yaml
llm:
model: RedHatAI/gemma-4-31B-it-FP8-Dynamic
```
That's the whole block in most activities, taken from the sorting-algorithms sample tutor. The same block works in tutors, quizzes, writing activities, and coding activities.
## The three providers
The provider decides where the AI runs. There are three choices:
- **SCCH**, the school's Austrian LLM hosting partner: the default. If you leave out `provider`, your activity runs there. On SCCH, `model` is a raw model id, like the one in the sample above.
- **Azure Foundry**: runs the activity on an Azure deployment your school has set up. With this provider, `model` names the Azure deployment, not a raw model id.
- **OpenRouter**: a gateway that reaches models from many different vendors through a single account, so one provider opens a broad catalogue. With this provider, `model` is OpenRouter's own id for the model, always a vendor name and a model name with a slash between them, for example `z-ai/glm-5.3-flash`, `openai/gpt-5-mini`, or `anthropic/claude-sonnet-4.5`.
An activity that runs on Azure looks like this:
```yaml
llm:
model: gpt-5.4-mini
provider: Azure Foundry
```
And one that runs through OpenRouter looks like this:
```yaml
llm:
model: z-ai/glm-5.3-flash
provider: OpenRouter
```
Write the provider name exactly as it appears here, capital letters included. `SCCH`, `Azure Foundry`, and `OpenRouter` are the three names Novedu accepts; anything else is rejected when you save.
Azure Foundry and OpenRouter are both optional, and each school decides whether to set them up. If yours hasn't, all activities simply run at SCCH, the school's Austrian LLM hosting partner, and Novedu tells you in plain words that the provider isn't configured on this server when you try to save an activity or a code that asks for it.
## What each provider costs
**Azure Foundry and OpenRouter should be used with care, because they are billed per use**: what runs on Azure Foundry is charged to your school's Azure account, and what runs through OpenRouter is charged to your school's OpenRouter account. SCCH, the school's Austrian LLM hosting partner, is covered by a partnership agreement and costs nothing per use.
That is why SCCH is the default, and why it's the right home for everyday classroom work: a class of 30 students chatting there for a full lesson doesn't add anything to a bill.
On a paid provider, every student message, every answer the model writes, and every bit of thinking it does on the way there is charged. A single busy lesson can cost real money, and a model set to think hard can cost several times what the same lesson costs at a lower effort.
So treat the paid providers as the exception rather than the habit:
- Run everyday activities at the school's hosting partner.
- Reach for Azure Foundry or OpenRouter when an activity genuinely needs it, for example a subject where the partner's models keep getting things wrong.
- On a paid provider, set the thinking effort deliberately and start low. Watch what an activity actually uses on the code's usage page before you hand it to a big class.
## How hard the model thinks
Some models can spend extra effort working an answer out before they write it. The optional `reasoning` field says how much of that effort you want:
```yaml
llm:
model: gpt-5.6-terra
provider: Azure Foundry
reasoning: low
```
The levels, in rising order of effort, are `none`, `minimal`, `low`, `medium`, `high`, and `xhigh`. They mean the same thing on all three providers. More effort means the model writes more thinking before its answer, which makes students wait longer, counts towards the activity's usage, and costs more on a paid provider. A low level is a good starting point for a classroom activity, and a higher one is worth trying when the AI keeps getting a tricky subject wrong.
When a model writes its thinking out, you see that thinking appear in the chat while the answer is being prepared. Students never see it, on any provider, and it isn't kept in the conversation afterwards: it is a live view for teachers only.
Leave `reasoning` out and the model decides for itself. There is no level Novedu fills in behind your back: the setting is simply not sent, and the model uses its own default.
`none` is the one level that isn't just "think less". It switches thinking off completely, so the model answers straight away. Leaving the field out and setting `none` are two different things: left out, a thinking model keeps thinking at its own default; set to `none`, it stops. If a model feels slow for a simple task, `none` is what makes it quick.
## The same level means different things to different models
What a reasoning level actually does depends on the model you picked, and there are three behaviours you'll meet.
**Some models have a real range.** Qwen 3.8, one of the models at the school's Austrian LLM hosting partner SCCH, thinks steadily longer as you go from `low` to `medium` to `xhigh`, and at `xhigh` it writes roughly two and a half times the thinking it writes at `low`. The gpt-5.x deployments on Azure work the same way. On these models the level is a genuine dial, and on a paid provider it's also a cost dial.
**Some models only have an on/off switch.** Gemma 4 at the school's hosting partner accepts every level, but only `none` changes anything. Asking it for `high` instead of `low` gives you the identical answer, so the useful choice there is thinking on or thinking off, nothing in between.
**Some models refuse a level outright.** Qwen 3.8 accepts only `none`, `low`, `medium`, and `xhigh`, and answers with an error if you ask for `minimal` or `high`. On Azure it varies per deployment: one gpt deployment refuses `minimal` while another is happy with it. Through OpenRouter it varies per model, because each vendor in the catalogue sets its own rules. A refused level doesn't show up when you save the file. It fails when a student starts working, in the same way a wrong model name does.
You can't tell from the app which of the three groups a model belongs to, so try the activity once yourself after you set a level. Send a question that needs real thought, check that an answer comes back at all, and see whether a higher level makes the answers better before you leave it there.
At the school's hosting partner some models come as two separate entries instead, one with reasoning on and one with it off. There you pick the behaviour by choosing the model name, and you can leave `reasoning` out entirely.
## Which model names can I use?
There is no fixed list to print here: the available models are set up by your school and change over time. Two reliable places to look:
- **The sample activities** your school shares (for example the ones under `activities/examples/` in the Novedu repository). They always name a model that works.
- **The preset buttons on the create-code form.** When you create a code, the form offers one-click presets that fill in a known-good provider and model, and a level for the presets that name a reasoning model. There is a preset per provider, including **OpenRouter · GLM 5.3 Flash**, which is a quick way to see the shape of an OpenRouter model id.
For OpenRouter there is a third place: OpenRouter publishes its whole catalogue on its own website, and the id shown there is exactly what goes into `model`.
The `model` field is free text, so a typo isn't caught when you save the file. A wrong name only fails when a student starts chatting, so copy a model name from a working sample rather than typing it from memory.
If you validate your YAML with the Novedu CLI, keep the CLI current: an older copy doesn't know the newer provider names and rejects a file that uses one. Running it as `npx @novedu/cli@latest` fetches the current version instead of an old one from your computer's cache.
## Override the model per code, without editing the YAML
The `llm:` block is only the activity's default. When you create or edit a code (the short link you hand to a class), the form lets you override it for that one code. The YAML file stays untouched, so the same activity can run once at the school's hosting partner and once on Azure, just by creating two codes.
A few things to know about the override:
- **Provider and model are both or nothing.** You set the two together, or neither. A half-filled pair is rejected when you save.
- **The reasoning level rides on top of them.** You can add a level to the pair, but you cannot set a level on its own; without a provider and a model, there is nothing for it to apply to.
- **The override replaces the whole block.** It doesn't merge with the activity file. If the file sets `reasoning: high` and your override leaves the level on "Provider default", the code runs without any level at all. Repeat the level in the override whenever you want to keep it.
- **Presets fill it in one click.** The form offers buttons for common combinations, and each button fills the whole override: a preset for a reasoning model also sets its level, a preset for a plain model clears the level again. **Clear** removes the override and returns the code to the activity's own `llm:` settings.
- **You can change it later.** Unlike the activity file, the override isn't frozen; edit the code any time to switch models or effort.
- **Only the AI settings change.** Everything else, such as the instructions and the anonymity setting, still comes from the YAML file. If your tutor lets students upload images, pick an override model that can read them.
# Building a tutor
> The fields that make a tutor YAML file, what students see on the empty chat, and when reusable fragments are worth the effort.
A tutor is one YAML file. At its core it needs three things: an id, an AI model, and your instructions, the free-text guidance that makes the tutor behave the way you want. Everything else is optional polish. This chapter walks through the fields using the sorting-algorithms sample tutor, a real, validated file you can copy from.
## The required fields
Five fields are required: `id`, `name`, `description`, an `llm` block with a `model`, and `prompt.tutor_instructions`. Here they are with the values from the sample tutor:
```yaml
id: ts-sorting-algorithms
name: "Tutor für Sortieralgorithmen (Bubble & Selection Sort)"
description: |
Ich helfe dir, Bubble Sort und Selection Sort in TypeScript zu verstehen ...
llm:
model: RedHatAI/gemma-4-31B-it-FP8-Dynamic
prompt:
tutor_instructions: |
You are a tutor for 16-year-old students at a vocational college. ...
```
- **`id`** is a short machine name, such as `fractions-de`. Students never see it.
- **`name`** is the human-readable title of the tutor.
- **`description`** appears to students on the empty chat, below the greeting. Write it for them: say what the tutor helps with, in the language your class speaks.
- **`llm.model`** names the AI model. An optional `provider` chooses where it runs, and an optional `reasoning` level says how hard the model thinks before it answers. When you create a code for the tutor you can override all three without touching the file. The chapter on choosing an AI model covers the details.
- **`tutor_instructions`** is where your own guidance goes: free text that tells the tutor how to behave.
## Your instructions
The `tutor_instructions` field is free text in your own words: who the tutor is, what it teaches, how it should respond, and what it must not do. It's the tutor's whole prompt. When you reuse fragments, you place them with markers inside this same text, so your own wording and any shared pieces read in the order you arrange them.
For a one-off tutor, it's fine to put the whole prompt here and skip fragments entirely. The sample tutor uses its instructions for the class-specific parts: what the students already know (basic TypeScript, no classes or arrow functions yet), the learning goals of the unit, and didactic hints such as preferring tiny concrete arrays over abstract talk.
## What students notice on the empty chat
Two optional fields shape the screen students see before their first message:
- **`title`** replaces the default "How can I help you today?" greeting. Leave it out to keep the default.
- **`exampleQuestions`** adds clickable starter questions below the description. Each entry has a short `title` (the clickable label) and the full `question` text. Clicking a label puts the question into the chat input, and students can still edit it before sending.
```yaml
exampleQuestions:
- title: "Wie funktioniert Bubble Sort?"
question: "Kannst du mir Schritt für Schritt erklären, wie Bubble Sort ein Array sortiert?"
- title: "Bubble vs. Selection Sort"
question: "Was ist der Unterschied zwischen Bubble Sort und Selection Sort? Wann ist welcher besser?"
```
You can define any number of starter questions, but students see at most five. With more than five, a random selection of five appears on each page load, kept in the order you wrote them, so order them deliberately, for example from easy to hard.
## Built-in tools
A tutor can give its AI model access to built-in tools: small helpers that run on the Novedu server while the tutor chats. The model decides when to call a tool, the server runs it, and the model uses the result in its answer. Students only see the finished reply.
You opt in with a top-level `tools:` list. Without it, a tutor has no tools.
```yaml
tools:
- random_number
```
The complete list of available tools:
| Tool | What it does |
| --------------- | --------------------------------------------------------------------------------------------------------- |
| `random_number` | Returns a random whole number between `min` and `max` (both included), with true server-side randomness. |
Why `random_number` matters: AI models are surprisingly bad at being random. Ask a model to "pick a random number" and it tends to produce the same few values in every session, so your students end up practising with the same exercises. The tool draws a genuinely random number instead, which gives every student varied practice problems.
Two rules to get right:
- **Validation checks the names.** A tool name that Novedu doesn't offer fails validation, so a typo can't slip through quietly.
- **Tell the tutor to use its tools.** Novedu never mentions the tools in the prompt for you. Say it in your `tutor_instructions`, for example: "When the student asks for a practice problem, use the random_number tool to pick the values. Don't invent the numbers yourself."
## Fragments: reusable pieces of prompt
A fragment is a named piece of prompt (a teaching style, a topic list, a safety policy) that lives in a separate fragment library file and can be pulled into many tutors. Write a rule once, reuse it everywhere, and fix it in one place when it needs a change.
Using fragments takes two steps inside `prompt`:
- **`fragment_files`** declares the libraries and gives each a short alias. The `url` is either a full `https://` link or a relative path, which is resolved next to your tutor file's own published location.
- **A marker** in `tutor_instructions` places each fragment where you want it: `{{fragment "alias.id" name="value"}}`. The part in quotes is the library alias and the fragment's id, split at the first dot. A fragment declares which values it needs; required ones must be supplied with the right type, and validation tells you exactly what's missing. An optional value can carry a default set by the library's author: leave the argument out to accept it, or supply your own to override it.
A fragment lands exactly where its marker sits, so your own wording and the shared pieces read in the order you arrange them in `tutor_instructions`. There is no priority number and no separate list to keep in sync.
Two things to know before you rely on a library:
- A fragment library is hosted like the tutor file itself: at a public web address, or as a file hosted in the app. Publishing works the same way for both.
- Validating a tutor also validates every fragment in every library it references, even fragments the tutor doesn't use. A broken template anywhere in the library fails the whole check, which is good news: a shared library that validates once is safe for everyone who uses it.
**When are fragments worth it?** When several activities should share the same wording: a school-wide safety policy, a Socratic teaching style, a language rule. Fragments aren't limited to tutors, either: the same library also works in quizzes, writing activities, and coding activities, and the chapter on reusable fragments shows how to write a library of your own. For a single tutor with instructions nobody else will reuse, plain `tutor_instructions` is simpler and just as good.
## The sample tutor, walked through
The sorting-algorithms tutor (`activities/examples/sorting-algorithms/sorting-tutor.yaml`) declares one shared library and places four fragments with markers at the top of its instructions, then adds its own text:
```yaml
prompt:
fragment_files:
- id: general_fragments
url: "../shared/general-fragments.yaml"
tutor_instructions: |
{{fragment "general_fragments.socratic_tutor"}}
{{fragment "general_fragments.topic_limits" allowed_topics=(array
"Bubble Sort: idea, passes, comparisons, swaps"
"Selection Sort: finding the minimum and swapping it to the front")}}
{{fragment "general_fragments.language_policy" natural_language="German" code_language="English (TypeScript and p5.js terms)"}}
{{fragment "general_fragments.teenager_safety"}}
You are a tutor for 16-year-old students at a vocational college ...
```
Reading it top to bottom:
1. **One library, one alias.** The library sits in a sibling folder, so a relative `url` is enough; `general_fragments` is the alias every marker below refers to.
2. **`socratic_tutor`** needs no values: it's a fixed teaching style (hints and questions instead of ready-made solutions) placed with a bare marker.
3. **`topic_limits`** takes a list of allowed topics, written with `(array …)`, so the same fragment keeps a maths tutor on maths and this one on sorting. The full sample lists seven topics; the excerpt above shows two.
4. **`language_policy`** takes two text values: the tutor speaks German with the students but keeps code and technical terms in English.
5. **`teenager_safety`** is the shared safety net for teenage students, placed with a bare marker.
Below the markers, in the same `tutor_instructions` text, the tutor adds everything specific to this class: the students' prior knowledge, the learning goals, and how to guide them through visualising the algorithms. That split is the pattern to copy: shared behaviour in fragments, your class in the instructions.
# Building a quiz
> Write a quiz file with open-ended questions and private grading guides, and set shuffling, photo answers, and the follow-up discussion.
A quiz is an activity made of **open-ended questions**. There is deliberately no multiple choice: students answer in their own words, and the AI grades each answer against a grading guide you write. The student immediately sees a verdict (**correct**, **partial**, or **incorrect**) and written feedback, and can then open a short discussion chat about that question.
You define a quiz in one YAML file. The core is simple: an `id`, a model, and a list of questions.
## One question, two texts
Every question in a quiz file carries two pieces of text with very different audiences:
- **`question`**: the Markdown the student sees. Maths (`$…$`) and code fences render.
- **`evaluation`**: the grading guide. Only the AI sees it; it never reaches the student's browser. Because it stays private, it can openly state the expected answer and the criteria for each verdict.
## The smallest quiz that works
This complete example comes from the quiz authoring guide:
```yaml
id: capitals-basics
name: "Capital Cities — Basics"
title: "Capitals Quiz"
description: "A short quiz on capital cities. Answer in your own words."
anonymous: false # record which student each attempt belongs to
shuffle: true # random question order per attempt
llm:
model: RedHatAI/gemma-4-31B-it-FP8-Dynamic
questions:
- id: capital-australia
title: "Capital of Australia"
question: |
What is the **capital city of Australia**?
evaluation: |
The correct answer is **Canberra**.
Grade as:
- `correct` — names Canberra (any reasonable spelling).
- `partial` — mentions Canberra among other guesses, or describes it.
- `incorrect` — names Sydney, Melbourne, or any other city.
```
Field by field:
- **`id`** (required): a short machine name for the quiz.
- **`name`** (optional): a human-readable label.
- **`title`** and **`description`** (optional): what students see on the welcome screen before the first question. Write the `description` for your students.
- **`anonymous`** (optional, default `true`): by default a quiz is anonymous, so answers feed the statistics but aren't linked to a student. Set `anonymous: false` to attribute each attempt to the signed-in student. The setting is frozen onto a code when you create one; editing the file later doesn't change a live code.
- **`shuffle`** (optional, default `true`): questions appear in a random order per attempt. Set `shuffle: false` to keep your authored order.
- **`immediate_feedback`** (optional, default `true`): while a student types an answer, a small label beside the answer box shows where that answer currently stands. Set `immediate_feedback: false` to switch the hint off for this quiz; see "The live hint while typing" below.
- **`llm.model`** (required): the model that grades the answers and drives the discussion chat. You can also set an optional `llm.provider` (the provider decides where the AI runs) and an optional `llm.reasoning` level (how hard the model thinks before it grades); the create-code form can override all three per code.
- **`question_count`** (optional): how many questions one attempt asks. Leave it out to ask every question exactly once; see "How many questions one attempt asks" below.
- **`questions`** (required, unless the quiz pulls its questions from other quiz files with `quiz_files`): each question needs an `id` (unique within the quiz), a `question`, and an `evaluation`; an optional `title` labels it in the statistics and progress display.
## Writing grading guidance that works
The `evaluation` is the heart of a question. The AI maps the student's answer onto one of three verdicts, so name all three explicitly: describe what counts as `correct`, what still earns `partial`, and what is `incorrect`. Vague guidance produces vague grading.
A pattern that works well:
1. State the expected answer first, including acceptable variants.
2. List the three verdicts with concrete criteria for each.
3. Tell the AI how to write the feedback: which language to use, and whether to show a worked example or a gentle correction.
Since students never see the grading guide, you don't need to hide anything in it. Spell out the full solution, common wrong answers, and how generous to be with spelling or phrasing.
## Question order
Questions are shuffled by default: each student gets a random order per attempt. Set `shuffle: false` at the top of the file when later questions build on earlier ones, as the sorting-algorithms sample quiz does (it moves from concept to code step by step).
## Skipping a question
Every quiz lets students skip a question. Before submitting an answer, a student can select **Skip for now**, and the quiz moves on to the next question.
A skipped question comes back later in the same attempt:
- It moves to the end of the line and returns after every question the student hasn't seen yet. Several skipped questions come back in the order they were skipped.
- A student can skip a returning question again, as long as another question is still waiting. The last remaining question can't be skipped.
- Whatever the student had already typed or attached as a photo is still there when the question returns.
- The progress display doesn't move forward on a skip. It shows how many questions are waiting for later, and marks a returning question as skipped earlier.
Skipping also applies with `shuffle: false`, so a student can move a question behind later ones even when you keep your authored order. If later questions really depend on an earlier answer, say so in the question text.
A skipped question counts as unanswered until the student answers it. If a student finishes early, the summary names how many skipped questions were never answered. Skips aren't stored anywhere and don't show up in your statistics.
## How many questions one attempt asks
By default one attempt walks through every question exactly once. Set a top-level `question_count` to change that:
```yaml
question_count: 30
```
The number combines with `shuffle` in a predictable way:
- **Fewer than the quiz has**: with `shuffle: true` each attempt asks a random selection of that size, so two students (or two attempts) get different questions. With `shuffle: false` every attempt asks the first `question_count` questions in your authored order.
- **More than the quiz has**: questions repeat, which turns the quiz into a practice drill. The whole pool is covered before anything repeats, and with `shuffle: true` the same question never appears twice in a row.
Students see the chosen length in the progress display ("Question 3 of 30"). Two things to keep in mind:
- `question_count` shapes one attempt in the student's browser; it is not an exam lock. Reloading the page starts a fresh attempt, and answers are not stored.
- Skipping never changes the length of an attempt: a skipped question returns later instead of being replaced by a new one.
- A repeated question is simply graded again, independently of the earlier answer.
## The live hint while typing
While a student types an answer, a small coloured label with an icon appears beside the **Your answer** label and shows where that answer currently stands:
- Green, with a tick: **Looks correct**.
- Amber, with a minus: **Partly there**.
- Red, with a cross: **Not yet**.
- Grey, with a question mark: **Unsure**, when the check cannot tell.
The hint appears once the student pauses typing for a moment, and it fades while they carry on typing, because it then describes text that has already moved on. Pointing at it shows the same wording as a tooltip, including the sentence that this is a live hint, not the final verdict.
The live hint is a quick check, never the grade. Nothing about it is stored, it never shows written feedback, and it does not change what a student gets on **Submit**: the verdict and the feedback still come from your `evaluation` guidance when the answer is submitted. Once a student submits, the hint disappears and the verdict card takes over. The hint reads only what the student has typed, so photos attached to an answer are not part of it.
The live hint appears only when your school's Novedu server has the feature switched on. If it is switched off there, students never see the hint and `immediate_feedback` changes nothing.
Set `immediate_feedback: false` to switch the hint off for one quiz:
```yaml
immediate_feedback: false
```
That is worth doing for an exam or a test, where students should commit to an answer instead of tuning it until the hint turns green.
## Photo answers
Students can attach photos of their work, for example a handwritten calculation, when you turn photo answers on:
```yaml
llm:
model: RedHatAI/gemma-4-31B-it-FP8-Dynamic
imageInput: true
```
Photo answers are **off by default**. A few things to know:
- The model must be **vision-capable** (able to look at images). That also applies to any per-code model override on such a quiz.
- Students can attach up to three photos per answer. An answer may even be photo-only, with no typed text.
- Novedu prepares each photo in the student's browser before the model sees it. Photos that a phone stored sideways are straightened, and very large photos are made smaller. Students can simply use their phone camera: an ordinary phone photo is fine, even though the original file is far bigger than what gets sent.
- Some phone photo formats, HEIC above all, cannot be opened by every browser. A student whose photo cannot be read gets a message naming the format and what to change on the phone, together with details they can copy and send to you.
- If a photo still does not work, ask the student to open the photo check page at `/image-check` on the device that took it, and to send you the text shown there. The photo is examined on their own device and is never uploaded.
- The quiz-level flag is the default for all questions; an `imageInput` on a single question overrides it in either direction.
## The follow-up discussion
After seeing their feedback, a student can open a short discussion chat about that question. Novedu already gives the assistant full context (the question, the expected answer, the student's answer, and the verdict), so the optional `discussion.instructions` field is only extra steering: tone, language, and didactic style. Omit it to use a sensible default.
When the quiz declares fragment libraries (see "Reusing fragments in a quiz" below), you can place `{{fragment …}}` and `{{file …}}` markers inside `discussion.instructions` too, exactly as in the top-level `instructions` field.
## Reusing fragments in a quiz
A quiz can place shared prompt fragments, the same reusable pieces tutors use (a persona, a safety policy, a language rule). Declare the library under a top-level `fragment_files:`, then place each fragment with a marker in a top-level `instructions:` field:
```yaml
fragment_files:
- id: general_fragments
url: "../shared/general-fragments.yaml"
instructions: |
{{fragment "general_fragments.teenager_safety"}}
```
A quiz's `instructions` field reaches further than the instructions of other activities: it applies **both** to how answers are graded **and** to the follow-up discussion chat, so a shared safety or persona rule shapes grading and conversation alike. It is a separate field from `discussion.instructions`, which only steers the discussion and takes the same markers. Your per-question `evaluation` texts stay plain, markers don't work there. The chapter on reusable fragments covers writing a library and supplying values.
## One final quiz over several chapters
When a course is split into chapters, each with its own quiz, you can build an overall quiz at the end that asks the questions of all chapters, without copying a single question. Declare the chapter quizzes under a top-level `quiz_files:`, each with a short alias and the file's address:
```yaml
id: ddp-final
name: "Final quiz: all chapters"
llm:
model: RedHatAI/gemma-4-31B-it-FP8-Dynamic
question_count: 30
quiz_files:
- id: intro
url: ./0010-introduction-quiz.yaml
- id: loops
url: ./0020-loops-quiz.yaml
```
**All** questions of every referenced quiz are included, and they are read **live**: when you edit a chapter quiz, the final quiz asks the updated questions the next time a student opens it. There is nothing to keep in sync. A final quiz built this way needs no `questions` of its own, though it may add some. Combining `quiz_files` with `question_count`, as in the example, keeps a large final quiz to a sensible length per attempt.
How the pieces fit together:
- **The final quiz's own settings rule.** The model, the anonymity setting, `shuffle`, `question_count`, and `discussion.instructions` all come from the final quiz's file, and the same settings inside a chapter quiz are ignored here. A chapter's own `discussion.instructions` never applies in the final quiz.
- **Grading instructions add up.** The chapter quiz's top-level `instructions:` text is the one thing that travels with its questions, and it applies to grading only. An imported question is graded with the final quiz's `instructions` first and the chapter's on top, so both are in force at once. That is worth keeping in mind while you write them: a language, persona, or safety rule in the final quiz's `instructions` also governs every imported question, so avoid putting a rule there that contradicts a chapter's.
- **The follow-up discussion follows the final quiz alone.** No chapter text reaches the discussion chat, so put any guidance the discussions need into the final quiz's own `instructions` and `discussion.instructions`.
- **Aliases name the source.** Pick a short alias per file (no dot, no slash, each one unique). In the statistics an imported question shows up as `alias/question-id`, for example `intro/capital-australia`, so you can tell the chapters apart.
- **Addresses work like elsewhere.** The `url` is a web address or a relative path, resolved next to the final quiz's own file. That also works between files hosted in the app: host the chapter quizzes and the final quiz together and refer to them with `./file-name` style paths.
- **One level only.** A referenced quiz must not declare `quiz_files` itself; a quiz of quizzes of quizzes is not supported.
If a referenced file is missing or broken, students see a friendly error instead of a shortened quiz: the final quiz never silently loses a chapter. Validation catches this before sharing; see "Before you share a quiz" below.
## A real example: the sorting-algorithms quiz
The sample file `activities/examples/sorting-algorithms/sorting-quiz.yaml` is a seven-question quiz for a TypeScript class, and it shows most of the options above in action. Its header keeps the authored order and steers the discussion chat:
```yaml
id: sorting-algorithms-quiz
name: "Quiz: Bubble Sort & Selection Sort"
# The questions build up from concept to code, so keep the authored order.
shuffle: false
llm:
model: RedHatAI/gemma-4-31B-it-FP8-Dynamic
discussion:
instructions: |
You are a friendly programming tutor helping the student understand THIS
quiz question about sorting algorithms. The student has already submitted
an answer, so you may reveal and explain the correct answer. ...
Respond in German, keep code and identifiers in English, and stay on the
topic of this question.
```
One question turns photo answers on just for itself, because a handwritten trace on paper is the natural way to answer it:
```yaml
- id: bubble-trace
title: "Bubble Sort von Hand"
imageInput: true
question: |
Gegeben ist das Array `[5, 2, 4, 1]`.
Wie sieht das Array nach dem **ersten vollständigen Durchlauf** von
Bubble Sort aus ...
```
Its grading guide states the full solution and even anticipates a typical wrong answer, grading it `partial` rather than `incorrect`:
```yaml
evaluation: |
Die korrekte Antwort ist **`[2, 4, 1, 5]`** ...
Bewerte als:
- `correct` — das Ergebnis `[2, 4, 1, 5]` (mit oder ohne
Zwischenschritte).
- `partial` — ... ODER die Antwort ist das fertig sortierte Array
`[1, 2, 4, 5]` (zu weit gedacht: das ist das Ergebnis
ALLER Durchläufe, nicht des ersten).
- `incorrect` — ein anderes Ergebnis ohne erkennbar richtiges Vorgehen.
Gib das Feedback auf Deutsch und zeige die drei Vergleichsschritte.
```
Notice the mix of languages: the questions and feedback instructions are in German for the students, while field names and code stay in English. Write your quiz in whatever language your class works in; the grading works the same way.
## Before you share a quiz
Validate the file before you hand out a link: an invalid quiz cannot be saved in the app or turned into a code. The validator checks that the YAML parses, that the structure is right (an `llm.model` and at least one question, each with an `id`, a `question`, and an `evaluation`), and that every question `id` is unique. For a quiz with `quiz_files`, it also fetches and fully checks every referenced quiz file, so a broken chapter quiz blocks the final quiz from being saved. From the terminal:
```bash
novedu-cli validate ./quizzes/my-quiz.yaml --kind quiz
```
A valid quiz can still grade differently from how you meant it. You can test the grading itself before students meet it: write a few sample answers with the marks they should get, and the CLI's `eval` command grades them with the real grader and reports where it disagreed with you. The chapter on testing how a quiz grades walks through it.
# Building a writing activity
> Write the YAML for a writing activity, describe the task, shape a coach that advises without rewriting, and add an optional starter scaffold.
A writing activity gives each student a split screen: an editor on the left where they write, and an AI writing coach on the right that gives feedback on their draft. The coach can read the draft at any time, but it has no way to change it. It only advises; the student writes every sentence. When students are happy with their text, they press **Save**, and you can review one saved text per student later.
You describe the task and how the coach should behave in a YAML file. The editor, the coach's read access to the draft, and the Save button are all built in.
## The smallest working file
Three things are required: an `id`, an AI model, and the coach's instructions.
```yaml
id: my-essay
llm:
model: RedHatAI/gemma-4-31B-it-FP8-Dynamic
instructions: |
You are an encouraging writing coach. Help the student improve THEIR essay,
never write it for them. Read the current draft with the `getCurrentText` tool
before giving feedback. Point at what works and what to improve, and end with
a concrete next step.
```
- `id` is a short machine name for the activity, such as `my-essay`.
- `llm.model` picks the AI model that drives the feedback chat, with an optional `llm.provider` for where it runs and an optional `llm.reasoning` level for how hard it thinks. It works the same as in tutors and quizzes, and a code can override the whole block later without touching the file.
- `instructions` is the coach's prompt: how it should behave, what to look for, and how to talk to the student. Students never see this text, so you can spell out your assessment criteria freely.
## The task students see
Two optional fields set the assignment on the welcome screen, before the student starts writing:
- `title` replaces the default greeting. Leave it out to keep the default.
- `description` appears below the greeting and supports Markdown. This is where you state the actual writing task: the situation, the text type, the length, and what the text must contain. Write it for your students.
A third optional field, `placeholder`, is starter text prefilled into the editor. Leave it empty (`""`) for a blank page, or give the text a scaffold: headings, a formal opening line, or bracketed hints for each paragraph. A scaffold shows students the expected shape while they still write every sentence themselves.
## Shaping the coach
The coach cannot edit the student's text. Its only access to the draft is a read-only tool called `getCurrentText`, so even instructions that asked it to rewrite would have no effect. Write the instructions to fit a read-only helper:
- **Tell it to read before it comments.** Ask it to call `getCurrentText` before giving feedback, and again whenever the student says they changed something, so it never guesses at the draft.
- **Advise, don't rewrite.** Tell it to point at what works and what doesn't, explain why, and give a direction or a guiding question rather than finished sentences. Short model phrases ("I would suggest ...") are fine; whole sentences about the student's own topic are not.
- **Set priorities.** List what to give feedback on, in order, so the coach raises the one or two most important improvements instead of everything at once. Putting grammar last keeps the focus on the writing task.
- **Set the tone.** Say who the student is (age, language level) and how to talk to them, for example in simple English with difficult words explained.
- **Cover the empty page.** Tell the coach what to do when the draft is empty or very short: help the student start with questions instead of critiquing.
## Reusing fragments in a writing activity
A writing activity can place shared prompt fragments, the same reusable pieces tutors use (a persona, a safety policy, a language rule). Declare the library under a top-level `fragment_files:`, then place each fragment with a marker in your `instructions`:
```yaml
fragment_files:
- id: general_fragments
url: "../shared/general-fragments.yaml"
instructions: |
{{fragment "general_fragments.teenager_safety"}}
You are an encouraging writing coach ...
```
A fragment lands where its marker sits, so a school-wide rule can frame the coach at the top of your `instructions` without you repeating it in every activity. The chapter on reusable fragments covers writing a library and supplying values.
## Recording who wrote what
Writing activities record the author by default, because reviewing saved texts only makes sense when you know whose text it is. This is different from tutors and quizzes, which default to anonymous.
You can set `anonymous: true` for ephemeral, unattributed writing, but then saving is turned off: there is nothing to keep or review. The anonymous setting is frozen onto the code when you create it.
## A real example: the restaurant review letter
The sample activity `activities/examples/review-writing/restaurant-review-letter.yaml` is a complete writing activity for an English class: a formal feedback letter (150 to 250 words) to a restaurant manager after a birthday party there. It shows all the pieces working together.
**The description sets the scene and the requirements.** It gives the student a concrete situation, what went well and what went wrong, and a checklist for the letter:
```yaml
title: "Write a Feedback Letter to the Restaurant"
description: |
Last Saturday you celebrated your **birthday party** with eight friends at
the restaurant *Bella Vista*. Some things were great: the pizza was
delicious and the staff sang for you. Some things were not: you had booked
a table for 7 p.m. but waited 30 minutes, and the drinks were expensive
and arrived slowly.
Write a **feedback letter (150–250 words)** to the restaurant's manager.
You don't know the manager's name. Your letter should:
- open and close like a **formal letter**,
- say **why** you are writing,
- mention what you **liked** and what **disappointed** you — politely,
- end with a **suggestion** for improvement.
```
**The placeholder is a formal-letter scaffold.** The student sees the expected shape but writes every sentence:
```yaml
placeholder: |
Dear Sir or Madam,
(Why are you writing? When were you at the restaurant, and what was the occasion?)
(What did you like? Be specific.)
(What disappointed you? Stay polite.)
(What do you suggest the restaurant should improve?)
Yours faithfully,
(your name)
```
**The instructions shape a read-only coach with clear priorities.** They tell the coach to read the draft with `getCurrentText` before every piece of feedback, never to hand over finished sentences, to quote the student's own words when praising or questioning, and to raise only the one or two improvements that matter most. Then they rank what to look at: task fulfilment first, then structure, then register and tone (the heart of this unit), then concreteness, language variety, and grammar last. They also say what to do with an empty draft: help the student start with questions about the party.
The full file is worth copying as your starting point; swap in your own task, scaffold, and priorities.
# Building a coding activity
> Write a coding activity's YAML with an id, a pinned AI model, and instructions that keep the assistant within what your class has learned.
A coding activity gives your class an AI coding assistant that behaves the way you decide. Students work in their own coding tool on their own machine; your YAML file only tells Novedu which model answers and what rules the assistant follows. It is the smallest activity file of all: an id, a model, and your instructions.
## The fields
A coding file has one required trio and two optional labels:
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/coding/coding-yaml.schema.json
id: sorting-visualizer
name: "Sorting Visualizer — TypeScript + p5.js"
title: "Sortieren sichtbar machen (TypeScript + p5.js)"
llm:
model: RedHatAI/gemma-4-31B-it-FP8-Dynamic
instructions: |
You are a friendly coding buddy for 16-year-old vocational-college students.
...
```
- **`id`** (required): a short machine name for the activity.
- **`name`** (optional): a human-readable label for you.
- **`title`** (optional): the heading students see on the connection page when they open the activity's code link.
- **`llm.model`** (required): the model that answers. You can also add `llm.provider` to run on Azure Foundry or OpenRouter instead of the school's Austrian LLM hosting partner, and `llm.reasoning` to set how hard the model thinks before it answers; the same `llm:` block works here as in every other activity.
- **`instructions`** (required): the assistant's rules, written by you.
The first comment line is an editor hint: with a YAML-aware editor such as VS Code with the Red Hat YAML extension, it turns on validation and autocompletion while you type.
## The model is yours to pin
The `llm.model` you write is final. A student's coding tool always sends some model name of its own, but Novedu ignores it and answers with the model you chose. Students never need to know which model runs, and they cannot switch to another one. A `reasoning` level is pinned in the same way: it replaces whatever thinking effort the student's tool asks for, and if you set none, the tool's own request goes through. When you later create a code for the activity, you can override these settings for that one code without editing the file.
## Instructions shape the assistant
The `instructions` field is the assistant's rulebook, a prompt in plain language. Students never see the text; they only notice its effect in every answer the assistant gives. Your rules also outrank anything the student's coding tool tells the model, so they hold even when the tool has ideas of its own.
The most useful thing to write is what your class may use, so the assistant's help never runs ahead of the lessons:
- **The language and its subset.** For example: only `number`, `string`, `boolean`, and arrays; no classes, no arrow functions; plain loops instead of `map` or `filter`.
- **The topic.** Tell the assistant to help only with the current project and to politely decline unrelated homework.
- **The teaching style.** Small steps, explain why, short runnable fragments, comments that explain the idea.
- **What the student must do alone.** If the algorithm is the learning goal, forbid generating it whole and ask the assistant to guide instead.
## Reusing fragments in a coding activity
A coding activity can place shared prompt fragments, the same reusable pieces tutors use (a persona, a safety policy, a language rule). Declare the library under a top-level `fragment_files:`, then place each fragment with a marker in your `instructions`:
```yaml
fragment_files:
- id: general_fragments
url: "../shared/general-fragments.yaml"
instructions: |
{{fragment "general_fragments.teenager_safety"}}
You are a friendly coding buddy ...
```
A fragment lands where its marker sits, so a school-wide rule can frame the coding assistant at the top of your `instructions` without you repeating it in every activity. The chapter on reusable fragments covers writing a library and supplying values.
## Embedding your sample solution
A coding activity can also embed a plain-text file, and the classic use is the teacher's sample solution. Declare the file under a top-level `text_files:` (a short alias plus a web address, for example the raw GitHub address of your reference implementation), then place it in your `instructions` with a `{{file}}` marker:
````yaml
text_files:
- id: solution
url: https://raw.githubusercontent.com/rstropek/htl-2025-26-2nd/refs/heads/main/40-classes/LinkedListWithTests/src/linkedList.ts
instructions: |
## The sample solution — your private reference, NOT a handout
Below is the teacher's reference implementation. Use it to recognise when a
student is on the right track and to give precise, minimal nudges toward it.
Hard rule: NEVER paste this class — or any complete method body from it — back
to the student.
```typescript
{{file "solution"}}
```
````
The file is inserted exactly as fetched, so your source code arrives unchanged. Because students never see the `instructions`, the assistant knows the exact target shape (method names, signatures, return conventions) and can guide towards it without ever handing it out. The repository ships a complete example of this pattern at `activities/examples/linked-lists/linked-list-buddy.yaml`, a linked-list exercise whose assistant carries the sample solution as its private reference. The chapter on reusable fragments covers the general text-file rules, including embedding just a line range of a file.
## What a coding file does not have
Coding files deliberately leave out fields you may know from tutors, quizzes, and writing activities:
- **No `anonymous` field.** A coding activity's requests carry no student identity: the assistant never knows or stores who asked what. The one thing recorded per student is that they picked up a personal connection key for the code, covered in the chapter on connecting a coding activity.
- **No `placeholder` and no `description`.** There is no in-app chat window to show them in; students work in their own tool.
Do not add any of the three. Validation rejects a coding file that contains them, so a leftover field from a copied tutor is caught before students are affected. You can check a file at any time with the Novedu CLI (`novedu-cli validate my-coding.yaml --kind coding`) or with the **Validate** button on the app's YAML Files page.
## A real example: the sorting visualizer
The repository ships a complete coding activity at `activities/examples/sorting-algorithms/sorting-visualizer.yaml`. The class is building a Bubble Sort and Selection Sort visualiser in TypeScript and p5.js, and the file shows all four instruction ideas in action.
It states what the class already knows, so the assistant assumes nothing more:
```yaml
instructions: |
## What the students already know
- TypeScript basics: functions, loops (`for`, `for...of`, `while`),
conditions (`if` / `else if` / `else`), variables with `let` and `const`,
the basic types `number`, `string`, `boolean`, and arrays of those.
```
It fences in the language, so generated code stays readable for beginners:
```yaml
## TypeScript limits — stay inside them
- Use only `number`, `string`, `boolean`, and simple arrays of those. No
classes, no interfaces, no enums, no generics, no union types, no
destructuring, no `async`/`await`.
- Write plain loops. Do NOT use `map`, `filter`, `reduce`, or `sort` —
writing the sorting loop themselves is the whole point of this project.
```
And it protects the learning goal: the assistant may freely build the scaffolding (canvas, bars, keyboard events) but must never write the sorting algorithm itself. Instead it explains the next step, shows a tiny isolated fragment, and lets the student assemble the loop:
```yaml
## The algorithm is the learning goal — don't write it for the student
- The comparison-and-swap logic of Bubble Sort and Selection Sort is what
the students must write THEMSELVES. Never generate a complete, working
sorting function on request.
```
Copy this file, replace the project and the limits with your own, and you have a working coding activity.
## Students bring their own tool
Unlike a tutor or a quiz, a coding activity has no chat inside Novedu. Students connect an external coding agent (for example little-coder) to the activity: opening the code's link, after signing in, gives each student a personal API key plus ready-to-copy connection settings for their tool. Creating the code and getting a class connected is its own topic, covered in the chapter on connecting a coding activity.
# Reusable fragments
> Write a fragment library once and reuse it across activities, and embed plain-text files such as course material or a sample solution.
A fragment is a named piece of prompt (a teaching style, a safety policy, a language rule) that lives in a fragment library, a YAML file of its own. Write the rule once, place it in as many activities as you like, and fix it in one place when it needs a change. The chapter on building a tutor shows how to *use* fragments; this chapter shows how to *write* a library of your own, and how the same library serves every kind of activity. It also covers the simpler sibling of a fragment library: embedding a plain-text file (course material, a sample solution) straight into your instructions with a text-file marker.
## One library, four kinds of activity
Fragments aren't a tutor feature. A tutor, a quiz, a writing activity, and a coding activity can all draw fragments from the same library, so a school-wide safety policy written once really does cover everything you build. Using fragments takes two steps in the activity file:
1. **List the libraries** you want to draw from under `fragment_files:`, each with a short `id` (an alias) and the library's `url`.
2. **Place each fragment** by writing a marker directly in the activity's own instructions, wherever you want that piece to appear.
Where `fragment_files:` goes depends on the kind. A tutor declares it inside its `prompt:` section. A quiz, writing, or coding activity declares it at the top level of the file, next to `id` and `name`.
The instructions text that holds the markers also depends on the kind: a tutor's `tutor_instructions`, a writing or coding activity's `instructions`, and for a quiz two fields, its top-level `instructions` and its `discussion.instructions`. A quiz's `instructions` field is special: that text applies both to how answers are graded and to the follow-up discussion chat, so a persona or safety rule you place there shapes grading and conversation alike. `discussion.instructions` steers only the discussion chat and takes the same markers; the per-question `evaluation` texts stay plain.
## Placing a fragment with a marker
You place a fragment by writing a marker in the instructions text:
```text
{{fragment "general_fragments.socratic_tutor"}}
```
The part in quotes is split at the first dot: `general_fragments` is the alias you gave the library under `fragment_files:`, and `socratic_tutor` is the fragment's `id` inside that library. The fragment's text drops in exactly where the marker sits, so the order of your prompt is simply the order you write the markers. There is no separate list of fragments and no ordering number to manage.
Here is a tutor placing three fragments from the shared example library, with its own wording in between:
```yaml
prompt:
fragment_files:
- id: general_fragments
url: "../shared/general-fragments.yaml"
tutor_instructions: |
{{fragment "general_fragments.socratic_tutor"}}
You are helping students with Bubble Sort and Selection Sort.
{{fragment "general_fragments.teenager_safety"}}
```
A fragment that expects values takes them as arguments on the marker, right where you place it:
```text
{{fragment "general_fragments.language_policy" natural_language="German" code_language="English"}}
```
You can place the same fragment more than once with different values, and a marker on its own line keeps the prompt readable. This quiz places two fragments in its top-level `instructions`, so both the grading and the discussion chat get the language policy and the safety net:
```yaml
fragment_files:
- id: general_fragments
url: "../shared/general-fragments.yaml"
instructions: |
{{fragment "general_fragments.language_policy" natural_language="German" code_language="English (TypeScript and p5.js terms)"}}
{{fragment "general_fragments.teenager_safety"}}
```
## What a library file looks like
A fragment library is a YAML file with an `id` and a list of fragments. Here's a small, complete library with two fragments:
```yaml
id: simple-fragments
fragments:
- id: persona
input_schema:
type: object
required:
- subject
properties:
subject:
type: string
greeting:
type: string
default: "Hi there!"
content: |
{{greeting}}
You are a friendly, encouraging tutor for {{subject}}.
- id: ground_rules
input_schema:
type: object
required:
- rules
properties:
rules:
type: array
items:
type: string
content: |
Follow these ground rules:
{{#each rules}}
- {{this}}
{{/each}}
```
Each fragment carries a few fields:
- **`id`** names the fragment; it must be unique within the library, and it's what a marker refers to.
- **`input_schema`** declares the values the fragment expects. Leave it out for a fragment that takes no values.
- **`content`** is the prompt text itself, written as a template.
- **`version`** is optional. It's a number you raise when you change the fragment meaningfully, for you and your colleagues to track changes; it doesn't change how the fragment behaves today.
A fragment may also carry a `classification` label, for example to mark a safety piece. It's a note for readers of the library; it doesn't change validation or behaviour today.
A larger, real library is `activities/examples/shared/general-fragments.yaml`: a Socratic teaching style, a topic limiter, a language policy, and a safety net for teenage students, reused across the sample activities of every kind.
## Writing the content
The `content` field is a template (the Handlebars format, if you want to look it up). You only need three constructs:
**Insert a value** with `{{name}}`:
```yaml
content: |
You are a tutor for {{subject}}.
```
**Loop over a list** with `{{#each}}`, using `{{this}}` for the current item:
```yaml
content: |
Follow these ground rules:
{{#each rules}}
- {{this}}
{{/each}}
```
**Show text only when a flag is off** with `{{#unless}}`:
```yaml
content: |
{{#unless allow_solution}}
Do not give away the full solution.
{{/unless}}
```
Text is inserted exactly as written: characters like `<`, `>`, and `&` survive, so small ASCII diagrams such as `[A] -> [B]` come through unchanged. One rule to respect: every `{{variable}}` the content uses must be declared in the fragment's `input_schema`. A variable the fragment never declares fails validation.
## Declaring values, and defaults
The `input_schema` block declares what a fragment needs, and a marker supplies those values as arguments. Three value types are supported, each with its own way of writing the argument:
| `type` | Meaning | Marker argument |
| --- | --- | --- |
| `string` | A piece of text | `subject="fractions"` |
| `boolean` | `true` or `false` | `allow_solution=false` |
| `array` (of `string`) | A list of text items | `rules=(array "…" "…")` |
A list is written with the `array` helper, one quoted item after another: `topics=(array "Bubble Sort" "Selection Sort")`. These three shapes are the only ones a marker accepts, and the reference in quotes is always required.
Inputs listed under `required` must be supplied by every marker that places the fragment, with the right type; validation names exactly what's missing or mismatched. Supplying a value the fragment doesn't declare isn't an error, but it draws a warning, because it usually means a typo.
An optional input can carry a **`default`**, used whenever a marker leaves the value out. In the `persona` fragment above, a marker that sets no `greeting` gets "Hi there!"; a marker that supplies one gets its own text, because a supplied value always wins. Two details worth knowing:
- A default must match its declared type: a text default on a `boolean` input is rejected.
- A default on a *required* input can never apply, since the value must be supplied anyway. The validator flags that combination with a warning.
Defaults are what make a fragment pleasant to reuse: the common case needs no values at all, and the unusual class overrides just the one value it cares about.
## Embedding a plain-text file
Sometimes you don't need a parameterised fragment, you just want an existing file inside the prompt: the markdown notes for this week's unit, or the sample solution a coding assistant should steer students towards. For that, declare the file under `text_files:` and place it with a `{{file}}` marker:
```yaml
text_files:
- id: solution
url: https://raw.githubusercontent.com/rstropek/htl-2025-26-2nd/refs/heads/main/40-classes/LinkedListWithTests/src/linkedList.ts
instructions: |
Here is the sample solution. Guide students towards it, never paste it back:
{{file "solution"}}
```
`text_files:` sits in the same place as `fragment_files:` for each kind (inside `prompt:` for a tutor, at the top level for a quiz, writing, or coding activity), and the entries have the same shape: a short `id` alias plus a `url`, absolute or relative to the activity file. The two lists share one set of aliases, so an id you use under `text_files:` must not also name a fragment library.
A few things make text files simpler than fragments:
- The marker is a bare quoted alias, `{{file "solution"}}`, with no dot: a plain file has nothing to select inside it.
- The file is inserted exactly as fetched. It's ordinary text, not a template, so `{{ }}` inside the material stays literal and needs no escaping; only your own surrounding prose follows the `\{{` rule.
- The only arguments are optional line numbers: `{{file "course" from=120 to=180}}` embeds lines 120 to 180 (counting from one, both ends included). Either end works alone: `from=120` means "from line 120 to the end", `to=40` means "the first 40 lines". You can place the same file several times with different ranges, for example the whole file for context and one excerpt to focus on today.
Like a fragment library, a text file is fetched fresh when a student opens the activity, so editing the hosted file updates the prompt without touching the activity. A file that can't be fetched, or is larger than 200 KB, stops the activity from starting rather than running with material missing. The linked-lists coding activity under `activities/examples/linked-lists/` is a complete, validated example of the sample-solution pattern.
## Braces that are not markers
An activity that declares neither `fragment_files:` nor `text_files:` is left exactly as you wrote it, character for character. So a plain activity, or a teaching example whose instructions show `{{ }}` as ordinary text, is safe: nothing tries to read those braces as markers.
Once an activity does declare a fragment library or a text file, its instructions are read as a template. Two things follow:
- If you want a literal `{{` in your own wording (not a marker), write it as `\{{` so it isn't mistaken for one.
- A marker only works when its library is declared. If you write `{{fragment "…"}}` but forget the matching `fragment_files:` entry, the text is sent to the model as-is instead of being replaced. Declare every library you place a marker from.
## Validating a library
You can validate a fragment library on its own, before any activity uses it: on the app's Validate page, switch the selector to **Fragment library** and paste the library's address, or run the CLI with `--kind fragment`. The check confirms the file's structure, that fragment ids are unique, and that every fragment's content renders against its own declared inputs, so a typo in a template surfaces before a colleague's activity trips over it.
Validating or sharing an *activity* runs the same thorough check over every fragment in every library the activity references, even fragments it doesn't place. A library that validates once is safe for everyone who builds on it. The activity check also fetches every declared text file and checks each placed line range against the real file, so a `from=` or `to=` past the end of the file fails validation instead of surprising a class later.
One safety property to rely on: fragments never vanish silently. If a library can't be loaded or a fragment fails when a student opens the activity, the activity refuses to start rather than running without the missing rules. A safety policy you placed is either in effect or the activity doesn't run.
## Hosting a library
A fragment library is hosted like any other activity file: at a public web address (for example a raw GitHub URL), or as a file hosted in the app itself on the **Files** page, which is the easiest route when you don't want to touch GitHub. Publishing works the same way for libraries and activities.
An activity references a library by URL, and the URL may be relative: a plain path like `../shared/general-fragments.yaml` resolves next to the activity file's own published location. Keep a library next to the activities that use it and the references stay short. When the files live on GitHub, remember to commit and push the library before validating; the server reads the published version, not your local copy.
# Hosting images
> Upload an image in Novedu and show it in a quiz or tutor by name, without running your own image hosting.
A picture often explains more than a paragraph: a diagram in a quiz question, a
map, a chart students should interpret. You can host such images directly in
Novedu. The image gets a stable name, you don't need a public web server, and
every activity you write can reference it by that name.
## Upload an image in the app
Open the **Images** page in the app (you need a teacher account).
1. Select **New image**.
2. Enter a name. Use only letters, digits, underscores, and hyphens, for
example `compass-rose` or `sorting_diagram_1`.
3. Pick the file: a PNG, JPEG, or SVG, at most 5 MB.
4. Optionally add a credit line, for example a licence notice like
`Compass rose — CC BY 4.0`. It appears in small print under the image
wherever the image is shown.
5. Upload. The image appears in your list, and the name is ready to use in
your YAML.
The list holds every teacher's images, and it opens on your own: the **Owner**
box starts on your name. Pick **All owners** to see everyone's images, pick a
colleague to see only theirs, or select **Clear** to come back to your own. You
can also filter by name, and sort by any column header. The **View** button
opens an image so you can check you picked the right one.
An image's owner is whoever saved it last, the same rule that applies to hosted
activity files. Uploading an image under a name that is already taken is
rejected, so an image only changes hands if someone deletes it and uploads a new
one under the same name.
## Upload an image with the CLI
If you already work in the terminal, or you let a coding agent manage your
activities, the `novedu-cli` command line does the same job (the introduction
chapter on the Novedu CLI and its AI skill covers the CLI itself; sign in once
with `novedu-cli login`):
```bash
npx @novedu/cli images upload compass-rose --file ./compass-rose.png --credit "CC BY 4.0"
npx @novedu/cli images list
```
`images upload` needs the file via `--file`; the type comes from the file
extension (`.png`, `.jpg`/`.jpeg`, or `.svg`). `images list` shows your images
the same way the Images page does.
## Show the image in an activity
Reference a hosted image from your activity YAML by its **name**, with
`hosted: true`. For example, above a quiz question:
```yaml
image:
hosted: true # look the image up by NAME in the app's image store
src: sample-compass-rose # the hosted name
alt: A compass rose showing the four cardinal directions. # accessible description
credit: Compass rose — CC BY 4.0 # optional, overrides the stored credit
```
- `src` is the name you chose at upload, not a link.
- `alt` is the accessible description read aloud by screen readers; write it in
the YAML for each place you use the image.
- `credit` is optional here: without it, the credit stored at upload is shown.
Tutors and fragments accept the same kind of image block; check the authoring
guide of the module you're writing for where it goes.
## Replace or delete an image
An uploaded image can't be edited or overwritten. To replace one, delete it on
the **Images** page (tick it, then **Delete Selected**) and upload the new file
under the same name. Activities that reference the name then show the new
image. Deleting is only possible in the app, not with the CLI; an activity that
references a deleted name simply shows no image.
## Three things to get right
- Reference images by **name** with `hosted: true`, never by pasting a link.
The link the image list offers only opens for a signed-in Novedu user, in a
browser; the name keeps working everywhere, including in the YAML.
- A name that is already taken is rejected, in the app and in the CLI alike.
Pick a new name, or delete the old image first if you want to replace it.
- Hosting an image is not the same as letting students **answer** with a photo.
Photo answers are a quiz setting (`imageInput`); hosted images are pictures
you show to students.
# Creating a shared code
> Turn an activity into a short link for your class, with a note, an availability window, and an optional model override.
Once an activity is ready, you hand it to a class as a code: a short link that opens the activity for anyone who has it. Creating one takes a minute, and you can create as many codes for the same activity as you like, for example one per class.
## What you need first
The activity's YAML file must be reachable at a public web address. There are two ways to get one:
- **Host the file yourself**, for example in a public GitHub repository, and use the raw file address.
- **Upload it to Novedu's file store.** Every stored file gets a public address, and the file list offers a **Create code** shortcut that opens the create form with the kind and the address already filled in.
## Create the code
Novedu checks the activity file before storing anything. If the file has errors, the form lists them and no code is created; fix the file and submit again.
1. Open **Codes** and select **New code**.
2. Under **Activity**, pick the kind of activity: tutor, quiz, writing, or coding.
3. Paste the file's address into **Activity YAML URL**.
4. Add a **Note** for yourself, for example "3AHIF linked lists exercise". Only teachers see it; it labels the code in your list.
5. Set **Available from** and **Available until** if the code should only work during a certain time. Both fields use your local time, and either may stay blank to leave that side open. The **Now**, **+1h**, **+1d**, and **+1w** buttons fill common values.
6. Fill in the **LLM override** only if this one code should run on different AI settings than the activity file names.
7. Select **Create code**.
## The LLM override section
The override section has three fields and a row of buttons:
- **LLM provider override** and **LLM model override**, two free-text fields. They always go together: fill in both, or leave both blank. The provider is one of `SCCH`, `Azure Foundry`, or `OpenRouter`, written exactly like that.
- **Reasoning (optional)**, a dropdown that starts on **Provider default** and offers the thinking-effort levels `none`, `minimal`, `low`, `medium`, `high`, and `xhigh`. A level only works alongside a provider and a model; on its own it is rejected. Not every model accepts every level, and some ignore the difference between them, so the chapter on choosing an AI model is worth reading before you set one.
- **Preset buttons** that fill the whole override in one click, one per common combination, covering all three providers: **OpenRouter · GLM 5.3 Flash**, for example, fills in the provider `OpenRouter` and the model `z-ai/glm-5.3-flash`. A preset for a reasoning model sets its level too, a preset for a plain model puts the dropdown back on **Provider default**. **Clear** empties all three fields, so the code uses the activity's own settings again.
The override replaces the activity file's whole `llm:` block for this code. If the activity file sets a reasoning level and you leave the dropdown on **Provider default**, the code runs without one. Repeat the level in the override whenever you want to keep it. The chapter on choosing an AI model explains the settings themselves.
One thing to keep in mind when you override: SCCH, the school's Austrian LLM hosting partner, doesn't bill per use, but Azure Foundry and OpenRouter are both paid per use, and a high thinking effort multiplies what a lesson costs on them. Pointing a code at either one is a spending decision as well as a quality one.
## What you get
Novedu generates the code text for you, a short random string; you can't choose your own. After creating it you land on the code's page, which shows the full link with a copy button. Share it however you reach your class: paste it into your learning platform, show it on the projector, or write it on the board. Students can also type just the code on the Novedu start page.
For a coding activity, opening that same link is how a student picks up the personal key their coding tool needs; the chapter on connecting a coding activity covers that step.
## What you can change later, and what's fixed
You can reopen any code from the list and edit three things at any time:
- the note,
- the availability window,
- the model override.
Three things are fixed when the code is created and never change: the kind of activity, the activity file's address, and whether the activity records who did what (anonymous or per-user, taken from the activity file at creation time). To share a different file, create a new code.
The code points at your file, not at a copy of it. If you edit the activity file itself, students get the new version the next time they open the link.
## Finding a code in the list
The Codes page holds every teacher's codes, and it opens on your own: the **Owner** box starts on your name. Pick **All owners** to see the whole staff room's codes, pick a colleague to see only theirs, or select **Clear** to come back to your own. You can also filter by note or code text and by kind of activity, and sort by any column header.
A code's owner is the teacher who created it, and that never changes. Every teacher can still open, edit, and delete any code, so ownership is a way to find your own work quickly, not a lock.
## Creating codes from the command line
If you're comfortable with a terminal, the Novedu CLI creates codes too, with the same checks as the web form. Sign in first with `novedu-cli login`, then:
```bash
npx @novedu/cli codes create --module quiz \
--file https://raw.githubusercontent.com/Teaching-HTL-Leonding/novedu-chat-mvp/refs/heads/main/activities/examples/sorting-algorithms/sorting-quiz.yaml \
--note "3AHIF sorting quiz" \
--start 2026-07-07T08:00:00Z --end 2026-07-07T10:00:00Z
```
`--start` and `--end` take ISO 8601 times with an explicit offset (the `Z` above means UTC) and may be left out for an open-ended code. `--llm-provider` and `--llm-model` set the model override, always together, and `--llm-reasoning` adds a thinking-effort level (`none`, `minimal`, `low`, `medium`, `high`, or `xhigh`) on top of that pair; without the pair it is rejected, and left out it drops whatever level the activity file sets. `novedu-cli codes list` shows your codes.
# Seeing how a code is used
> Check a code's own statistics page and the overall usage view to see how much students and the AI have been working.
Novedu gives you two views on how your activities are being used: each code has its own statistics page, and a separate usage view sums up all AI use across the school. Both are visible to teachers only.
## What counts as a use
A use is counted when a student actually writes something: sends a chat message, submits a quiz answer, or works on a text. A student who merely opens the page and leaves without typing doesn't count. So the numbers tell you who worked, not who clicked.
## A code's statistics page
The **Codes** list shows a quick interaction count next to every code. Select the stats icon on a row to open that code's own page. What it shows depends on the module:
- **Tutor and quiz codes** show the number of conversations (for a quiz, discussions), and a table of each one with its first and last message time and how many messages the student wrote. Each row opens the full conversation as a read-only transcript.
- **Writing codes** show who saved a text: each student's name, when they last saved, and how many feedback conversations they had. Opening a student takes you to their saved text and their conversations, with previous and next buttons to read through the whole class.
- **Coding codes** show your instructions, the pinned model, a button that gets you your own connection details, and a list of everyone who has picked up a personal key for the code, with the time they did so; there are no in-app conversations to list.
For a per-user code you also see how many distinct students took part, and each conversation carries the student's name.
An anonymous code shows the same counts and the same conversations, but no student identities: no names, no per-student view. Anonymity hides *who* wrote something, never *what* was written, so you can still read the transcripts.
## The overall usage view
Select **Usage** in the menu to see how much AI the whole installation has used. A time filter (**Last 24 hours**, **7 days**, **30 days**, or **365 days**) applies to everything on the page:
- **Token usage over time**: a bar chart of how much the AI models processed per hour, day, or month, with the same numbers in a table below it.
- **Three breakdowns**: tokens by category (tutor, quiz, writing, coding), by code (labelled with the note you gave each code), and by model.
- **Two totals**: **Chats** (conversations where a student wrote at least one message) and **Quiz answers graded**.
All times on the usage view are UTC, so during the school year the labels sit one or two hours behind Austrian clock time.
The usage view never shows student names or message content; it is about volume, not people. To read what was said under a specific code, use that code's own statistics page.
# Time-limiting a code
> Set an optional start and end time so a code only works during your lesson or until a deadline.
Every code can carry a time window: a start time, an end time, or both. Outside the window the link doesn't open the activity, so you can hand out a code before the lesson and know it only works when you want it to. The window belongs to the code, not the activity, so two codes for the same activity can have different windows.
## Setting the window
You set the window on the same form you use to create or edit a code. Two fields control it:
- **Available from**: when the code starts working. Select **Now** to start immediately, or pick a date and time.
- **Available until**: when the code stops working. The **+1h**, **+1d**, and **+1w** buttons extend the end time by an hour, a day, or a week, counting from the current end time (or from the start time if you haven't set an end yet).
You enter both times in your own local time, exactly as you'd read them off your classroom clock. A typical lesson setup: set **Available from** to the start of the lesson and press **+1h**.
If you set both times, the end must be after the start; the form tells you if it isn't.
## Either bound is optional
Both fields can be left blank, and each blank field has a clear meaning:
- No start time: the code works as soon as you create it.
- No end time: the code never expires.
- Both blank: the code is always open.
Use the **Clear** button next to a field to remove a bound you set earlier.
## What students see outside the window
During the window, the link works normally. Outside it, the link shows a short explanation instead of the activity:
- Before the start, students see that the activity is not available yet, together with the time it becomes active.
- After the end, students see that the code has expired, when it stopped working, and a hint to ask their teacher for a new code.
Times in these messages appear in the student's local time. The window is checked on every interaction, not just when the page opens, so a chat that is still on a student's screen stops accepting new messages the moment the window closes.
## Changing the window later
You can change the window at any time after creating the code: open the code's edit page and adjust the **Available from** and **Available until** fields. The change takes effect immediately.
This also works for a code that has already expired. Extending its end time (or clearing it) makes the same link work again, so students keep any conversations they already had under that code. There's no need to create and distribute a new code for a second round.
## Expired codes stay in your list
An expired code isn't deleted for you. It stays in your list of codes, marked with an **expired** badge, and keeps all of its statistics and student conversations so you can review them after the lesson. The list also shows each code's **Valid from** and **Valid until** times in your local time. The code and its data only disappear when you delete the code yourself.
# Anonymous or per-user: what an activity records
> What you can see in each mode, the default for each activity kind, and why the choice is fixed when a code is created.
Every activity runs in one of two modes: anonymous, where the app never links work to a student, or per-user, where it records who did what. The mode decides what you can see afterwards, so it's worth choosing deliberately, especially for graded work.
Students sign in with their school account either way. The mode isn't about who can open the activity; it's about whether their work is linked to their name.
## Anonymous: you see what, not who
In an anonymous activity the app stores no link between a student and their work. On the statistics page for a code you see how much the activity was used, and you can still open and read every conversation, but nothing tells you which student it belongs to. Anonymity hides *who*, never *what*.
Anonymous is a good fit for practice and exploration: students can ask basic questions or make mistakes without worrying that it lands next to their name.
## Per-user: you see who did what
In a per-user activity the app records the author. The statistics page for a code shows how many different students took part and names the student behind each conversation or quiz attempt, and for a writing activity you can open each student's saved text together with their coach conversations. Choose per-user when you need to review or grade individual work.
Tell your class when an activity records who did what, so nobody assumes they are practising anonymously.
## The defaults by kind
Each kind of activity has its own default:
| Kind | Default | Why |
| --- | --- | --- |
| Tutor | Anonymous | Chatting with a tutor is practice; students should feel free to ask anything. |
| Quiz | Anonymous | Answers feed the aggregate statistics without naming anyone. |
| Writing | Per-user | Reviewing a saved text needs an author, so writing records one by default. |
| Coding | Always anonymous | Requests from a coding tool carry no student identity; there is no setting to change. Picking up the connection key is the one exception, always recorded with the student's name. |
## Where you set it
The mode comes from the `anonymous` field in the activity's YAML file. Omit it to keep the kind's default, or set it explicitly. This example from a writing activity spells out the default rather than relying on it:
```yaml
# Writing defaults to attributed (anonymous: false): the teacher reviews and
# grades the saved letters, so they must know whose text it is.
anonymous: false
```
For a tutor or a quiz, `anonymous: false` switches the activity from its anonymous default to per-user. A coding activity has no `anonymous` field at all; adding one is rejected as an error. Coding keeps its own fixed exception regardless: a student's coding conversations stay anonymous, but picking up the connection key their tool needs is always recorded with their name, covered in the chapter on connecting a coding activity.
## The choice is frozen when you create a code
When you create a code, the activity's current mode is fixed onto that code and stays with it for its whole life. Editing the activity file later does not change codes that already exist; the edit only affects codes you create afterwards. If you change your mind, create a new code from the updated file and share that one instead.
## Anonymous writing turns off saving
A writing activity set to `anonymous: true` has no author to save a text under, so saving is disabled: students see no **Save** button and their draft is gone when they leave the page. The coach chat and the formatted preview still work, so the activity remains useful as a pure practice space. Keep writing per-user whenever you plan to review or grade the texts.
# Deleting a code
> Remove codes you no longer need, and know that a code's conversations, saved texts, and statistics are deleted with it, permanently.
A code never disappears on its own. Even after its time window closes, it stays in your **Codes** list (marked **expired**) with all of its data, until a teacher deletes it. Deleting is how you tidy up after a unit is finished, and it's permanent: the code and everything recorded under it are gone for good.
## How to delete codes
Deleting happens in the **Codes** list, and only there. There's no delete button on a code's edit page or statistics page.
1. Open the **Codes** list.
2. Tick the checkbox on each code you want to remove. The checkbox in the header selects every listed code at once.
3. Select **Delete Selected**. The button shows how many codes you've picked.
4. Confirm in the dialog that appears.
You can delete one code or many in the same step. Any teacher can delete any code, not only their own, so check the note and creator before you tick a row in a shared installation.
## What is deleted with a code
Deleting a code removes the code and everything that was recorded under it:
- The link stops working immediately. Students who open it see an unknown-code message, and the code disappears from their recent-codes shortcuts.
- All conversations and quiz discussions under the code are deleted, including their transcripts.
- Saved student texts from a writing activity are deleted.
- For a coding activity, every personal key issued for the code is deleted, so any coding tool still using one stops working immediately.
- The code's statistics page is gone, along with its interaction counts and per-student view.
Review the code's statistics and read or copy anything you still need before you delete: there's no way to open a deleted code's conversations, saved texts, or statistics afterwards.
## Deleting is permanent and per-code
There's no undo and no recycle bin. Deletion also always applies to a whole code: you can't remove a single student's conversation or saved text while keeping the rest. If a code holds work you want to keep, keep the code.
If you only want students to stop using an activity, you don't need to delete anything. Set an end time on the code instead: an expired code no longer opens for students, but its conversations and statistics stay readable for you.
# Connecting a coding activity to an outside tool
> How a student picks up a personal API key for a coding code, the attribution notice that comes with it, and what you see as the teacher.
You share a coding activity exactly like any other: create the code and hand its link to your class. What is different is what happens after a student opens that link. There is no chat inside Novedu to greet them; instead, they sign in and pick up a personal key for their own coding tool.
## Sharing works like any other activity
Creating a coding code follows the same steps as a tutor, quiz, or writing code: pick the activity file, add a note, set an availability window if you want one, and select **Create code**. The code's page then shows the same share link every other kind of activity gets. Send it to your class however you'd share any other link: your learning platform, the projector, or the board. Students can also type the code on the Novedu start page.
## What a student sees
Opening the link asks a student to sign in with their school account, the same as any other activity. Because a coding activity has no chat page, the page then shows connection details instead of a conversation:
- the server address (base URL),
- a personal API key, theirs alone,
- a model name,
- for [little-coder](https://github.com/itayinbarr/little-coder), a ready-to-paste configuration file (`models.json`) and a run command.
Each detail has a copy button, so setting up the coding tool is copy, save, run. The key stays the same every time the student comes back, from any device, so they only need to set their tool up once.
The page also carries a notice a student cannot miss: requesting this activity's API key is recorded with their name for you, and that their coding conversations are never stored. Opening the page and signing in is what asks for the key, so make sure your class knows that in advance, the same way they know that reporting a conversation is not anonymous.
## What you see as the teacher
A coding code's own page (reached from the **Codes** list) shows your instructions and the pinned model, plus two things about connections:
- **Your own connection details.** You can get a personal key for yourself, the same kind a student gets, so you can test the endpoint end to end before handing out the code. It is not handed to you automatically: the page offers a **Get my API key** button, and only pressing it creates the key. Once you have one, the page shows your connection details straight away on every later visit.
- **Issued keys**, a read-only list of everyone who has requested a key for this code, with the time they requested it. There is no per-student conversation to open, because coding conversations are never stored; the list only tells you who is connected, not what they asked.
Getting your own key is recorded exactly like a student's, so the button carries the same notice: your name goes into the **Issued keys** list below it. That is why the key is behind a button rather than automatic. Simply opening a coding code's page records nothing, so you can review your activities without appearing in your own class list.
## Access control is the window, not the key
There is no button to take a single key away. Access to a coding activity works the same way as any other code:
- **The availability window.** The moment a code's window closes, every key issued for it stops working immediately, for every student, even ones who set their tool up days earlier. This makes a coding code a good fit for bounding AI help to your lesson time, or shutting it off for an exam. Reopen or extend the window and the same keys work again; nobody needs to reconnect their tool.
- **Deleting the code.** Deleting a coding code deletes every key issued for it along with everything else recorded under it. There is nothing left to revoke one student at a time; deleting the code turns every one of its keys off at once.
## The activity code itself opens nothing on its own
The code string you share is not, by itself, an API key. A student who only has the code and has not signed in gets nowhere: pasting a bare code into a coding tool does not work, only a personal key does. This means a leaked code grants nothing on its own, someone would still need to sign in with a school account to turn it into a working key.
# Reviewing student reports
> Let students flag notable AI answers, then work through what comes in on the Reports page.
Students can flag a notable AI answer to you: something brilliant, something wrong, or something that needs your attention right away. A report is one small action a student takes inside an activity, and every report lands in one place for you to review: the Reports page. Reports are a feedback channel, not only a complaint box, so it's worth telling a class they can flag great answers too.
## What students can report
A small **Report** button sits in every chat: a tutor conversation, a quiz discussion, and the feedback chat in a writing activity. It also sits next to every graded quiz answer. Coding activities have no in-app chat, so they have no Report button.
Filing a report takes two clicks. The student picks one of four reactions and can add a short note:
- **Good** for an answer worth praising.
- **OMG** for a surprising one.
- **Bad** for a weak or wrong one.
- **Holy sh..** for something that needs a teacher's attention now.
The note is optional. The student selects a reaction, can type a sentence about what happened, and sends it.
## Reports are never anonymous
A report always carries the reporting student's name, even on an anonymous code. The form says so before anything is sent: it warns the student that the report is not anonymous and that their name and the reported conversation or answer will be shared with you. So filing a report is the student's own choice to be named. Nobody is identified without acting.
An anonymous code stays anonymous everywhere else. A report on such a code names only the student who filed it. The rest of the class's work under that code keeps its anonymity: you still see what was written, never who wrote it, exactly as on the code's statistics page.
## The Reports page
Select **Reports** in the menu to open the list of every report across your codes. Only teachers can see it. There are no notifications and no emails: reports show up here and nowhere else, so check the page when you want to see what students have flagged.
When you open the page it shows the reports that still need attention, with the most urgent first:
- Only **open** reports, the ones you have not resolved yet.
- Only your own codes, because **Only my codes** is ticked by default. Untick it to see reports on codes created by other teachers.
- The urgent **Holy sh..** reports float to the top and carry a red stripe, so the ones that need you now are easy to spot.
You can narrow the list with the filters at the top: switch between **Open**, **Resolved**, and **All**, pick a single reaction, or type in the search box to match a description, a student, or a code.
## Opening a report
Each report opens to show you exactly what the student saw. How it opens depends on where it came from:
- A **chat** report opens the full conversation as a read-only transcript, so you can read the whole exchange around the flagged moment.
- A **quiz** report opens a detail view with the question, the student's answer, and the AI's feedback, shown just as the student saw them when the answer was graded.
A quiz report carries its own copy of the answer and the feedback. Quiz answers are not stored anywhere else, so this copy is the only record of that graded moment: keep the report if you want to keep the answer. If the student answered with a photo, the report notes that a photo was attached, but the photo itself is not kept.
Every report also has a details view that shows the reaction, the student's name, the code, and the note they wrote, whatever kind of report it is.
## Working through reports
You handle reports in bulk from the Reports page. Tick the reports you want to act on, then use the buttons above the list:
- **Mark resolved** clears the ones you've dealt with, so they drop out of the default open view.
- **Reopen** brings a resolved report back if you need to look again.
- **Delete Selected** removes reports you no longer need to keep.
Resolving a report does not delete it. It stays available under the **Resolved** and **All** filters, so you have a record of what was flagged and what you did about it.
## Triaging reports from the command line
If you'd rather work in a terminal, or you'd like an AI coding assistant to help, the Novedu CLI handles reports too. Sign in once with `novedu-cli login`, the same sign-in the other CLI commands use, and then three commands cover triage:
```bash
# List open reports on your own codes (add --all for every teacher's codes)
npx @novedu/cli reports list
npx @novedu/cli reports list --status resolved --reaction holysh --search "linked list"
# Show one report in full; a chat report also prints the whole conversation
npx @novedu/cli reports show
# Mark one or more reports resolved
npx @novedu/cli reports resolve
```
- **`reports list`** starts from the same view as the page: open reports on your own codes. Narrow it with `--status` (`open`, `resolved`, or `all`), `--reaction` (`good`, `omg`, `bad`, or `holysh`), and `--search`, or add `--all` to include other teachers' codes.
- **`reports show`** prints one report in full. For a chat report it includes the conversation transcript, so you read the flagged exchange without opening a browser.
- **`reports resolve`** marks reports resolved, one or several at a time. The CLI records the resolve as done by you, the signed-in teacher, exactly as resolving on the page does.
The CLI prints its results as JSON. That reads a little densely for a person, but it's exactly what a script or an AI assistant needs. Reopening and deleting a report are left out of the CLI on purpose: those stay on the Reports page, so an automated helper can never delete a student's report. Reports are always filed by students inside an activity; the CLI never creates one.
This opens up a repair loop you can hand to an AI coding assistant working in a copy of your activities: it reads a report, works out what went wrong, fixes the activity file, publishes the new version, and marks the report resolved, all from the command line. Reports become the to-do list for improving your activities.
## Deleting a code deletes its reports
When you delete a code, its reports go with it. If you want to keep a flagged quiz answer or the note a student wrote, act on the report, or copy out what you need, before you delete the code. Once the code is gone, its reports are gone too.
# Many activities at once: the activity registry
> Keep every activity of a course in one registry file and let the CLI mint and refresh all its codes in a single command.
With one or two activities, creating a code by hand is quick and there is nothing to organise. With twenty, it stops working. Every new quiz means the same ritual: check the file, build its address, create the code, copy the code, paste it into the chapter that links to it. Nothing in your material says which code belongs to which activity file, so a year later the only way to find out is to open each code in Novedu and compare addresses.
The activity registry solves that. You write one file that lists every activity of your course under a short name you choose. One command reconciles that list with Novedu: activities that already have a code keep it, activities without one get a new code. The command writes a second file that maps your names to the codes, and your material refers to activities by name instead of by code.
The registry is a command-line feature. There is no registry page in Novedu, and the app knows nothing about your names: they live only in the two files in your repository. For a single activity, the create form in Novedu stays the simpler path.
## What you need
The registry lives next to your material, usually at the top of the git repository that holds it. To use it you need:
- The Novedu CLI, ready to run and signed in with your teacher account. The introduction chapter on the Novedu CLI and its AI skill covers what the CLI is and how to set it up. Sign-in is one browser step, and every later command runs without asking again.
- Your activity files reachable at a public web address, for example the raw addresses of a public GitHub repository, or addresses from Novedu's file store.
## Write the registry file
The registry is plain YAML. Give it any name you like, for example `ddp-activities.yaml`, and commit it with your material.
```yaml
# The address every relative file is resolved against. It must end with a slash.
base-url: "https://raw.githubusercontent.com/rstropek/ddp-ts-p5-beginner-course/refs/heads/main/"
activities:
quizzes:
welcome:
file: 0010-introduction/0010-welcome-quiz.yaml
note: "Creative Coding book: Welcome (0010)"
number-systems:
file: 0030-conditions/0050-number-systems-quiz.yaml
start: 2026-09-01T00:00:00+02:00
end: 2027-01-31T23:59:59+01:00
tutors:
sorting:
url: https://app.novedu.at/api/files/sorting-tutor
```
The example is the real registry shape used by the Creative Coding book, a TypeScript course where most chapters end with a quiz.
**The groups decide the kind of activity.** There are four, and each one may be left out: `quizzes` for quizzes, `tutors` for tutors, `writing` for writing activities, and `coding` for coding activities. A group name that is not one of these four is an error, so a typo can never silently drop half your course.
**Each entry says where the activity file is**, in one of two ways: `file` for a path relative to `base-url`, or `url` for a complete address. Use one or the other, not both. If you use `file` anywhere, `base-url` must be set and must end with a slash.
**Everything else in an entry is optional:**
- `start` and `end` set the availability window, written as a full date and time with a time zone offset, for example `2026-09-01T00:00:00+02:00`, or `Z` for UTC. These are the same window rules the create form uses.
- `note` is your own label for the code, up to 200 characters. Only teachers see it.
- `llm` sets a model override for this one code, with `provider` and `model` always given together and an optional `reasoning` level (`none`, `minimal`, `low`, `medium`, `high`, or `xhigh`) on top of them. The `provider` must be one of `SCCH`, `Azure Foundry`, or `OpenRouter`; any other name is an error, so a misspelt provider can never reach a code:
```yaml
llm:
provider: Azure Foundry
model: gpt-5.6-terra
reasoning: low
```
You can also add your own extra lines to an entry, for example a chapter number. Anything the registry does not recognise is ignored, so annotate freely.
## Choose good names
The name in front of each entry (`welcome`, `number-systems`, `sorting` in the example) is yours to pick, and it is what your material will refer to. Two rules apply:
- Lowercase letters, digits, and hyphens only, up to 64 characters.
- Unique across the whole file, not just within a group. One list of names covers all four groups.
Pick names that will still make sense next year: a chapter slug usually beats a number.
## Run the command
From the folder that holds the registry:
```bash
npx @novedu/cli codes sync ddp-activities.yaml
```
The report lists every entry with what happened to it:
```
ddp-activities.yaml: 3 entries
reused welcome cu4afwoa23 https://app.novedu.at/cu4afwoa23
minted number-systems hb34gpvahn https://app.novedu.at/hb34gpvahn
reused sorting nlc90ezf5z https://app.novedu.at/nlc90ezf5z
2 reused, 1 minted, 0 failed
Lock file: ddp-activities.lock.yaml
```
- **reused** means one of your existing codes already matches that entry, so nothing was created.
- **minted** means a new code was created. Novedu checks the activity file first, exactly as the create form does.
- **failed** means Novedu rejected that one activity, usually because its file has an error or is not reachable. The other entries still sync, the command ends with an error exit code, and the failed entry keeps the code it had before, so your material does not break while you fix the file.
Add `--dry-run` to see the same report without creating anything and without writing any file. It is the safe way to try a change to the registry.
## Commit the lock file
Next to the registry, the command writes a second file with `.lock.yaml` in its name, for example `ddp-activities.lock.yaml`:
```yaml
# Generated by @novedu/cli — do not edit.
# Regenerate with: novedu-cli codes sync ddp-activities.yaml
activity-codes:
number-systems: hb34gpvahn
sorting: nlc90ezf5z
welcome: cu4afwoa23
```
Commit this file together with the registry. It is generated: never edit it by hand, because every run rewrites it completely. The names are sorted, so two runs that change nothing produce exactly the same file and your version history stays quiet.
## What happens when you run it again
Running the command again is the normal workflow, not something to be careful about. Whether an entry keeps its code or gets a new one follows three rules:
- **Nothing changed, nothing happens.** An entry whose activity file, availability window, and model override still match one of your codes reuses that code. Editing the activity file itself changes nothing here: a code always points at the file's address, so students get your edits without a new code.
- **A new window, or any change to the model override, means a new code.** That includes the reasoning level on its own: the same provider and model at a different thinking effort is a different code. The command never edits or deletes an existing code, so it creates a second one and reports the old one as superseded. The old code keeps working and keeps its statistics until you delete it in Novedu, which matters because links you have already handed to students still point at it.
- **A different note changes nothing.** The note is a label for you, so it is not part of what makes a code match. Novedu keeps the note the code was created with.
Removing an entry from the registry drops its name from the lock file on the next run. The code itself is untouched and still works; the command mentions it so you can decide whether to delete it.
## Use the names in your material
The point of the lock file is that your material never contains a code. In a Quarto book, register the lock file as document metadata:
```yaml
# in _quarto.yml
metadata-files:
- ddp-activities.lock.yaml
```
The book's quiz shortcode then takes a name and looks it up in `activity-codes`, so a chapter reads:
```markdown
{{< quiz welcome title="Welcome" >}}
```
The name says what it links to, and a reader of the source can find the matching entry in the registry. The build only reads the committed lock file, so rendering the book works offline and never depends on Novedu being reachable. If a name is missing from the lock file, the build fails with a clear message instead of publishing a dead link.
Any other publishing system works the same way, as long as it can read a small YAML file at build time.
## The everyday loop
Once the registry is in place, adding an activity is five short steps:
1. Write the activity file and check it with `novedu-cli validate`.
2. Push it, so its public address serves the new file.
3. Add one entry to the registry with a new name.
4. Run `npx @novedu/cli codes sync ddp-activities.yaml`.
5. Commit the registry and the lock file, and refer to the activity by its name.
No code is ever copied by hand again.
## Options at a glance
| Option | What it does |
| --- | --- |
| `--dry-run` | Show the report without creating codes or writing the lock file. |
| `--json` | Print the report as machine-readable data, for scripts. |
| `--lock ` | Write the lock file somewhere other than next to the registry. |
| `--server ` | Talk to another Novedu server than the default one. |
# The Novedu CLI and its AI skill
> What the Novedu command-line companion does for you, and how to install and update the skill that lets your AI assistant use it.
The Novedu CLI is a small companion tool for the app that you run from a terminal. It does two things for you. It checks the activity files you write, and it lets you do your teacher work without opening the website.
Checking comes first, because it saves the most trouble. The CLI runs the very same checks the app runs when it loads your activity, so a missing field or a typo shows up on your screen instead of in front of a class. It can also print the exact instructions your activity sends to the AI model, which is the fastest way to answer "why is the tutor behaving like that?".
The second half is the teacher work: creating a code for an activity, uploading activity files and images to the app, seeing what students have reported, and testing how a quiz grades sample answers. Every one of those has its own chapter later in this guide.
You don't install the CLI permanently. With Node.js (version 22 or newer) on your computer, one command fetches and runs it whenever you need it:
```bash
npx @novedu/cli --help
```
## Working with PROD or DEV
Novedu runs in two environments: PROD at `https://app.novedu.at` for your classes, and DEV at `https://dev.novedu.at` for trying new features and experimenting with activities. Every CLI command that talks to Novedu works with one of them. Checking a file and printing its prompt work offline and don't need either.
### PROD, the default
The CLI works with PROD unless you tell it otherwise, so for your class work you don't add anything:
```bash
npx @novedu/cli@latest login
npx @novedu/cli@latest whoami
npx @novedu/cli@latest codes list
```
`whoami` shows your name and the environment you are signed in to, so it's a quick way to check where the next command will go. `@latest` makes sure `npx` runs the current CLI and not an old copy from your computer's cache.
### DEV, one command at a time
To send a single command to DEV, add `--server https://dev.novedu.at` to it:
```bash
npx @novedu/cli@latest login --server https://dev.novedu.at
npx @novedu/cli@latest whoami --server https://dev.novedu.at
npx @novedu/cli@latest codes list --server https://dev.novedu.at
```
The CLI remembers your sign-in per environment. Signing in to DEV doesn't sign you out of PROD, so you can stay signed in to both and switch between them command by command.
### DEV, for a whole terminal session
When you work on DEV for a while, set the variable `NOVEDU_SERVER` once. Every command in that terminal then goes to DEV without the extra option. On macOS or Linux:
```bash
export NOVEDU_SERVER=https://dev.novedu.at
npx @novedu/cli@latest whoami
```
In Windows PowerShell:
```powershell
$env:NOVEDU_SERVER = "https://dev.novedu.at"
npx @novedu/cli@latest whoami
```
The variable lasts until you close the terminal. A `--server` option on a single command still wins over it, so `--server https://app.novedu.at` sends that one command to PROD.
## Why there is an AI skill for it
You don't have to remember any of those commands. Novedu ships a skill, a set of written instructions that teaches an AI coding assistant (such as Claude Code, Codex, or Cursor) how to use the CLI properly: which command answers which question, which options matter, and how to read the error messages.
With the skill installed you work in plain language. You ask "is my quiz valid?" or "show me the grading prompt for question 3" or "create a code for this tutor", and the assistant picks the right command, runs it, and explains the result. That is a real difference for teachers who don't enjoy the terminal: you describe what you want, not how to get it.
Installing the skill does not install the CLI as part of your project. The skill is instructions only. When your assistant needs the CLI, it fetches it on demand with `npx`, exactly as in the command above.
## Install the skill
The skill is installed with [skills.sh](https://skills.sh), a small tool for managing agent skills. Open a terminal in the folder where you keep your course material and run:
```bash
npx --yes skills@latest add Teaching-HTL-Leonding/novedu-chat-mvp \
--skill novedu-tutor-cli \
--agent claude-code \
--yes
```
What the parts do:
- `Teaching-HTL-Leonding/novedu-chat-mvp` is the public Novedu repository the skill comes from. No sign-in needed.
- `--skill novedu-tutor-cli` picks exactly this one skill. The repository contains several, and you only want the one about the CLI.
- `--agent claude-code` says which assistant to install it for. Replace `claude-code` with `codex`, `cursor`, `github-copilot`, or whichever assistant you use. Naming it is safer than letting the tool guess.
- The first `--yes` tells `npx` to fetch the skills tool without asking. The final `--yes` skips the skills tool's own questions.
- `skills@latest` makes sure you get the current version of the skills tool and not an old copy from your computer's cache.
The command installs into the folder you are in, which is what you want: the skill sits next to your course material, so anyone you share the folder with gets it too. Don't add `--global`. A global install lives on your own machine only and your colleagues won't have it.
Afterwards you will find the skill's own folder (the exact place depends on your assistant) and a `skills-lock.json` file that records where the skill came from. If your course material is in Git, commit both, not just the lock file.
Start a fresh session with your assistant after installing, so it picks up the new skill.
## Check that it worked
Ask the skills tool what it has installed for your assistant:
```bash
npx --yes skills@latest list --agent claude-code
```
`novedu-tutor-cli` should appear in the list. The real test is easier: ask your assistant something like "validate my quiz with the Novedu CLI" and see whether it reaches for the right command.
## Update the skill
Novedu changes often, and the skill changes with it. From the same folder, update just this skill:
```bash
npx --yes skills@latest update novedu-tutor-cli --project --yes
```
Naming `novedu-tutor-cli` keeps the update to that one skill instead of everything you have installed. `--project` keeps it in your course material folder rather than updating a copy somewhere on your machine. To update all the skills in the folder on purpose, leave the name out:
```bash
npx --yes skills@latest update --project --yes
```
If your course material is in Git, look at what changed before you commit it, the same way you would review any other incoming change.
## Leave the installed skill as it is
Treat the installed skill as delivered material, like a textbook you received rather than one you write in. Don't edit the files by hand: the next update replaces them, and your changes disappear. If you want your assistant to follow extra rules of your own, put those in a separate skill or in your project's own instructions file, where an update can't overwrite them.
# Let your AI assistant read this guide
> Where the machine-readable form of the teacher guide lives, and how to point an AI assistant at it.
You read this guide as web pages. An AI assistant reads it more easily in another form: plain Markdown, the simple text format that assistants handle well. Novedu publishes the guide in that form too, following the llms.txt convention, an industry-wide way for websites to offer their documentation to AI tools. When your assistant has read the guide, its answers about Novedu come from the same chapters you use, not from guesswork.
## Three addresses
The machine-readable guide is public and needs no sign-in, just like the pages you are reading. It lives at three kinds of addresses:
- **The table of contents: .** It lists every chapter with a one-line description and a link to that chapter as a Markdown file. This is the best first link to hand an assistant, because it can pick out just the chapters it needs.
- **The whole guide in one document: .** Every chapter, in reading order. Use it when the assistant should know everything at once. It is a lot of text, so for a specific question the table of contents is the better start.
- **Any single chapter: append `.md` to its page address.** For example, the page becomes the Markdown file . This works on every chapter page of the guide.
## What the assistant gets
The Markdown files carry this guide's own text, word for word. They are not a summary and not a separate export that can fall behind: they are published together with the guide, so when a chapter changes, its machine-readable twin changes with it.
## Hand it to your assistant
The simplest way is to paste an address into the conversation together with your question, for example: "Read https://docs.novedu.at/llms.txt and then help me build my first quiz." An assistant that can fetch web pages (many can, such as ChatGPT, Claude, or Copilot, depending on how they are set up) follows the chapter links on its own and reads what it needs.
If your assistant cannot fetch web pages, open in your browser, copy the text, and paste it into the conversation instead.
## Reading and doing
The machine-readable guide covers the reading half: an assistant that has read it can explain how Novedu behaves. The doing half is the Novedu AI skill, which teaches an assistant to run the Novedu CLI for you: checking activity files, creating codes, and the rest of the teacher work. The two combine naturally. Install the skill, hand your assistant the guide, and it can both explain Novedu and act on it.