Block reference
Every one of the 39 block types, with a complete example you can paste into a course and change the words in.
A block is one element on a screen. Every block has an id and a type; the rest of its fields depend on the type and are listed below. Fields a type doesn’t use are ignored, which is worth knowing: a block with a misspelled field name saves without complaint and then renders empty, so if something is blank in the player, check the spelling here first.
Ids must be unique within the course — the player keys a learner’s answers and their resume position by them. Any string will do; UUIDs if you have no preference.
An optional style object puts blocks side by side. width is a share of the row — "25", "50", "75" or "100" — and sameRowAsPrevious docks a block beside the one before it. Everything else about a block’s appearance comes from the course’s theme.
[
{ "id": "b_left", "type": "text", "text": "…", "style": { "width": "50" } },
{ "id": "b_right", "type": "image", "url": "…", "style": { "width": "50", "sameRowAsPrevious": true } }
]Content
Words on a page. Most of any course is these.
headingAI CAN WRITE ITA title. Usually the first block on a screen.
textThe heading itself.level"h1", "h2" or "h3". Use h2 for a screen title — h1 belongs to the course.{
"id": "b_title",
"type": "heading",
"text": "Handling a refund",
"level": "h2"
}textAI CAN WRITE ITA paragraph. The block you'll use most.
textThe paragraph. Plain text — no HTML.{
"id": "b_intro",
"type": "text",
"text": "Refunds are approved by the agent who takes the call, up to $200."
}calloutAI CAN WRITE ITA boxed aside in one of three tones — worth knowing, careful here, or never do this.
tone"info", "caution" or "critical". Sets the colour and the icon.titleThe line in bold at the top.textThe aside itself.{
"id": "b_note",
"type": "callout",
"tone": "caution",
"title": "Careful here",
"text": "Renaming a folder renames the route. Anything linking to the old path 404s until you add a redirect."
}quoteAI CAN WRITE ITA pull quote with an attribution.
textThe quote. Quotation marks are drawn for you.titleWho said it.captionTheir role — shown under the name.urlOptional avatar image. Without one, initials are drawn from the name.{
"id": "b_quote",
"type": "quote",
"text": "The router is the file system. Once that clicks, half the framework stops being framework.",
"title": "Rana Saleh",
"caption": "Staff engineer · Platform"
}codeAI CAN WRITE ITA code snippet with its path, a language chip and a Copy button.
textThe code. Newlines are kept; it is never reflowed.languageLanguage chip and highlighting — "tsx", "python", "sql".filenameThe path shown in the title bar. Where this lives in their repo, not a link.copyableWhether to offer Copy. Defaults to true.{
"id": "b_code",
"type": "code",
"filename": "app/orders/[id]/page.tsx",
"language": "tsx",
"text": "export default async function Page({ params }) {\n const order = await db.orders.find(params.id)\n return <OrderDetail order={order} />\n}"
}compareAI CAN WRITE ITTwo columns, do and don't, side by side.
comparePointsEach `{ id, side, label }` — side is "do" or "dont". Order within a side is kept.frontTextHeading over the do column. Defaults to "Do".backTextHeading over the don't column. Defaults to "Don't".{
"id": "b_cmp",
"type": "compare",
"comparePoints": [
{
"id": "c1",
"side": "do",
"label": "Read secrets on the server"
},
{
"id": "c2",
"side": "dont",
"label": "Import server config in a client file"
}
]
}spec_tableAI CAN WRITE ITKey and value, one row each — settings, limits, defaults.
specRowsEach `{ id, key, value }`. Values are drawn in mono, keys are not.{
"id": "b_spec",
"type": "spec_table",
"specRows": [
{
"id": "s1",
"key": "Runtime",
"value": "Node.js 20"
},
{
"id": "s2",
"key": "Revalidate",
"value": "3600s"
}
]
}checklistAI CAN WRITE ITLines the learner ticks off. The ticks belong to the learner and travel with their progress.
checklistItemsEach `{ id, label }`. Nothing is pre-ticked — that is the learner's to do.{
"id": "b_check",
"type": "checklist",
"checklistItems": [
{
"id": "k1",
"label": "Create the route folder"
},
{
"id": "k2",
"label": "Add page.tsx and export a component"
}
]
}resourceFiles to take away — a PDF, a spreadsheet, a link.
resourcesEach `{ id, name, url, kind, size }`. `kind` is one of "pdf", "sheet", "doc", "slide", "image", "archive", "link" and drives the icon; `size` is written by you, because a signed URL cannot be measured from here.{
"id": "b_files",
"type": "resource",
"resources": [
{
"id": "f1",
"name": "Refund policy v4.pdf",
"url": "https://cdn.example.com/refunds.pdf",
"kind": "pdf",
"size": "240 KB"
}
]
}instructor_noteAn aside in a person's own voice, signed — a human, not the system.
titleWho is speaking. Initials are drawn from it.textWhat they want to say.urlOptional photo in place of the initials.{
"id": "b_note2",
"type": "instructor_note",
"title": "Ahmed, who wrote this course",
"text": "You'll see the old getServerSideProps in three places. Leave them — touching them now breaks billing."
}glossaryAI CAN WRITE ITA term defined once. It is highlighted everywhere it appears in the paragraph.
textThe paragraph the term appears in.termThe word being defined. Matched case-insensitively.definitionWhat it means — shown in the card under the paragraph.{
"id": "b_term",
"type": "glossary",
"term": "partial prerendering",
"definition": "A static shell served instantly, with the dynamic holes streamed in after.",
"text": "Every route can opt into partial prerendering without changing how you write the page."
}markdownA Markdown document, rendered through the theme. Headings, lists, task lists, tables, quotes, fenced code, images and links all arrive as the same components the individual blocks use, so a pasted README looks like the rest of the course rather than like a README.
textThe Markdown source. GitHub-flavoured: `#` through `######`, `-`/`1.` lists nested to any depth, `- [ ]` task lists, `>` quotes, ``` fences with a language, tables with `:---:` alignment, `---` rules, `**bold**`, `*italic*`, `~~strike~~`, `` `code` ``, `[links](url)` and ``. Raw HTML is not rendered — it comes out as text.copyableWhether fenced code inside the document offers a Copy button. Defaults to true.styleThe usual block style. `fontSize`, `fontFamily`, `fontWeight`, `color` and `align` apply to the whole document — everything inside is sized relative to the block, so one size change scales the headings and the table along with the paragraphs.{
"id": "b_md",
"type": "markdown",
"text": "## Before you start\n\nYou need two things:\n\n- an **API key** from Settings → API keys\n- a course `id` to publish into\n\n| Field | Required |\n| :--- | :---: |\n| `title` | yes |\n| `screens` | yes |\n\n> Keys are shown once. Store it before you close the dialog."
}Media
Anything with a URL you host. We never upload or proxy these — the learner's browser fetches them.
imageA picture.
urlWhere the image is hosted. Must be publicly reachable — learners' browsers fetch it directly.altWhat the image shows, for anyone who can't see it. A different job from the caption: leave it blank only when the image is decorative.captionOptional line read by everyone, shown underneath.{
"id": "b_photo",
"type": "image",
"url": "https://cdn.example.com/refund-screen.png",
"alt": "The refund button in the admin panel",
"caption": "The refund button sits under Orders."
}videoAn embedded video.
urlA YouTube, Vimeo or direct video-file URL.posterUrlStill frame shown before playback. Without one the video is a black rectangle until it decodes.captionsUrlA WebVTT track. Hosted elsewhere, it needs CORS headers or the browser drops it silently.captionOptional line underneath.{
"id": "b_clip",
"type": "video",
"url": "https://cdn.example.com/refund-walkthrough.mp4",
"posterUrl": "https://cdn.example.com/refund-walkthrough-poster.jpg",
"captionsUrl": "https://cdn.example.com/refund-walkthrough.en.vtt"
}audioAn audio player.
urlA direct link to an audio file.captionOptional line underneath.transcriptWhat the audio says, in plain text. Shown in a collapsed panel under the player and offered for translation.{
"id": "b_call",
"type": "audio",
"url": "https://cdn.example.com/sample-call.mp3"
}embedAny other site in an iframe — a slide deck, a form, a dashboard.
urlThe page to embed. It must allow being framed, or the learner sees a blank box.aspectRatioHow the frame is shaped, as a CSS ratio — "16 / 9" (default), "4 / 3", "1 / 1" or "3 / 4". Forms and prototypes are rarely 16:9.textUsed as the frame's accessible title. Without one the embedded page's host is used.{
"id": "b_deck",
"type": "embed",
"url": "https://docs.google.com/presentation/d/e/.../embed"
}slideshowA carousel of images the learner swipes or clicks through.
slidesAn array of { id, url, caption }. Two or more.{
"id": "b_tour",
"type": "slideshow",
"slides": [
{
"id": "s1",
"url": "https://cdn.example.com/step-1.png",
"caption": "Open the order"
},
{
"id": "s2",
"url": "https://cdn.example.com/step-2.png",
"caption": "Choose Refund"
}
]
}video_chaptersA recording with jump points down its side.
urlThe video. Same sources a `video` block takes.chaptersEach `{ id, at, label }` — `at` is seconds from the start.posterUrlStill frame before it plays.{
"id": "b_chapters",
"type": "video_chapters",
"url": "https://cdn.example.com/routing.mp4",
"chapters": [
{
"id": "ch1",
"at": 0,
"label": "What we're building"
},
{
"id": "ch2",
"at": 134,
"label": "Folder to URL"
}
]
}Interactive
Things a learner clicks, flips or drags. None of them are scored.
stackAI CAN WRITE ITA deck of cards the learner clicks through one at a time.
stackCardsAn array of { id, title, text }. Up to 10.{
"id": "b_cards",
"type": "stack",
"stackCards": [
{
"id": "c1",
"title": "Under $50",
"text": "Approve it yourself."
},
{
"id": "c2",
"title": "Over $200",
"text": "Escalate to a supervisor."
}
]
}stepsAI CAN WRITE ITA numbered procedure, in order. Use it when order is causal — do this, then this.
stackCardsAn array of { id, title, text }, in the order they happen.{
"id": "b_how",
"type": "steps",
"stackCards": [
{
"id": "s1",
"title": "Find the order",
"text": "Search by email or order number."
},
{
"id": "s2",
"title": "Check eligibility",
"text": "Refunds are allowed within 30 days."
},
{
"id": "s3",
"title": "Issue the refund",
"text": "Choose Refund and confirm the amount."
}
]
}timelineAI CAN WRITE ITEvents in chronological order. Use it when order is time, not cause.
stackCardsAn array of { id, date, title, text }. `date` is free text — "2019", "Q3 2024" and "Today" are all fine — and renders in its own gutter so dates line up down the column.{
"id": "b_history",
"type": "timeline",
"stackCards": [
{
"id": "t1",
"date": "2019",
"title": "Finance-only refunds",
"text": "Every refund went through the finance team."
},
{
"id": "t2",
"date": "2023",
"title": "Agent limit introduced",
"text": "Agents given a $200 limit of their own."
}
]
}accordionAI CAN WRITE ITCollapsible sections. Good for FAQs, where people read one entry, not all of them.
stackCardsAn array of { id, title, text }. The title stays visible, so it must stand alone as a question.{
"id": "b_faq",
"type": "accordion",
"stackCards": [
{
"id": "a1",
"title": "Can I refund a gift card?",
"text": "No — gift cards are non-refundable."
},
{
"id": "a2",
"title": "What if the order is over 30 days old?",
"text": "Escalate to a supervisor."
}
]
}flashcardAI CAN WRITE ITA card with a front and a back. The learner flips it.
frontTextThe prompt side.backTextThe answer side.{
"id": "b_flip",
"type": "flashcard",
"frontText": "What's the agent refund limit?",
"backText": "$200 without approval."
}matchAI CAN WRITE ITDrag-and-drop matching pairs. Interactive but not scored.
matchPairsAn array of { id, left, right }. Three to six pairs works best.{
"id": "b_match",
"type": "match",
"matchPairs": [
{
"id": "m1",
"left": "Under $50",
"right": "Approve yourself"
},
{
"id": "m2",
"left": "Over $200",
"right": "Escalate"
}
]
}hotspotAn image with clickable points on it. Interactive but not scored.
urlThe image to place hotspots on.hotspotsAn array of { id, x, y, label, description }. x and y are percentages, 0–100.{
"id": "b_spots",
"type": "hotspot",
"url": "https://cdn.example.com/admin-panel.png",
"hotspots": [
{
"id": "h1",
"x": 42,
"y": 61,
"label": "Refund",
"description": "Opens the refund dialog."
}
]
}pollAI CAN WRITE ITAn ungraded question. Answering shows how everyone else answered.
questionWhat you're asking.optionsEach `{ id, label }`. `correct` is ignored — a poll has no right answer.{
"id": "b_poll",
"type": "poll",
"question": "How often do you touch routing?",
"options": [
{
"id": "o1",
"label": "Every week"
},
{
"id": "o2",
"label": "Once a month"
}
]
}reflectionAI CAN WRITE ITA private answer box. Nobody else reads it and it is never marked.
questionThe prompt.maxLengthCharacter budget. Defaults to 500.{
"id": "b_reflect",
"type": "reflection",
"question": "Where in your own product would a route group have saved you?",
"maxLength": 500
}scenarioA situation with branching choices. Each choice goes to a screen — it decides what happens next rather than whether anyone was right.
titleThe kicker over the situation.questionThe situation itself.optionsEach `{ id, label, note, targetScreenId }`. `note` is the consequence line under the choice; `targetScreenId` is where it leads, and unset falls through to the next screen.{
"id": "b_scenario",
"type": "scenario",
"title": "Your call",
"question": "A customer wants a refund on a $340 order placed nine weeks ago. Policy says 60 days.",
"options": [
{
"id": "p1",
"label": "Refund it anyway",
"note": "Goes to the escalation path",
"targetScreenId": "s_escalate"
},
{
"id": "p2",
"label": "Decline, cite policy",
"note": "Goes to the complaint path",
"targetScreenId": "s_complaint"
}
]
}Assessment
Questions. The first four are the only scored blocks.
quiz_single_choiceSCOREDAI CAN WRITE ITOne question, several answers, exactly one right.
questionThe question.optionsAn array of { id, label, correct }. Exactly one must have correct: true.{
"id": "b_q1",
"type": "quiz_single_choice",
"question": "What is an agent's refund limit without approval?",
"options": [
{
"id": "o1",
"label": "$50",
"correct": false
},
{
"id": "o2",
"label": "$200",
"correct": true
},
{
"id": "o3",
"label": "No limit",
"correct": false
}
]
}quiz_multiple_choiceSCOREDAI CAN WRITE ITOne question, several answers, two or more right. Marked right only if they pick all of them.
questionThe question.optionsAn array of { id, label, correct }. Two or more must have correct: true.{
"id": "b_q2",
"type": "quiz_multiple_choice",
"question": "Which need supervisor approval?",
"options": [
{
"id": "o1",
"label": "Orders over $200",
"correct": true
},
{
"id": "o2",
"label": "Orders over 30 days old",
"correct": true
},
{
"id": "o3",
"label": "Orders under $50",
"correct": false
}
]
}quiz_true_falseSCOREDAI CAN WRITE ITA statement the learner marks true or false.
questionThe statement.optionsExactly two: { id: "true", ... } and { id: "false", ... }. Mark the right one correct.{
"id": "b_q3",
"type": "quiz_true_false",
"question": "Gift cards can be refunded.",
"options": [
{
"id": "true",
"label": "True",
"correct": false
},
{
"id": "false",
"label": "False",
"correct": true
}
]
}fill_in_blankSCOREDAI CAN WRITE ITThe learner types the answer. Compared as text, case-insensitively.
questionThe question or sentence with the gap.expectedAnswerThe canonical answer. Compared as text after trimming and lower-casing, not understood.acceptedAnswersOther spellings that should also count — plurals, units, a trailing full stop. Without these, "30 days" is marked wrong against an expected "30".{
"id": "b_q4",
"type": "fill_in_blank",
"question": "Refunds are allowed within how many days?",
"expectedAnswer": "30",
"acceptedAnswers": [
"30 days",
"thirty"
]
}resultsA score summary. Optional — the player shows a completion screen at the end whether or not you add one, so use this only to put the score somewhere else.
{
"id": "b_score",
"type": "results"
}rankingSCOREDAI CAN WRITE ITRows dragged into the right order. Graded — the order you author is the correct one.
questionWhat order you're asking for.optionsEach `{ id, label }`, written in the correct order. The player shuffles what it shows.{
"id": "b_rank",
"type": "ranking",
"question": "Put these in the order you'd do them",
"options": [
{
"id": "r1",
"label": "Create the folder"
},
{
"id": "r2",
"label": "Add page.tsx"
},
{
"id": "r3",
"label": "Fetch the data"
}
]
}timed_questionSCOREDAI CAN WRITE ITA question on a clock. Graded — running out of time counts as wrong.
questionThe question.secondsHow long they get. Unset means no clock, which makes this an ordinary single-choice question.optionsEach `{ id, label, correct }`, exactly as a single-choice quiz.{
"id": "b_timed",
"type": "timed_question",
"question": "Which file makes a folder routable?",
"seconds": 20,
"options": [
{
"id": "t1",
"label": "page.tsx",
"correct": true
},
{
"id": "t2",
"label": "route.ts",
"correct": false
}
]
}Layout
Structure and spacing, no content of their own.
dividerAI CAN WRITE ITA horizontal rule, optionally with a word in the middle of it.
textOptional label shown centred on the line. Leave empty for a plain rule.{
"id": "b_rule",
"type": "divider",
"text": "Part two"
}spacerVertical breathing room. No content of its own.
{
"id": "b_gap",
"type": "spacer"
}buttonA button that continues, jumps to a specific screen, or opens a link.
textThe label on the button.buttonAction"next" to advance, "screen" to jump, "url" to open a link.targetScreenIdWhich screen to jump to. Only read when buttonAction is "screen".urlWhere to send them. Only read when buttonAction is "url". A "url" button with no url is not rendered to learners at all — a button that cannot act is not shown as one.{
"id": "b_next",
"type": "button",
"text": "I understand",
"buttonAction": "next"
}milestoneAI CAN WRITE ITA beat between sections — what is done, and what the next part is about.
titleThe kicker, e.g. "Part one complete".textThe headline.captionThe paragraph under it.milestoneStatsEach `{ id, value, label }` — the figures along the side.ctaLabelThe button's label; `buttonAction` and `targetScreenId` say where it goes.{
"id": "b_milestone",
"type": "milestone",
"title": "Part one complete",
"text": "You can route. Now let's make it fast.",
"caption": "The next three screens are about caching.",
"milestoneStats": [
{
"id": "m1",
"value": "4",
"label": "screens"
},
{
"id": "m2",
"value": "6",
"label": "min"
}
],
"ctaLabel": "Start part two",
"buttonAction": "next"
}Two things that trip people up
Interactive is not the same as scored. Matching and hotspot blocks are things a learner does, and neither counts toward their score. That is deliberate: it keeps the score on screen identical to the score in the API, the webhook and the SCORM package.
You don’t need a results block. The player shows a completion screen — score, certificate link, what comes next — at the end of every course whether or not you added one. Add it only when you want the score somewhere other than the end.
Blocks marked AI CAN WRITE IT are the ones generation will produce. The rest need a real asset URL, and a model asked for one invents it.