Create Resource Skill
This skill helps you create educational resources for the Beau platform. Resources are the building blocks of your curriculum - each is a self-contained learning unit delivered to students as a voice conversation, a narrated presentation, or a no-bot worksheet (self-paced screens / printable handout written for the student). Resources are grouped into courses.
Reference guides
This skill folder ships two markdown guides. They have different audiences (see each file's audience frontmatter):
resource-creation-guide.md (audience: assistant) — your authoring reference. Follow it when designing and writing resources.
teacher-guide.md (audience: human-ui) — end-user documentation for the Beau dashboard UI, written for human teachers. Useful background for terminology and workflow, but it is not authoring rules — don't treat its UI walkthroughs as instructions to you. (The repo's top-level guides/ folder holds the human-facing admin and student guides; those are not part of this skill.)
Instructions for Claude
When this skill is invoked, follow these steps:
Preflight Check: Verify the beaubot MCP server is connected by calling list_tags(pageSize: 1). If this fails with a tool error, tell the user: "The Beau MCP server is not connected. Please check your MCP connection and authenticate if prompted." Then stop.
Tag Governance (before creating anything):
a. Call list_tags(pageSize: 50) to fetch the full tag catalog
b. Reuse existing tags where possible — do not create near-duplicates (e.g. "maths" vs "math")
c. For genuinely new tags: ask the user to confirm, then call create_tag(name) to add them
d. Only use tags that exist in the catalog when calling create_resource
Gather Requirements: Ask the user about:
- Topic/subject for the resource
- Target audience (age, skill level)
- Delivery mode preference: conversation (two-way voice), presentation (one-way narration), or worksheet (no bot — self-paced screens / printable, written FOR THE STUDENT). This changes who the content is written for: conversation/presentation are written for the bot; a worksheet is written directly to the student.
- Desired length (default: 5-10 minutes)
- Any specific learning objectives
- Whether they want AI-generated images or have URLs to images/PDFs
Design the Resource: Based on the guides, create a structure with:
- Clear learning objectives
- Logical sections with headings
- A quiz after every major concept or section — aim for at least 2-3 quizzes per resource. Quizzes are the primary way students actively engage with the material, and lessons without them feel passive and text-heavy.
- At least 1-2 images per resource — visuals break up text, illustrate concepts, and give the bot something concrete to discuss with the student. A resource with no images is almost always too text-heavy.
- Mix quiz types for variety (single choice, multiple choice, open answer, fraction, ordered list, matching)
Create Content: Draft the markdown content following best practices:
- Write instructions for the bot, not a script
- Use "the student" with they/them pronouns
- Keep sections digestible
- IMPORTANT: Avoid text-heavy resources. Every section should either have a quiz, an image, or both. If a section is just text with no interactivity, consider adding a quick quiz to check understanding or an image to illustrate the concept. Students learn best when they actively participate, not passively listen.
- If a quiz refers to a figure, attach that figure to the quiz (its
image field), don't just place it inline before ::quiz{#id}. Then the diagram and the question are on screen together while the student answers. See Quiz Illustrations.
Source Media: Find or create visuals for the resource. Every resource should have images — they are essential for engagement, not optional decoration.
- Do NOT generate images via Python/code — this is too slow and produces poor results
- Use the
text visual (create_visual with kind: "text") for vocabulary cards, sight words, labels and key terms, and the math visual for equations — these are instant, vector (crisp at any size), and teacher-editable. create_text_image is deprecated — never use it.
- Use
generate_image only for custom educational illustrations / photoreal scenes — server-side AI generation; slower, so reserve it for when a real raster picture is genuinely needed
- Use
upload_image_from_url for existing public images (Wikimedia, educational sites)
- Use
create_image (base64) only as a last resort for user-provided local files
- Verify every image: After generation/upload, view the image to confirm it matches intent
- Write description/question/answer/hint based on what the image ACTUALLY shows, not what you intended
- For PDFs: description is critical since the bot cannot read PDF content
Execute Creation: Use MCP tools to:
- Create the resource via
create_resource
- Upload any media files via
upload_image_from_url or create_image
- Create quizzes via
create_quiz
- CRITICAL: Update resource content via
update_resource to embed image and quiz references using the correct markdown syntax (see below)
- Set a cover image: pick the most representative image for the resource and set it via
update_resource(id, coverImage: imageId). Cover images are shown to students when the lesson starts and make the catalog feel less like a wall of text. Do this in the same update_resource call as the content update.
- Optionally create a course and add the resource to it
Provide Testing Instructions: Tell the user how to test their resource using the Test Resource button in the admin UI
MCP Tools
Use the beaubot MCP server tools:
Available Tools
| Tool |
Description |
list_resources |
List existing resources with optional tag filtering |
get_resource |
Fetch a resource by ID |
create_resource |
Create a new resource |
update_resource |
Update an existing resource |
get_image |
Download an image by ID — returns the image visually (for inspection) plus metadata |
generate_image |
Generate an AI image from a text prompt and attach to a resource (uses org's OpenAI key, preferred) |
create_text_image |
DEPRECATED — do not use. For words/phrases/labels use the text visual (create_visual with kind: "text") — vector, instant, editable. |
create_visual |
Create a teacher-authored visual tool — number line, fraction, grid, timeline, math equation, word/text card, or counters — from a small JSON config (no AI, renders as crisp SVG). Embed with ::visual{#id} |
update_visual |
Update an existing visual tool's config or metadata |
create_svg |
Draw a scalable SVG image from raw markup (crisp, responsive, no AI). Best when no create_visual kind fits but it can be drawn with shapes/lines/text. Embed with  |
upload_image_from_url |
Upload an image or PDF from a URL to a resource |
prepare_image_upload |
Mint a single-use multipart upload URL for a local file (preferred over create_image for files on disk) |
create_image |
Upload an image or PDF (base64 data) to a resource (last resort) |
generate_audio |
Generate a spoken clip (text-to-speech) and attach as audio media. For listening or spelling/dictation: attach to a freetext quiz illustration (with exactMatch: true) so the student hears it and types the answer. The spoken text isn't shown — keep the answer in expectedAnswer. |
create_quiz |
Create a quiz for a resource. For spelling/dictation, use questionType: "freetext" + exactMatch: true (deterministic case/punctuation grading) + attach a generate_audio clip as the image. |
update_quiz |
Update an existing quiz (fix typos, attach an illustration image, set exactMatch, etc.) |
list_tags |
List available tags in the organization's catalog |
create_tag |
Create a new tag in the organization's tag catalog |
list_bots |
List the org's bots with their voice, avatar, and persona — call to pick a bot whose persona suits the subject |
get_bot |
Fetch a single bot's full config (including system prompt) by ID |
export_resource |
Export a resource as a base64-encoded ZIP (includes all media and quizzes) |
import_resource |
Import a resource from a base64-encoded ZIP (creates a new resource) |
create_course |
Create a new course (collection of resources) |
list_courses |
List existing courses |
add_resource_to_course |
Add a resource to a course |
list_course_resources |
List resources already in a course (with details) |
Choosing how to show something visual
When a lesson needs a picture or diagram, pick the most specific option — in this order. Going higher up the list gives crisper, more responsive, more editable results, so don't drop to a raster image out of habit.
- A dedicated visual tool —
create_visual (best). If the concept maps to a built-in kind, use it: number line, fraction, grid, timeline, math (KaTeX), word/text, counters, clock, ten frame, bar chart, base-ten blocks, money, phoneme frame, place value, Chart.js chart, function graph, geometry, coordinate grid, box plot, probability tree, atom (Bohr), pH scale, syllable split, onset/rime. These are purpose-built, teacher-editable, and render responsively. For arbitrary data charts the chart kind (Chart.js) covers bar/line/pie/scatter/etc.
- A hand-drawn SVG —
create_svg (good fallback). If no visual kind fits but the thing can be expressed with shapes, lines and text — a labelled diagram, a custom illustration, a flowchart, a simple map, a process diagram — write an SVG. It stays sharp at any size and is lightweight. Prefer this over a raster image.
- A raster image (last resort). Only when you truly need a photograph or a richly detailed picture that can't be vector-drawn:
generate_image (AI) or prepare_image_upload / create_image (an existing file). Supported raster uploads: PNG and JPEG. (SVG uploads are supported too, but use create_svg rather than uploading SVG by hand.)
All of these embed the same way in the content —  for images and SVGs, ::visual{#ID} for visual tools.
Workflow
0. list_tags(pageSize: 50) → Fetch org's tag catalog
create_tag(name) → Create new tags (after user confirmation)
1. create_resource(name, content, tags, deliveryMode)
→ Returns resource with ID (tags must come from the catalog)
2. create_visual(resourceId, kind, config)
→ Vector visual — use kind "text" for words/labels/vocabulary, "math" for equations,
or any other kind for diagrams. Instant, editable. PREFERRED for words. Embed with ::visual{#id}.
OR
generate_image(resourceId, prompt, description, question, answer, hint, botVisible)
→ AI-generated image — only for photoreal illustrations/scenes
OR
upload_image_from_url(resourceId, url, description, question, answer, hint, botVisible)
→ Downloads and attaches image from URL
OR (for a file on local disk)
prepare_image_upload(resourceId) → { uploadUrl }; then `curl -F file=@/path "$uploadUrl"`
→ Single-use multipart upload, no base64 round-trip
OR (last resort)
create_image(resourceId, name, mimeType, data, description, question, answer, hint, botVisible)
→ Uploads base64-encoded image
2b. create_visual(resourceId, kind, config, ...)
→ Authored visual tool (number line, fraction, grid, timeline, math, text, counters)
→ Renders as crisp SVG; embed in content with ::visual{#id}. Prefer this over an image
whenever the content is structured data the platform can draw.
3. create_quiz(resourceId, question, questionType, answers, image?, ...)
→ Returns quiz with ID
→ Pass image: <imageId> to attach an illustration (image must be on the same resource)
OR
update_quiz(resourceId, id, image?, ...)
→ Update an existing quiz — fix typos or attach an illustration after generating it
4. update_resource(id, content, coverImage?)
→ CRITICAL: Update content to include image and quiz references
→ Optionally set coverImage to an image ID to give the resource a cover
5. (Optional) create_course(name, description, tags, progressionType)
→ Returns course with ID
6. (Optional) list_course_resources(courseId)
→ View existing resources in a course before adding
7. (Optional) add_resource_to_course(courseId, resourceId, order)
→ Adds resource to the course
8. (Optional) export_resource(resourceId)
→ Returns base64-encoded ZIP for backup or sharing
9. (Optional) import_resource(zipData)
→ Creates a new resource from a previously exported ZIP
Tool Parameters
get_image:
imageId (required): The image ID to download
- Returns: For images (
image/png, image/jpeg), returns the image visually plus a text label. For PDFs/videos, returns metadata only:{
"id": 42,
"name": "dog-breeds.png",
"mimeType": "image/png",
"description": "Common dog breeds",
"question": "Can you identify these breeds?",
"answer": "Labrador, Poodle, German Shepherd",
"hint": "Look at the ear shapes",
"botVisible": true,
"resource": 15,
"organization": 3,
"createdAt": "2026-03-15T10:30:00.000Z",
"updatedAt": "2026-03-15T10:30:00.000Z"
}
create_resource:
name (required): Display name for the resource
content (required): Markdown content
tags (optional): Array of tags — must come from the tag catalog (call list_tags first, use create_tag for new ones)
deliveryMode (optional): "conversation", "presentation", or "worksheet"
update_resource:
id (required): The resource ID to update
name (optional): New name
content (optional): New markdown content
tags (optional): New tags (replaces existing — must come from the catalog)
deliveryMode (optional): "conversation", "presentation", or "worksheet"
coverImage (optional): Image ID to use as the resource cover (shown to students when the lesson starts). The image must already be attached to the resource (use create_visual, generate_image, upload_image_from_url, prepare_image_upload + curl, or create_image first, then pass the returned ID here). Pass null to remove the cover.
generate_image (preferred for custom illustrations):
resourceId (required): Resource to attach the image to
prompt (required): Text prompt describing the educational image to generate (max 4000 chars)
description (optional): What the image shows (important for bot)
question (optional): Question to ask about the image
answer (optional): Expected answer
hint (optional): Help for students
botVisible (optional): If true, bot can see the image
create_text_image — DEPRECATED. Do not use. To render a word, phrase, label or key
term, use the text visual instead: create_visual(resourceId, "text", { text: "...", … }). It
renders the same inline formatting (_underline_, *italic*, **bold**, [highlight],
{red:colored}), is vector instead of a raster PNG, is teacher-editable, and supports the org logo.
upload_image_from_url (preferred for remote use):
resourceId (required): Resource to attach image to
url (required): Public URL of the image to download
name (required): Image filename
description (optional): What the image shows (important for bot)
question (optional): Question to ask about the image
answer (optional): Expected answer
hint (optional): Help for students
botVisible (optional): If true, bot can see the image
create_image:
resourceId (required): Resource to attach image to
name (required): Image filename
mimeType (required): "image/png", "image/jpeg", "image/svg+xml", or "application/pdf". For SVG, prefer the dedicated create_svg tool (it takes raw markup, no base64).
data (required): Base64-encoded image data
description (optional): What the image shows (important for bot)
question (optional): Question to ask about the image
answer (optional): Expected answer
hint (optional): Help for students
botVisible (optional): If true, bot can see the image
create_visual (authored visual tool — renders as crisp SVG, no AI):
resourceId (required): Resource to attach the visual to
kind (required): One of "number_line", "fraction", "grid", "timeline", "math", "text", "counters", "clock", "ten_frame", "bar_chart", "base_ten", "money", "phoneme_frame", "place_value", "chart", "function_graph", "geometry", "coordinate_grid", "box_plot", "probability_tree", "atom", "ph_scale", "syllable_split", "onset_rime", "writing_area"
config (required): Kind-specific JSON object (shapes below). Add "logo": true to render the org logo above the visual.
question, answer, hint (optional): let the bot quiz the student on the visual, exactly like an image. Do not set a description — the bot is given one regenerated from the config every lesson, so it always matches what's drawn; and there is no botVisible (a vector/text visual is always legible).
- Returns an image ID — embed it in the content with
::visual{#id} (mirrors ::quiz{#id}). A visual ID can also be passed as a create_quiz image or a update_resource coverImage.
Per-kind config shapes:
// number_line — marks may be numbers or strings; highlights = up to 4 points, auto-coloured red/blue/green/pink in order (legacy single "highlight" still works)
{ "from": 0, "to": 10, "marks": [0, 2, 4, 6, 8, 10], "highlights": [3, 7] }
// fraction — style: "bar" | "circle"; showValue:false hides the n/d label (name-the-fraction quiz)
{ "num": 3, "den": 4, "style": "bar", "showValue": true }
// grid — rows of cells; optional header per row; optional highlighted cells
{ "cols": ["1", "2", "3"], "rows": [{ "header": "x2", "cells": [2, 4, 6] }], "highlight": [{ "row": 0, "col": 1 }] }
// timeline
{ "events": [{ "date": "1969", "label": "Moon landing" }, { "date": "1989", "label": "Web invented" }] }
// math — KaTeX / LaTeX
{ "tex": "\\frac{1}{2} + \\frac{1}{3}" }
// text — font: "beginner" | "comic" | "standard"; inline _u_ *i* **both** [hl] {red:color}. ONE word/phrase/label only — NO newlines or multi-line lists; a literal \n will NOT break the line (use separate text visuals or a grid for several lines)
{ "text": "cat", "color": "#333333", "font": "beginner" }
// counters — a single emoji, count 1-99
{ "emoji": "🍎", "count": 5 }
// clock — 12-hour face; hours 0-23, minutes 0-59; showMinuteTicks optional (default true)
{ "hours": 3, "minutes": 15, "showMinuteTicks": true }
// ten_frame — 0-20 counters in a 2x5 frame (two frames above 10); color optional
{ "count": 7, "color": "#E53935" }
// bar_chart — unit + color optional; showValues:false hides value labels (read heights off the axis) for a quiz
{ "bars": [{ "label": "Cats", "value": 4 }, { "label": "Dogs", "value": 6 }], "unit": "children", "showValues": true }
// base_ten — Dienes blocks for place value, 0-999
{ "value": 124 }
// money — coins/notes in MINOR units (pence/cents); currency GBP|USD|EUR (default GBP); showTotal:false hides the total (adding-up quiz)
{ "coins": [200, 200, 20, 20, 5], "currency": "GBP", "showTotal": true }
// phoneme_frame — one grapheme per sound (phonics sound buttons)
{ "graphemes": ["sh", "i", "p"] }
// place_value — digits in place columns; columns optional (derived if omitted)
{ "value": 3406, "columns": ["Th", "H", "T", "O"] }
// chart — a full Chart.js spec (the most flexible kind). type one of
// bar|line|pie|doughnut|radar|polarArea|scatter|bubble. Pure data only: no URLs, max 50KB.
{ "spec": { "type": "bar", "data": { "labels": ["Jan", "Feb"], "datasets": [{ "label": "Rain", "data": [42, 35] }] }, "options": {} } }
// function_graph — plot y=f(x); explicit * (2*x); funcs sin cos tan sqrt abs exp ln log; consts pi e
{ "functions": ["x^2", "2*x+1"], "xMin": -5, "xMax": 5 }
// geometry — labelled polygon; coords auto-scale; rightAngles = vertex indices
{ "vertices": [{ "x": 0, "y": 0, "label": "A" }, { "x": 4, "y": 0, "label": "B" }, { "x": 0, "y": 3, "label": "C" }], "sideLabels": ["4 cm", "5 cm", "3 cm"], "rightAngles": [0] }
// coordinate_grid — plot points/segments; xMin/xMax/yMin/yMax optional
{ "points": [{ "x": 2, "y": 3, "label": "A" }], "segments": [{ "from": [0, 0], "to": [2, 3] }] }
// box_plot — five-number summary
{ "min": 2, "q1": 5, "median": 7, "q3": 10, "max": 14, "label": "Scores" }
// probability_tree — recursive branches with probabilities
{ "branches": [{ "label": "Red", "prob": "1/2", "branches": [{ "label": "Red", "prob": "1/2" }, { "label": "Blue", "prob": "1/2" }] }] }
// atom — Bohr model; electrons per shell
{ "protons": 11, "neutrons": 12, "electrons": [2, 8, 1] }
// ph_scale — 0-14
{ "value": 7 }
// syllable_split — phonics: one chunk per syllable
{ "syllables": ["but", "ter", "fly"] }
// onset_rime — phonics: onset may be empty (e.g. "at")
{ "onset": "str", "rime": "ing" }
// writing_area — WORKSHEET blank answer space (size: "sentence" | "paragraph" | "multi_paragraph"; label optional). Print-first.
{ "size": "paragraph", "label": "Your answer" }
update_visual:
resourceId (required), imageId (required)
config (optional): replaces the whole config; must match the visual's existing kind (kind is immutable — delete and recreate to change it)
name, question, answer, hint (optional). No description/botVisible — see create_visual.
create_quiz:
resourceId (required): Resource to attach quiz to
question (required): Question text
questionType (required): "single", "multiple", "freetext", "ordered_list", "matching", or "fill_in_blank"
answers (for single/multiple/ordered_list): Array of {id, text, isCorrect}. For matching: {id, text, isCorrect, matchText}
expectedAnswer (for freetext): Correct answer
inputRestriction (for freetext): "text", "integer", "decimal", or "fraction"
numericMin (optional): Minimum allowed value for numeric inputs (integer, decimal, fraction)
numericMax (optional): Maximum allowed value for numeric inputs (integer, decimal, fraction)
allowNegative (optional): Whether negative values are accepted (default: true)
evaluationCriteria (optional): AI grading guidelines
retryLimit (optional): Max attempts
hint (optional): Guidance for wrong answers
description (optional): When the bot should present this quiz
exactMatch (freetext only): grade the answer EXACTLY against expectedAnswer (case + punctuation count) with deterministic feedback, instead of lenient AI grading. This is the flag that turns a freetext quiz into a spelling/dictation quiz — see the recipe below.
image (optional): Illustration image ID — see Quiz Illustrations below. The image must already be attached to the same resource.
Recipe: a spelling or dictation quiz (end-to-end)
A spelling/dictation quiz = the student hears a word or sentence and types it; grading is exact. Build it in three steps:
- Make the audio prompt —
generate_audio({ resourceId, text: "<the word or sentence>", name: "..." }). Returns an image id. The text is the answer spoken aloud; it is never shown to the student and never sent to the bot, so it can't leak.
- Create the quiz —
create_quiz({ resourceId, questionType: "freetext", inputRestriction: "text", exactMatch: true, expectedAnswer: "<the word or sentence>", question: "Listen and type what you hear.", image: <audio id from step 1>, retryLimit: <null for unlimited, or a number> }). expectedAnswer MUST match the audio text exactly (the capitalisation + punctuation you want graded). Leave evaluationCriteria off — exactMatch ignores it.
- Embed it — put
::quiz{#<quiz id>} in the markdown where the dictation should happen.
Notes:
- Words vs sentences both work — for a sentence, include the exact capital letter + full stop in both
text and expectedAnswer; the deterministic feedback calls out "Check your capital letters" / "You forgot the full stop" / "You got N letters wrong".
exactMatch only applies to inputRestriction: "text" (not numeric inputs).
- Use
retryLimit: null (unlimited) for practice; a number to cap attempts.
- To make a whole dictation lesson, repeat steps 1–3 per phrase (one audio + one quiz each), under headings like
## Phrase 1, and the bot reads the instruction, plays each clip, and checks the typed answer.
update_quiz:
resourceId (required): The resource the quiz belongs to
id (required): The quiz ID to update
- All other fields from
create_quiz are optional — omitted fields are left unchanged
- Use this to fix typos, change answers, or attach an illustration image after the image has been generated
- Pass
image: null to remove an existing illustration image
create_course:
name (required): Course name
description (optional): Course description
tags (optional): Array of tags
progressionType (optional): "flexible", "sequential", or "random"
openEnrollment (optional): If true, students can self-enroll from the course catalog
defaultTeacher (optional): User ID of the teacher assigned to self-enrolled students (required when openEnrollment is true)
maxEnrollments (optional): Maximum number of students who can enroll (null = unlimited)
priceCents (optional): Price in cents for paid courses (requires Stripe Connect)
currency (optional): Currency code for paid courses (e.g., "GBP", "USD", "EUR")
teacherOnly (optional): If true, course is only visible to teachers in the catalog (preview mode)
export_resource:
resourceId (required): The resource ID to export
- Returns: Base64-encoded ZIP containing manifest, content, media, and quizzes
import_resource:
zipData (required): Base64-encoded ZIP file data matching the format below
fileName (optional): Filename for the ZIP (default: "import.zip")
- Returns: The newly created resource (with ID and name)
Resource Archive Format (for import/export)
The ZIP file must have all files at the root level (no subdirectory wrapper) with this structure:
manifest.json # Required: metadata and file references
content.md # Required: markdown content with portable tokens
media/ # Media files (images, videos, PDFs)
media-001.png
media-002.jpeg
quizzes/ # Quiz definitions
quiz-001.json
quiz-002.json
CRITICAL: Files must be at the ZIP root — NOT inside a subdirectory. A ZIP containing my-resource/manifest.json will fail; it must be just manifest.json.
manifest.json format
{
"version": 1,
"exportedAt": "2026-01-01T00:00:00.000Z",
"resource": {
"name": "Subtraction on a Number Line",
"tags": ["maths", "subtraction"],
"deliveryMode": "conversation"
},
"media": [
{
"ref": "media-001",
"filename": "media/media-001.png",
"name": "Number Line Diagram",
"mimeType": "image/png",
"description": "A number line showing 15 - 3 = 12",
"question": "Where do we land after 3 hops back from 15?",
"answer": "We land on 12",
"hint": "Count the arrows backwards",
"order": null,
"botVisible": true
}
],
"quizzes": [
{
"ref": "quiz-001",
"filename": "quizzes/quiz-001.json"
}
]
}
Key rules:
version must be 1
media and quizzes are arrays (not objects)
- Each media entry must have a
ref field (e.g. "media-001") matching its portable token in content.md
- Each quiz entry must have a
ref field (e.g. "quiz-001") matching its portable token in content.md
- Media filenames must match their
mimeType extension (e.g. media-001.png for image/png)
- Only
image/png, image/jpeg, and application/pdf are supported for images
content.md portable tokens
Content must use portable media:// and quiz tokens (NOT database IDs):

::quiz{#quiz-001}
The importer replaces these tokens with real database IDs after creating the records.
Quiz JSON format
Each quiz file (e.g. quizzes/quiz-001.json) contains:
{
"question": "What is 15 - 3?",
"questionType": "freetext",
"inputRestriction": "integer",
"expectedAnswer": "12",
"numericMin": 0,
"numericMax": 20,
"allowNegative": false,
"evaluationCriteria": "Accept only the number 12.",
"retryLimit": 3,
"hint": "Start at 15 and count back 3 hops.",
"description": "Quiz after the number line example."
}
create_tag:
name (required): Tag name to create (max 128 chars, lowercase recommended)
list_course_resources:
courseId (required): The course ID to list resources for
- Returns: Ordered list of resources with name, tags, order, and bot details (content stripped to save tokens)
add_resource_to_course:
courseId (required): Course ID
resourceId (required): Resource ID to add
order (optional): Position in the course (important for sequential courses)
bot (optional): Override the course's default bot for this specific resource
deliveryMode (optional): Override the resource's default delivery mode within this course
Organizing Resources into Courses
Courses group related resources together for students to complete. When creating a course, consider:
Progression Types
| Type |
Behavior |
Best For |
| Flexible |
Students complete resources in any order |
Independent topics, reference materials |
| Sequential |
Students must follow the specified order |
Prerequisite content, building-block skills |
| Random |
System selects the next resource |
Practice drills, varied review |
Course Design Tips
- Use sequential progression when earlier resources teach concepts needed for later ones
- Use flexible progression when resources cover independent topics within a theme
- Set resource order carefully for sequential courses - use the
order parameter in add_resource_to_course
- Override the bot for specific resources if a different AI persona or expertise is needed
- Override delivery mode per resource if some topics work better as conversation, presentation, or worksheet
- Keep courses focused - 3-8 resources per course is a good range
- Use consistent tags across resources and courses for easy organization
Open Enrollment
Enable open enrollment to let students self-enroll from the course catalog:
- Requires a
defaultTeacher to be assigned for managing self-enrolled students
- Optionally set
maxEnrollments to cap capacity
- Set
teacherOnly: true to preview the course before releasing to students
Paid Courses
If the organization has Stripe Connect configured:
- Set
priceCents and currency to create a paid course
- Open enrollment must be enabled for paid courses
- Students go through Stripe checkout and are automatically enrolled after payment
CRITICAL: Embedding Media and Quizzes in Resource Content
After creating images and quizzes, you MUST update the resource content to include references to them. Without these references, the bot will NOT display the media or quizzes during delivery.
Image Reference Syntax

Example: If create_image returns { id: 156 }, insert:

Quiz Reference Syntax
::quiz{#QUIZ_ID}
Example: If create_quiz returns { id: 28 }, insert:
::quiz{#28}
Visual Reference Syntax
::visual{#VISUAL_ID}
Example: If create_visual returns { id: 91 }, insert:
::visual{#91}
The bot displays the visual via display_media when it reaches this point in the prose — exactly like images and quizzes.
Complete Example
# Step 1: Create resource
create_resource(
name: "Introduction to Photosynthesis",
content: "# Learning Objectives\n\nPlaceholder content...",
tags: ["science", "biology"],
deliveryMode: "conversation"
)
# Response: { id: 42, ... }
# Step 2: Upload image from URL
upload_image_from_url(
resourceId: 42,
url: "https://example.com/plant-cell.png",
name: "Plant Cell Diagram",
description: "Diagram of a plant cell showing chloroplasts",
question: "Where does photosynthesis occur?",
answer: "In the chloroplasts",
hint: "Look for the green organelles",
botVisible: true
)
# Response: { id: 156, ... }
# Step 3: Create quiz
create_quiz(
resourceId: 42,
description: "Test understanding of photosynthesis location",
question: "In which organelle does photosynthesis take place?",
questionType: "single",
answers: [
{ id: "a", text: "Mitochondria", isCorrect: false },
{ id: "b", text: "Chloroplast", isCorrect: true },
{ id: "c", text: "Nucleus", isCorrect: false }
],
retryLimit: 2,
hint: "It contains chlorophyll, which is green"
)
# Response: { id: 28, ... }
# Step 4: CRITICAL - Update resource with image and quiz references
update_resource(
id: 42,
content: "# Learning Objectives\n\nBy the end of this resource, the student should understand photosynthesis.\n\n## What is Photosynthesis?\n\nExplain that plants convert sunlight into energy. Show the diagram:\n\n\n\nDiscuss the diagram with the student.\n\n## Check Understanding\n\nNow test the student's knowledge:\n\n::quiz{#28}\n\n## Summary\n\nReview the key points."
)
Quiz Types
Single Choice
{
"question": "What gas do plants absorb?",
"questionType": "single",
"answers": [
{ "id": "a", "text": "Oxygen", "isCorrect": false },
{ "id": "b", "text": "Carbon dioxide", "isCorrect": true }
],
"hint": "Think about what humans breathe out"
}
Multiple Choice
{
"question": "Select ALL ingredients for photosynthesis",
"questionType": "multiple",
"answers": [
{ "id": "a", "text": "Sunlight", "isCorrect": true },
{ "id": "b", "text": "Water", "isCorrect": true },
{ "id": "c", "text": "Oxygen", "isCorrect": false }
]
}
Open Answer
{
"question": "Calculate 15% of 80",
"questionType": "freetext",
"inputRestriction": "decimal",
"expectedAnswer": "12",
"evaluationCriteria": "Accept 12, 12.0, or 12.00"
}
Fraction
{
"question": "What is 1/2 + 1/6?",
"questionType": "freetext",
"inputRestriction": "fraction",
"expectedAnswer": "2/3",
"numericMin": 0,
"numericMax": 1,
"allowNegative": false,
"evaluationCriteria": "Accept equivalent fractions (e.g. 4/6, 8/12)"
}
Ordered List
Students drag and drop items into the correct sequence. The answers array order defines the correct sequence — students see items shuffled.
{
"question": "Put these in order from smallest to largest",
"questionType": "ordered_list",
"answers": [
{ "id": "a", "text": "1/2", "isCorrect": false },
{ "id": "b", "text": "0.55", "isCorrect": false },
{ "id": "c", "text": "3/5", "isCorrect": false },
{ "id": "d", "text": "62%", "isCorrect": false }
],
"hint": "Convert everything to decimals to compare"
}
Note: isCorrect is ignored for ordered_list — the array order IS the correct answer. Minimum 2 items, maximum 10.
Matching
Students match items from two columns. The left column shows fixed terms, the right column shows shuffled matches that students drag to align correctly.
{
"question": "Match each part of speech to its example",
"questionType": "matching",
"answers": [
{ "id": "a", "text": "verb", "isCorrect": false, "matchText": "jump" },
{ "id": "b", "text": "noun", "isCorrect": false, "matchText": "book" },
{ "id": "c", "text": "adverb", "isCorrect": false, "matchText": "quickly" }
],
"hint": "Think about what each word does in a sentence"
}
Note: isCorrect is ignored for matching — correct state is when each answer's matchText is aligned next to its text. Each answer needs both text (term) and matchText (match). Minimum 2 pairs, maximum 10.
Fill in the Blank
Students fill in missing words within a sentence. Supports free text input or word bank (dropdown) mode.
{
"question": "Water freezes at [] degrees Celsius and boils at [] degrees Celsius",
"questionType": "fill_in_blank",
"answers": [
{ "id": "a", "text": "0", "isCorrect": true },
{ "id": "b", "text": "100", "isCorrect": true }
],
"hint": "Think about the states of water"
}
For word bank mode (dropdown with distractors), set inputRestriction to "word_bank" and add distractor answers with isCorrect: false:
{
"question": "The [] sat on the []",
"questionType": "fill_in_blank",
"inputRestriction": "word_bank",
"answers": [
{ "id": "a", "text": "cat", "isCorrect": true },
{ "id": "b", "text": "mat", "isCorrect": true },
{ "id": "c", "text": "dog", "isCorrect": false },
{ "id": "d", "text": "hat", "isCorrect": false }
]
}
Note: Use [] in the question to mark blank positions. Correct answers (isCorrect: true) map in order to blanks. Distractors (isCorrect: false) appear as extra options in word bank mode. Minimum 1 blank.
Quiz Illustrations
A quiz can have an illustration image attached to it (separate from the resource's inline images and cover image). The illustration appears alongside the question, on the same screen, when the quiz is shown to the student.
This is the preferred way to give a quiz a supporting figure. If a question refers to a diagram — "find the missing side of this triangle", "label the parts of this cell", "which angle is x?" — attach that figure to the quiz via its image field. Because the picture and the question render together and stay together, the student can read the diagram while they answer. Do not instead drop the figure inline in the content just before ::quiz{#id}: the bot shows an inline image earlier, as it narrates that part of the lesson, so by the time the student is answering the quiz the picture may no longer be on screen. A quiz that depends on a figure should own that figure.
A quiz illustration can be any image kind — an uploaded/AI image, a create_svg drawing, or a create_visual visual tool (pass the visual's id as the quiz image). For maths/science diagrams, prefer a create_visual (e.g. geometry, coordinate_grid, function_graph) or a create_svg so it stays crisp.
Quiz images are not embedded in the resource markdown — they are linked directly to the quiz record via the image field. The resource content does NOT need a  reference for quiz illustrations.
Attaching an illustration
The image must already be attached to the same resource as the quiz. The normal flow is:
# Step 1: Generate or upload the illustration, attached to the resource
generate_image(resourceId: 42, prompt: "A cross-section of a leaf showing chloroplasts")
# → { id: 177, ... }
# Step 2a: Create the quiz with the image attached
create_quiz(
resourceId: 42,
question: "Which labelled part contains the chloroplasts?",
questionType: "freetext",
expectedAnswer: "The palisade mesophyll",
image: 177
)
# OR Step 2b: Create the quiz first, then attach the image later
create_quiz(resourceId: 42, question: "...", questionType: "single", answers: [...])
# → { id: 99, ... }
update_quiz(resourceId: 42, id: 99, image: 177)
When to use a quiz illustration vs. an inline image
…(truncated)
1---2name: create-resource3description: Create educational content (resources and courses) for the Beau platform. Use when the user wants to create resources, courses, quizzes, or upload images/PDFs for voice delivery.4---56# Create Resource Skill78This skill helps you create educational resources for the Beau platform. Resources are the building blocks of your curriculum - each is a self-contained learning unit delivered to students as a voice conversation, a narrated presentation, or a no-bot worksheet (self-paced screens / printable handout written for the student). Resources are grouped into courses.910## Reference guides1112This skill folder ships two markdown guides. They have different audiences (see each file's `audience` frontmatter):1314- **`resource-creation-guide.md`** (`audience: assistant`) — your **authoring reference**. Follow it when designing and writing resources.15- **`teacher-guide.md`** (`audience: human-ui`) — **end-user documentation** for the Beau dashboard UI, written for human teachers. Useful background for terminology and workflow, but it is **not** authoring rules — don't treat its UI walkthroughs as instructions to you. (The repo's top-level `guides/` folder holds the human-facing admin and student guides; those are not part of this skill.)1617## Instructions for Claude1819When this skill is invoked, follow these steps:20210. **Preflight Check**: Verify the beaubot MCP server is connected by calling `list_tags(pageSize: 1)`. If this fails with a tool error, tell the user: "The Beau MCP server is not connected. Please check your MCP connection and authenticate if prompted." Then stop.22231. **Tag Governance** (before creating anything):24 a. Call `list_tags(pageSize: 50)` to fetch the full tag catalog25 b. Reuse existing tags where possible — do not create near-duplicates (e.g. "maths" vs "math")26 c. For genuinely new tags: ask the user to confirm, then call `create_tag(name)` to add them27 d. Only use tags that exist in the catalog when calling `create_resource`28291. **Gather Requirements**: Ask the user about:30 - Topic/subject for the resource31 - Target audience (age, skill level)32 - Delivery mode preference: conversation (two-way voice), presentation (one-way narration), or worksheet (no bot — self-paced screens / printable, written FOR THE STUDENT). This changes who the content is written for: conversation/presentation are written for the bot; a worksheet is written directly to the student.33 - Desired length (default: 5-10 minutes)34 - Any specific learning objectives35 - Whether they want AI-generated images or have URLs to images/PDFs36372. **Design the Resource**: Based on the guides, create a structure with:38 - Clear learning objectives39 - Logical sections with headings40 - **A quiz after every major concept or section** — aim for at least 2-3 quizzes per resource. Quizzes are the primary way students actively engage with the material, and lessons without them feel passive and text-heavy.41 - **At least 1-2 images per resource** — visuals break up text, illustrate concepts, and give the bot something concrete to discuss with the student. A resource with no images is almost always too text-heavy.42 - Mix quiz types for variety (single choice, multiple choice, open answer, fraction, ordered list, matching)43443. **Create Content**: Draft the markdown content following best practices:45 - Write instructions for the bot, not a script46 - Use "the student" with they/them pronouns47 - Keep sections digestible48 - **IMPORTANT: Avoid text-heavy resources.** Every section should either have a quiz, an image, or both. If a section is just text with no interactivity, consider adding a quick quiz to check understanding or an image to illustrate the concept. Students learn best when they actively participate, not passively listen.49 - **If a quiz refers to a figure, attach that figure to the quiz** (its `image` field), don't just place it inline before `::quiz{#id}`. Then the diagram and the question are on screen together while the student answers. See *Quiz Illustrations*.50514. **Source Media**: Find or create visuals for the resource. **Every resource should have images** — they are essential for engagement, not optional decoration.52 - **Do NOT generate images via Python/code** — this is too slow and produces poor results53 - **Use the `text` visual** (`create_visual` with `kind: "text"`) for vocabulary cards, sight words, labels and key terms, and the **`math` visual** for equations — these are instant, vector (crisp at any size), and teacher-editable. **`create_text_image` is deprecated — never use it.**54 - **Use `generate_image`** only for custom educational illustrations / photoreal scenes — server-side AI generation; slower, so reserve it for when a real raster picture is genuinely needed55 - **Use `upload_image_from_url`** for existing public images (Wikimedia, educational sites)56 - **Use `create_image` (base64) only as a last resort** for user-provided local files57 - **Verify every image**: After generation/upload, view the image to confirm it matches intent58 - Write description/question/answer/hint based on what the image ACTUALLY shows, not what you intended59 - For PDFs: description is critical since the bot cannot read PDF content60615. **Execute Creation**: Use MCP tools to:62 - Create the resource via `create_resource`63 - Upload any media files via `upload_image_from_url` or `create_image`64 - Create quizzes via `create_quiz`65 - **CRITICAL**: Update resource content via `update_resource` to embed image and quiz references using the correct markdown syntax (see below)66 - **Set a cover image**: pick the most representative image for the resource and set it via `update_resource(id, coverImage: imageId)`. Cover images are shown to students when the lesson starts and make the catalog feel less like a wall of text. Do this in the same `update_resource` call as the content update.67 - Optionally create a course and add the resource to it68696. **Provide Testing Instructions**: Tell the user how to test their resource using the Test Resource button in the admin UI7071## MCP Tools7273Use the `beaubot` MCP server tools:7475### Available Tools7677| Tool | Description |78|------|-------------|79| `list_resources` | List existing resources with optional tag filtering |80| `get_resource` | Fetch a resource by ID |81| `create_resource` | Create a new resource |82| `update_resource` | Update an existing resource |83| `get_image` | Download an image by ID — returns the image visually (for inspection) plus metadata |84| `generate_image` | Generate an AI image from a text prompt and attach to a resource (uses org's OpenAI key, preferred) |85| `create_text_image` | **DEPRECATED — do not use.** For words/phrases/labels use the `text` visual (`create_visual` with `kind: "text"`) — vector, instant, editable. |86| `create_visual` | Create a teacher-authored visual tool — number line, fraction, grid, timeline, math equation, word/text card, or counters — from a small JSON config (no AI, renders as crisp SVG). Embed with `::visual{#id}` |87| `update_visual` | Update an existing visual tool's config or metadata |88| `create_svg` | Draw a scalable SVG image from raw markup (crisp, responsive, no AI). Best when no `create_visual` kind fits but it can be drawn with shapes/lines/text. Embed with `` |89| `upload_image_from_url` | Upload an image or PDF from a URL to a resource |90| `prepare_image_upload` | Mint a single-use multipart upload URL for a local file (preferred over `create_image` for files on disk) |91| `create_image` | Upload an image or PDF (base64 data) to a resource (last resort) |92| `generate_audio` | Generate a spoken clip (text-to-speech) and attach as audio media. For listening or **spelling/dictation**: attach to a freetext quiz illustration (with `exactMatch: true`) so the student hears it and types the answer. The spoken text isn't shown — keep the answer in `expectedAnswer`. |93| `create_quiz` | Create a quiz for a resource. For **spelling/dictation**, use `questionType: "freetext"` + `exactMatch: true` (deterministic case/punctuation grading) + attach a `generate_audio` clip as the `image`. |94| `update_quiz` | Update an existing quiz (fix typos, attach an illustration image, set `exactMatch`, etc.) |95| `list_tags` | List available tags in the organization's catalog |96| `create_tag` | Create a new tag in the organization's tag catalog |97| `list_bots` | List the org's bots with their voice, avatar, and persona — call to pick a bot whose persona suits the subject |98| `get_bot` | Fetch a single bot's full config (including system prompt) by ID |99| `export_resource` | Export a resource as a base64-encoded ZIP (includes all media and quizzes) |100| `import_resource` | Import a resource from a base64-encoded ZIP (creates a new resource) |101| `create_course` | Create a new course (collection of resources) |102| `list_courses` | List existing courses |103| `add_resource_to_course` | Add a resource to a course |104| `list_course_resources` | List resources already in a course (with details) |105106### Choosing how to show something visual107108When a lesson needs a picture or diagram, pick the **most specific** option — in this order. Going higher up the list gives crisper, more responsive, more editable results, so don't drop to a raster image out of habit.1091101. **A dedicated visual tool — `create_visual` (best).** If the concept maps to a built-in kind, use it: number line, fraction, grid, timeline, math (KaTeX), word/text, counters, clock, ten frame, bar chart, base-ten blocks, money, phoneme frame, place value, **Chart.js chart**, function graph, geometry, coordinate grid, box plot, probability tree, atom (Bohr), pH scale, syllable split, onset/rime. These are purpose-built, teacher-editable, and render responsively. For arbitrary data charts the `chart` kind (Chart.js) covers bar/line/pie/scatter/etc.1112. **A hand-drawn SVG — `create_svg` (good fallback).** If **no** visual kind fits but the thing can be expressed with shapes, lines and text — a labelled diagram, a custom illustration, a flowchart, a simple map, a process diagram — write an SVG. It stays sharp at any size and is lightweight. **Prefer this over a raster image.**1123. **A raster image (last resort).** Only when you truly need a *photograph* or a richly detailed picture that can't be vector-drawn: `generate_image` (AI) or `prepare_image_upload` / `create_image` (an existing file). Supported raster uploads: PNG and JPEG. (SVG uploads are supported too, but use `create_svg` rather than uploading SVG by hand.)113114All of these embed the same way in the content — `` for images and SVGs, `::visual{#ID}` for visual tools.115116### Workflow117118```1190. list_tags(pageSize: 50) → Fetch org's tag catalog120 create_tag(name) → Create new tags (after user confirmation)1211221. create_resource(name, content, tags, deliveryMode)123 → Returns resource with ID (tags must come from the catalog)1241252. create_visual(resourceId, kind, config)126 → Vector visual — use kind "text" for words/labels/vocabulary, "math" for equations,127 or any other kind for diagrams. Instant, editable. PREFERRED for words. Embed with ::visual{#id}.128 OR129 generate_image(resourceId, prompt, description, question, answer, hint, botVisible)130 → AI-generated image — only for photoreal illustrations/scenes131 OR132 upload_image_from_url(resourceId, url, description, question, answer, hint, botVisible)133 → Downloads and attaches image from URL134 OR (for a file on local disk)135 prepare_image_upload(resourceId) → { uploadUrl }; then `curl -F file=@/path "$uploadUrl"`136 → Single-use multipart upload, no base64 round-trip137 OR (last resort)138 create_image(resourceId, name, mimeType, data, description, question, answer, hint, botVisible)139 → Uploads base64-encoded image1401412b. create_visual(resourceId, kind, config, ...)142 → Authored visual tool (number line, fraction, grid, timeline, math, text, counters)143 → Renders as crisp SVG; embed in content with ::visual{#id}. Prefer this over an image144 whenever the content is structured data the platform can draw.1451463. create_quiz(resourceId, question, questionType, answers, image?, ...)147 → Returns quiz with ID148 → Pass image: <imageId> to attach an illustration (image must be on the same resource)149 OR150 update_quiz(resourceId, id, image?, ...)151 → Update an existing quiz — fix typos or attach an illustration after generating it1521534. update_resource(id, content, coverImage?)154 → CRITICAL: Update content to include image and quiz references155 → Optionally set coverImage to an image ID to give the resource a cover1561575. (Optional) create_course(name, description, tags, progressionType)158 → Returns course with ID1591606. (Optional) list_course_resources(courseId)161 → View existing resources in a course before adding1621637. (Optional) add_resource_to_course(courseId, resourceId, order)164 → Adds resource to the course1651668. (Optional) export_resource(resourceId)167 → Returns base64-encoded ZIP for backup or sharing1681699. (Optional) import_resource(zipData)170 → Creates a new resource from a previously exported ZIP171```172173### Tool Parameters174175**get_image:**176- `imageId` (required): The image ID to download177- Returns: For images (`image/png`, `image/jpeg`), returns the image visually plus a text label. For PDFs/videos, returns metadata only:178 ```json179 {180 "id": 42,181 "name": "dog-breeds.png",182 "mimeType": "image/png",183 "description": "Common dog breeds",184 "question": "Can you identify these breeds?",185 "answer": "Labrador, Poodle, German Shepherd",186 "hint": "Look at the ear shapes",187 "botVisible": true,188 "resource": 15,189 "organization": 3,190 "createdAt": "2026-03-15T10:30:00.000Z",191 "updatedAt": "2026-03-15T10:30:00.000Z"192 }193 ```194195**create_resource:**196- `name` (required): Display name for the resource197- `content` (required): Markdown content198- `tags` (optional): Array of tags — must come from the tag catalog (call `list_tags` first, use `create_tag` for new ones)199- `deliveryMode` (optional): `"conversation"`, `"presentation"`, or `"worksheet"`200201**update_resource:**202- `id` (required): The resource ID to update203- `name` (optional): New name204- `content` (optional): New markdown content205- `tags` (optional): New tags (replaces existing — must come from the catalog)206- `deliveryMode` (optional): `"conversation"`, `"presentation"`, or `"worksheet"`207- `coverImage` (optional): Image ID to use as the resource cover (shown to students when the lesson starts). The image must already be attached to the resource (use `create_visual`, `generate_image`, `upload_image_from_url`, `prepare_image_upload` + curl, or `create_image` first, then pass the returned ID here). Pass `null` to remove the cover.208209**generate_image** (preferred for custom illustrations):210- `resourceId` (required): Resource to attach the image to211- `prompt` (required): Text prompt describing the educational image to generate (max 4000 chars)212- `description` (optional): What the image shows (important for bot)213- `question` (optional): Question to ask about the image214- `answer` (optional): Expected answer215- `hint` (optional): Help for students216- `botVisible` (optional): If true, bot can see the image217218**create_text_image** — **DEPRECATED. Do not use.** To render a word, phrase, label or key219term, use the `text` visual instead: `create_visual(resourceId, "text", { text: "...", … })`. It220renders the same inline formatting (`_underline_`, `*italic*`, `**bold**`, `[highlight]`,221`{red:colored}`), is vector instead of a raster PNG, is teacher-editable, and supports the org logo.222223**upload_image_from_url** (preferred for remote use):224- `resourceId` (required): Resource to attach image to225- `url` (required): Public URL of the image to download226- `name` (required): Image filename227- `description` (optional): What the image shows (important for bot)228- `question` (optional): Question to ask about the image229- `answer` (optional): Expected answer230- `hint` (optional): Help for students231- `botVisible` (optional): If true, bot can see the image232233**create_image:**234- `resourceId` (required): Resource to attach image to235- `name` (required): Image filename236- `mimeType` (required): `"image/png"`, `"image/jpeg"`, `"image/svg+xml"`, or `"application/pdf"`. For SVG, prefer the dedicated `create_svg` tool (it takes raw markup, no base64).237- `data` (required): Base64-encoded image data238- `description` (optional): What the image shows (important for bot)239- `question` (optional): Question to ask about the image240- `answer` (optional): Expected answer241- `hint` (optional): Help for students242- `botVisible` (optional): If true, bot can see the image243244**create_visual** (authored visual tool — renders as crisp SVG, no AI):245- `resourceId` (required): Resource to attach the visual to246- `kind` (required): One of `"number_line"`, `"fraction"`, `"grid"`, `"timeline"`, `"math"`, `"text"`, `"counters"`, `"clock"`, `"ten_frame"`, `"bar_chart"`, `"base_ten"`, `"money"`, `"phoneme_frame"`, `"place_value"`, `"chart"`, `"function_graph"`, `"geometry"`, `"coordinate_grid"`, `"box_plot"`, `"probability_tree"`, `"atom"`, `"ph_scale"`, `"syllable_split"`, `"onset_rime"`, `"writing_area"`247- `config` (required): Kind-specific JSON object (shapes below). Add `"logo": true` to render the org logo above the visual.248- `question`, `answer`, `hint` (optional): let the bot quiz the student on the visual, exactly like an image. **Do not set a description** — the bot is given one regenerated from the `config` every lesson, so it always matches what's drawn; and there is no `botVisible` (a vector/text visual is always legible).249- Returns an image ID — **embed it in the content with `::visual{#id}`** (mirrors `::quiz{#id}`). A visual ID can also be passed as a `create_quiz` `image` or a `update_resource` `coverImage`.250251Per-`kind` `config` shapes:252```jsonc253// number_line — marks may be numbers or strings; highlights = up to 4 points, auto-coloured red/blue/green/pink in order (legacy single "highlight" still works)254{ "from": 0, "to": 10, "marks": [0, 2, 4, 6, 8, 10], "highlights": [3, 7] }255// fraction — style: "bar" | "circle"; showValue:false hides the n/d label (name-the-fraction quiz)256{ "num": 3, "den": 4, "style": "bar", "showValue": true }257// grid — rows of cells; optional header per row; optional highlighted cells258{ "cols": ["1", "2", "3"], "rows": [{ "header": "x2", "cells": [2, 4, 6] }], "highlight": [{ "row": 0, "col": 1 }] }259// timeline260{ "events": [{ "date": "1969", "label": "Moon landing" }, { "date": "1989", "label": "Web invented" }] }261// math — KaTeX / LaTeX262{ "tex": "\\frac{1}{2} + \\frac{1}{3}" }263// text — font: "beginner" | "comic" | "standard"; inline _u_ *i* **both** [hl] {red:color}. ONE word/phrase/label only — NO newlines or multi-line lists; a literal \n will NOT break the line (use separate text visuals or a grid for several lines)264{ "text": "cat", "color": "#333333", "font": "beginner" }265// counters — a single emoji, count 1-99266{ "emoji": "🍎", "count": 5 }267// clock — 12-hour face; hours 0-23, minutes 0-59; showMinuteTicks optional (default true)268{ "hours": 3, "minutes": 15, "showMinuteTicks": true }269// ten_frame — 0-20 counters in a 2x5 frame (two frames above 10); color optional270{ "count": 7, "color": "#E53935" }271// bar_chart — unit + color optional; showValues:false hides value labels (read heights off the axis) for a quiz272{ "bars": [{ "label": "Cats", "value": 4 }, { "label": "Dogs", "value": 6 }], "unit": "children", "showValues": true }273// base_ten — Dienes blocks for place value, 0-999274{ "value": 124 }275// money — coins/notes in MINOR units (pence/cents); currency GBP|USD|EUR (default GBP); showTotal:false hides the total (adding-up quiz)276{ "coins": [200, 200, 20, 20, 5], "currency": "GBP", "showTotal": true }277// phoneme_frame — one grapheme per sound (phonics sound buttons)278{ "graphemes": ["sh", "i", "p"] }279// place_value — digits in place columns; columns optional (derived if omitted)280{ "value": 3406, "columns": ["Th", "H", "T", "O"] }281// chart — a full Chart.js spec (the most flexible kind). type one of282// bar|line|pie|doughnut|radar|polarArea|scatter|bubble. Pure data only: no URLs, max 50KB.283{ "spec": { "type": "bar", "data": { "labels": ["Jan", "Feb"], "datasets": [{ "label": "Rain", "data": [42, 35] }] }, "options": {} } }284// function_graph — plot y=f(x); explicit * (2*x); funcs sin cos tan sqrt abs exp ln log; consts pi e285{ "functions": ["x^2", "2*x+1"], "xMin": -5, "xMax": 5 }286// geometry — labelled polygon; coords auto-scale; rightAngles = vertex indices287{ "vertices": [{ "x": 0, "y": 0, "label": "A" }, { "x": 4, "y": 0, "label": "B" }, { "x": 0, "y": 3, "label": "C" }], "sideLabels": ["4 cm", "5 cm", "3 cm"], "rightAngles": [0] }288// coordinate_grid — plot points/segments; xMin/xMax/yMin/yMax optional289{ "points": [{ "x": 2, "y": 3, "label": "A" }], "segments": [{ "from": [0, 0], "to": [2, 3] }] }290// box_plot — five-number summary291{ "min": 2, "q1": 5, "median": 7, "q3": 10, "max": 14, "label": "Scores" }292// probability_tree — recursive branches with probabilities293{ "branches": [{ "label": "Red", "prob": "1/2", "branches": [{ "label": "Red", "prob": "1/2" }, { "label": "Blue", "prob": "1/2" }] }] }294// atom — Bohr model; electrons per shell295{ "protons": 11, "neutrons": 12, "electrons": [2, 8, 1] }296// ph_scale — 0-14297{ "value": 7 }298// syllable_split — phonics: one chunk per syllable299{ "syllables": ["but", "ter", "fly"] }300// onset_rime — phonics: onset may be empty (e.g. "at")301{ "onset": "str", "rime": "ing" }302// writing_area — WORKSHEET blank answer space (size: "sentence" | "paragraph" | "multi_paragraph"; label optional). Print-first.303{ "size": "paragraph", "label": "Your answer" }304```305306**update_visual:**307- `resourceId` (required), `imageId` (required)308- `config` (optional): replaces the whole config; must match the visual's existing `kind` (kind is immutable — delete and recreate to change it)309- `name`, `question`, `answer`, `hint` (optional). No `description`/`botVisible` — see create_visual.310311**create_quiz:**312- `resourceId` (required): Resource to attach quiz to313- `question` (required): Question text314- `questionType` (required): `"single"`, `"multiple"`, `"freetext"`, `"ordered_list"`, `"matching"`, or `"fill_in_blank"`315- `answers` (for single/multiple/ordered_list): Array of `{id, text, isCorrect}`. For matching: `{id, text, isCorrect, matchText}`316- `expectedAnswer` (for freetext): Correct answer317- `inputRestriction` (for freetext): `"text"`, `"integer"`, `"decimal"`, or `"fraction"`318- `numericMin` (optional): Minimum allowed value for numeric inputs (integer, decimal, fraction)319- `numericMax` (optional): Maximum allowed value for numeric inputs (integer, decimal, fraction)320- `allowNegative` (optional): Whether negative values are accepted (default: true)321- `evaluationCriteria` (optional): AI grading guidelines322- `retryLimit` (optional): Max attempts323- `hint` (optional): Guidance for wrong answers324- `description` (optional): When the bot should present this quiz325- `exactMatch` (freetext only): grade the answer EXACTLY against `expectedAnswer` (case + punctuation count) with deterministic feedback, instead of lenient AI grading. **This is the flag that turns a freetext quiz into a spelling/dictation quiz** — see the recipe below.326- `image` (optional): Illustration image ID — see **Quiz Illustrations** below. The image must already be attached to the same resource.327328#### Recipe: a spelling or dictation quiz (end-to-end)329330A spelling/dictation quiz = the student **hears** a word or sentence and **types** it; grading is exact. Build it in three steps:3313321. **Make the audio prompt** — `generate_audio({ resourceId, text: "<the word or sentence>", name: "..." })`. Returns an image id. The `text` is the answer spoken aloud; it is never shown to the student and never sent to the bot, so it can't leak.3332. **Create the quiz** — `create_quiz({ resourceId, questionType: "freetext", inputRestriction: "text", exactMatch: true, expectedAnswer: "<the word or sentence>", question: "Listen and type what you hear.", image: <audio id from step 1>, retryLimit: <null for unlimited, or a number> })`. `expectedAnswer` MUST match the audio text exactly (the capitalisation + punctuation you want graded). Leave `evaluationCriteria` off — `exactMatch` ignores it.3343. **Embed it** — put `::quiz{#<quiz id>}` in the markdown where the dictation should happen.335336Notes:337- **Words vs sentences** both work — for a sentence, include the exact capital letter + full stop in both `text` and `expectedAnswer`; the deterministic feedback calls out "Check your capital letters" / "You forgot the full stop" / "You got N letters wrong".338- `exactMatch` only applies to `inputRestriction: "text"` (not numeric inputs).339- Use `retryLimit: null` (unlimited) for practice; a number to cap attempts.340- To make a whole **dictation lesson**, repeat steps 1–3 per phrase (one audio + one quiz each), under headings like `## Phrase 1`, and the bot reads the instruction, plays each clip, and checks the typed answer.341342**update_quiz:**343- `resourceId` (required): The resource the quiz belongs to344- `id` (required): The quiz ID to update345- All other fields from `create_quiz` are optional — omitted fields are left unchanged346- Use this to fix typos, change answers, or attach an illustration image after the image has been generated347- Pass `image: null` to remove an existing illustration image348349**create_course:**350- `name` (required): Course name351- `description` (optional): Course description352- `tags` (optional): Array of tags353- `progressionType` (optional): `"flexible"`, `"sequential"`, or `"random"`354- `openEnrollment` (optional): If true, students can self-enroll from the course catalog355- `defaultTeacher` (optional): User ID of the teacher assigned to self-enrolled students (required when openEnrollment is true)356- `maxEnrollments` (optional): Maximum number of students who can enroll (null = unlimited)357- `priceCents` (optional): Price in cents for paid courses (requires Stripe Connect)358- `currency` (optional): Currency code for paid courses (e.g., `"GBP"`, `"USD"`, `"EUR"`)359- `teacherOnly` (optional): If true, course is only visible to teachers in the catalog (preview mode)360361**export_resource:**362- `resourceId` (required): The resource ID to export363- Returns: Base64-encoded ZIP containing manifest, content, media, and quizzes364365**import_resource:**366- `zipData` (required): Base64-encoded ZIP file data matching the format below367- `fileName` (optional): Filename for the ZIP (default: `"import.zip"`)368- Returns: The newly created resource (with ID and name)369370### Resource Archive Format (for import/export)371372The ZIP file **must** have all files at the root level (no subdirectory wrapper) with this structure:373374```375manifest.json # Required: metadata and file references376content.md # Required: markdown content with portable tokens377media/ # Media files (images, videos, PDFs)378 media-001.png379 media-002.jpeg380quizzes/ # Quiz definitions381 quiz-001.json382 quiz-002.json383```384385**CRITICAL**: Files must be at the ZIP root — NOT inside a subdirectory. A ZIP containing `my-resource/manifest.json` will fail; it must be just `manifest.json`.386387#### manifest.json format388389```json390{391 "version": 1,392 "exportedAt": "2026-01-01T00:00:00.000Z",393 "resource": {394 "name": "Subtraction on a Number Line",395 "tags": ["maths", "subtraction"],396 "deliveryMode": "conversation"397 },398 "media": [399 {400 "ref": "media-001",401 "filename": "media/media-001.png",402 "name": "Number Line Diagram",403 "mimeType": "image/png",404 "description": "A number line showing 15 - 3 = 12",405 "question": "Where do we land after 3 hops back from 15?",406 "answer": "We land on 12",407 "hint": "Count the arrows backwards",408 "order": null,409 "botVisible": true410 }411 ],412 "quizzes": [413 {414 "ref": "quiz-001",415 "filename": "quizzes/quiz-001.json"416 }417 ]418}419```420421Key rules:422- `version` must be `1`423- `media` and `quizzes` are **arrays** (not objects)424- Each media entry must have a `ref` field (e.g. `"media-001"`) matching its portable token in content.md425- Each quiz entry must have a `ref` field (e.g. `"quiz-001"`) matching its portable token in content.md426- Media filenames must match their `mimeType` extension (e.g. `media-001.png` for `image/png`)427- Only `image/png`, `image/jpeg`, and `application/pdf` are supported for images428429#### content.md portable tokens430431Content must use portable `media://` and `quiz` tokens (NOT database IDs):432433```markdown434435436::quiz{#quiz-001}437```438439The importer replaces these tokens with real database IDs after creating the records.440441#### Quiz JSON format442443Each quiz file (e.g. `quizzes/quiz-001.json`) contains:444445```json446{447 "question": "What is 15 - 3?",448 "questionType": "freetext",449 "inputRestriction": "integer",450 "expectedAnswer": "12",451 "numericMin": 0,452 "numericMax": 20,453 "allowNegative": false,454 "evaluationCriteria": "Accept only the number 12.",455 "retryLimit": 3,456 "hint": "Start at 15 and count back 3 hops.",457 "description": "Quiz after the number line example."458}459```460461**create_tag:**462- `name` (required): Tag name to create (max 128 chars, lowercase recommended)463464**list_course_resources:**465- `courseId` (required): The course ID to list resources for466- Returns: Ordered list of resources with name, tags, order, and bot details (content stripped to save tokens)467468**add_resource_to_course:**469- `courseId` (required): Course ID470- `resourceId` (required): Resource ID to add471- `order` (optional): Position in the course (important for sequential courses)472- `bot` (optional): Override the course's default bot for this specific resource473- `deliveryMode` (optional): Override the resource's default delivery mode within this course474475## Organizing Resources into Courses476477Courses group related resources together for students to complete. When creating a course, consider:478479### Progression Types480481| Type | Behavior | Best For |482|------|----------|----------|483| **Flexible** | Students complete resources in any order | Independent topics, reference materials |484| **Sequential** | Students must follow the specified order | Prerequisite content, building-block skills |485| **Random** | System selects the next resource | Practice drills, varied review |486487### Course Design Tips488489- **Use sequential progression** when earlier resources teach concepts needed for later ones490- **Use flexible progression** when resources cover independent topics within a theme491- **Set resource order** carefully for sequential courses - use the `order` parameter in `add_resource_to_course`492- **Override the bot** for specific resources if a different AI persona or expertise is needed493- **Override delivery mode** per resource if some topics work better as conversation, presentation, or worksheet494- **Keep courses focused** - 3-8 resources per course is a good range495- **Use consistent tags** across resources and courses for easy organization496497### Open Enrollment498499Enable open enrollment to let students self-enroll from the course catalog:500- Requires a `defaultTeacher` to be assigned for managing self-enrolled students501- Optionally set `maxEnrollments` to cap capacity502- Set `teacherOnly: true` to preview the course before releasing to students503504### Paid Courses505506If the organization has Stripe Connect configured:507- Set `priceCents` and `currency` to create a paid course508- Open enrollment must be enabled for paid courses509- Students go through Stripe checkout and are automatically enrolled after payment510511## CRITICAL: Embedding Media and Quizzes in Resource Content512513After creating images and quizzes, you **MUST** update the resource content to include references to them. Without these references, the bot will NOT display the media or quizzes during delivery.514515### Image Reference Syntax516517```markdown518519```520521Example: If `create_image` returns `{ id: 156 }`, insert:522```markdown523524```525526### Quiz Reference Syntax527528```markdown529::quiz{#QUIZ_ID}530```531532Example: If `create_quiz` returns `{ id: 28 }`, insert:533```markdown534::quiz{#28}535```536537### Visual Reference Syntax538539```markdown540::visual{#VISUAL_ID}541```542543Example: If `create_visual` returns `{ id: 91 }`, insert:544```markdown545::visual{#91}546```547548The bot displays the visual via `display_media` when it reaches this point in the prose — exactly like images and quizzes.549550### Complete Example551552```553# Step 1: Create resource554create_resource(555 name: "Introduction to Photosynthesis",556 content: "# Learning Objectives\n\nPlaceholder content...",557 tags: ["science", "biology"],558 deliveryMode: "conversation"559)560# Response: { id: 42, ... }561562# Step 2: Upload image from URL563upload_image_from_url(564 resourceId: 42,565 url: "https://example.com/plant-cell.png",566 name: "Plant Cell Diagram",567 description: "Diagram of a plant cell showing chloroplasts",568 question: "Where does photosynthesis occur?",569 answer: "In the chloroplasts",570 hint: "Look for the green organelles",571 botVisible: true572)573# Response: { id: 156, ... }574575# Step 3: Create quiz576create_quiz(577 resourceId: 42,578 description: "Test understanding of photosynthesis location",579 question: "In which organelle does photosynthesis take place?",580 questionType: "single",581 answers: [582 { id: "a", text: "Mitochondria", isCorrect: false },583 { id: "b", text: "Chloroplast", isCorrect: true },584 { id: "c", text: "Nucleus", isCorrect: false }585 ],586 retryLimit: 2,587 hint: "It contains chlorophyll, which is green"588)589# Response: { id: 28, ... }590591# Step 4: CRITICAL - Update resource with image and quiz references592update_resource(593 id: 42,594 content: "# Learning Objectives\n\nBy the end of this resource, the student should understand photosynthesis.\n\n## What is Photosynthesis?\n\nExplain that plants convert sunlight into energy. Show the diagram:\n\n\n\nDiscuss the diagram with the student.\n\n## Check Understanding\n\nNow test the student's knowledge:\n\n::quiz{#28}\n\n## Summary\n\nReview the key points."595)596```597598## Quiz Types599600### Single Choice601```json602{603 "question": "What gas do plants absorb?",604 "questionType": "single",605 "answers": [606 { "id": "a", "text": "Oxygen", "isCorrect": false },607 { "id": "b", "text": "Carbon dioxide", "isCorrect": true }608 ],609 "hint": "Think about what humans breathe out"610}611```612613### Multiple Choice614```json615{616 "question": "Select ALL ingredients for photosynthesis",617 "questionType": "multiple",618 "answers": [619 { "id": "a", "text": "Sunlight", "isCorrect": true },620 { "id": "b", "text": "Water", "isCorrect": true },621 { "id": "c", "text": "Oxygen", "isCorrect": false }622 ]623}624```625626### Open Answer627```json628{629 "question": "Calculate 15% of 80",630 "questionType": "freetext",631 "inputRestriction": "decimal",632 "expectedAnswer": "12",633 "evaluationCriteria": "Accept 12, 12.0, or 12.00"634}635```636637### Fraction638```json639{640 "question": "What is 1/2 + 1/6?",641 "questionType": "freetext",642 "inputRestriction": "fraction",643 "expectedAnswer": "2/3",644 "numericMin": 0,645 "numericMax": 1,646 "allowNegative": false,647 "evaluationCriteria": "Accept equivalent fractions (e.g. 4/6, 8/12)"648}649```650651### Ordered List652Students drag and drop items into the correct sequence. The answers array order defines the correct sequence — students see items shuffled.653```json654{655 "question": "Put these in order from smallest to largest",656 "questionType": "ordered_list",657 "answers": [658 { "id": "a", "text": "1/2", "isCorrect": false },659 { "id": "b", "text": "0.55", "isCorrect": false },660 { "id": "c", "text": "3/5", "isCorrect": false },661 { "id": "d", "text": "62%", "isCorrect": false }662 ],663 "hint": "Convert everything to decimals to compare"664}665```666Note: `isCorrect` is ignored for ordered_list — the array order IS the correct answer. Minimum 2 items, maximum 10.667668### Matching669Students match items from two columns. The left column shows fixed terms, the right column shows shuffled matches that students drag to align correctly.670```json671{672 "question": "Match each part of speech to its example",673 "questionType": "matching",674 "answers": [675 { "id": "a", "text": "verb", "isCorrect": false, "matchText": "jump" },676 { "id": "b", "text": "noun", "isCorrect": false, "matchText": "book" },677 { "id": "c", "text": "adverb", "isCorrect": false, "matchText": "quickly" }678 ],679 "hint": "Think about what each word does in a sentence"680}681```682Note: `isCorrect` is ignored for matching — correct state is when each answer's `matchText` is aligned next to its `text`. Each answer needs both `text` (term) and `matchText` (match). Minimum 2 pairs, maximum 10.683684### Fill in the Blank685Students fill in missing words within a sentence. Supports free text input or word bank (dropdown) mode.686```json687{688 "question": "Water freezes at [] degrees Celsius and boils at [] degrees Celsius",689 "questionType": "fill_in_blank",690 "answers": [691 { "id": "a", "text": "0", "isCorrect": true },692 { "id": "b", "text": "100", "isCorrect": true }693 ],694 "hint": "Think about the states of water"695}696```697For word bank mode (dropdown with distractors), set `inputRestriction` to `"word_bank"` and add distractor answers with `isCorrect: false`:698```json699{700 "question": "The [] sat on the []",701 "questionType": "fill_in_blank",702 "inputRestriction": "word_bank",703 "answers": [704 { "id": "a", "text": "cat", "isCorrect": true },705 { "id": "b", "text": "mat", "isCorrect": true },706 { "id": "c", "text": "dog", "isCorrect": false },707 { "id": "d", "text": "hat", "isCorrect": false }708 ]709}710```711Note: Use `[]` in the question to mark blank positions. Correct answers (`isCorrect: true`) map in order to blanks. Distractors (`isCorrect: false`) appear as extra options in word bank mode. Minimum 1 blank.712713## Quiz Illustrations714715A quiz can have an **illustration image** attached to it (separate from the resource's inline images and cover image). The illustration appears **alongside the question, on the same screen**, when the quiz is shown to the student.716717**This is the preferred way to give a quiz a supporting figure.** If a question refers to a diagram — "find the missing side of *this* triangle", "label the parts of *this* cell", "which angle is *x*?" — attach that figure to the quiz via its `image` field. Because the picture and the question render together and stay together, the student can read the diagram while they answer. **Do not** instead drop the figure inline in the content just before `::quiz{#id}`: the bot shows an inline image earlier, as it narrates that part of the lesson, so by the time the student is answering the quiz the picture may no longer be on screen. A quiz that depends on a figure should *own* that figure.718719A quiz illustration can be any image kind — an uploaded/AI image, a `create_svg` drawing, or a `create_visual` visual tool (pass the visual's `id` as the quiz `image`). For maths/science diagrams, prefer a `create_visual` (e.g. `geometry`, `coordinate_grid`, `function_graph`) or a `create_svg` so it stays crisp.720721Quiz images are **not** embedded in the resource markdown — they are linked directly to the quiz record via the `image` field. The resource content does NOT need a `` reference for quiz illustrations.722723### Attaching an illustration724725The image must already be attached to the same resource as the quiz. The normal flow is:726727```728# Step 1: Generate or upload the illustration, attached to the resource729generate_image(resourceId: 42, prompt: "A cross-section of a leaf showing chloroplasts")730# → { id: 177, ... }731732# Step 2a: Create the quiz with the image attached733create_quiz(734 resourceId: 42,735 question: "Which labelled part contains the chloroplasts?",736 questionType: "freetext",737 expectedAnswer: "The palisade mesophyll",738 image: 177739)740741# OR Step 2b: Create the quiz first, then attach the image later742create_quiz(resourceId: 42, question: "...", questionType: "single", answers: [...])743# → { id: 99, ... }744update_quiz(resourceId: 42, id: 99, image: 177)745```746747### When to use a quiz illustration vs. an inline image748749- **Quiz illu750751…(truncated)