Create a shareable RooQuiz preview quiz — a right/wrong assessment where correct answers earn points and the taker gets a score — and get a link to open in the browser. No account, login, or API key required. Use this when someone wants to build, try out, or share a graded quiz, test, trivia, or exa
---
name: preview-quiz
description: Create a shareable RooQuiz preview quiz — a right/wrong assessment where correct answers earn points and the taker gets a score — and get a link to open in the browser. No account, login, or API key required. Use this when someone wants to build, try out, or share a graded quiz, test, trivia, or exam with correct answers and a final score. For a scored questionnaire where every option adds points, use the preview-scorecard skill; for a personality/type test with no right answers, use the preview-outcome skill.
---
# Create a RooQuiz Preview Quiz
POST a quiz as JSON to RooQuiz's open preview endpoint and instantly get a **short-lived (~1 hour)**, **browser-openable** preview link. The creation endpoint is public (`access.create => true`), so this needs **no account, login, API key, or credentials** — anything that can make an HTTP request can use it.
A **quiz** (`scene: "quiz"`) is a right/wrong assessment: each scored question carries a correct answer and earns points; the total is the sum of earned points, then bucketed into levels on the results page. It produces a **temporary preview**, not a permanently published form — the link expires automatically (recreate the quiz in RooQuiz if you need to keep it).
Two sibling skills cover the other assessment types — pick the one that matches:
- **preview-scorecard** — a scored questionnaire where each option adds points toward a total + level (no "correct" answer).
- **preview-outcome** — a personality / type test where options vote for result types and the most-voted type wins (no right answers).
## Three steps
1. **Build the quiz JSON** (structure below).
2. **Create it:** `POST {PREVIEW_BASE}/api/preview-forms` with the JSON as the body and header `Content-Type: application/json`. Returns `{ doc: { publicToken, expiresAt }, message }`.
3. **Hand back the link:** `{QUIZ_BASE}/b/{publicToken}` (append `?secret={secret}` only if you set a `secret` when creating).
### Endpoints
| | Default (RooQuiz cloud) | Override env var |
| --- | --- | --- |
| `PREVIEW_BASE` (create) | `https://preview.rooquiz.com` | `ROOQUIZ_PREVIEW_BASE` |
| `QUIZ_BASE` (open preview) | `https://quizster.app` | `ROOQUIZ_QUIZ_BASE` |
Override the env vars only when targeting a self-hosted RooQuiz deployment; otherwise the defaults work as-is.
### Create it
Send one HTTP request. There is no auth — `Content-Type: application/json` is the only required header. Use whatever HTTP client your environment has (an agent's built-in fetch/HTTP tool, `curl`, `requests`, `fetch`, Postman, …):
```http
POST https://preview.rooquiz.com/api/preview-forms
Content-Type: application/json
<the quiz JSON as the raw request body>
```
For example, with `curl` (write the JSON to a file first, or inline it with `--data`):
```bash
curl -sS -X POST https://preview.rooquiz.com/api/preview-forms \
-H 'Content-Type: application/json' \
--data-binary @/tmp/preview-form.json
```
The response is JSON shaped like:
```json
{ "doc": { "publicToken": "7k3m9q2p", "expiresAt": "2026-06-16T09:12:00.000Z" }, "message": "..." }
```
Read `doc.publicToken` and build the preview link to hand the user:
```
https://quizster.app/b/<publicToken>
```
If you set a `secret` in the JSON, append `?secret=<secret>` to that link. For a self-hosted RooQuiz, swap the two hosts for your deployment's preview-API and quiz hosts (see the endpoints table above).
## Quiz JSON — top level
```jsonc
{
"scene": "quiz", // required & fixed for this skill. Sets allowed question types and scoring. Cannot change after creation.
"title": "My Quiz", // required
"description": "Optional intro",
"language": "en_US", // form language; default zh_CN. See "language values" in Notes.
"personalized": { // appearance; omit to use defaults (light theme)
"key": "default",
"theme": { "name": "light" }
},
"indexDisplayMode": "number", // question numbering: none (default) | number | uppercase | roman
"fields": [ /* questions — see "Question types" and "Scoring" */ ],
"report": { /* results page — see "Report configuration" */ },
"secret": "optional; if set, the preview link must include ?secret="
}
```
Key points:
- Every question needs a **unique `code`** (string); option `code`s must be unique within their question. A `code` must be a valid identifier — start with a letter or `_`, then only letters/digits/`_` (no `-`, spaces, or leading digit), max 64 chars, and not a reserved math word (`e`, `E`, `pi`, `PI`, `tau`, `phi`, `i`, `Infinity`, `NaN`, `true`, `false`, `null`, `undefined`). The server rejects violations with HTTP 400.
- `name` is the question text; `description` is optional helper text.
- Unknown top-level fields are silently ignored by the server (no error).
- **`secret`**: omit it for a clean link that works with just the token (tokens are random 8-char and expire in ~1 hour — fine for previews). Set it to make the token unguessable, at the cost of requiring `?secret=` on the link. Default: omit.
## Themes
`personalized.theme.name` sets the visual theme. Omit `theme` to default to `light`. Set it as `"personalized": { "key": "default", "theme": { "name": "synthwave" } }`. There is no `personalized.layout` — card is the only answering layout. (The per-field `layout` (`list`/`grid`) is a different thing and still valid.)
Pick a theme that fits the quiz's topic/mood. **To use a random theme** — when the user asks for one, wants variety, or has no preference — just choose a random `name` from this list when building the JSON (there's no server-side "random" option). Recommended quiz palette:
| name | vibe / good for |
| --- | --- |
| `light` | clean neutral bright; default, formal/general quizzes |
| `corporate` | professional blue+gray; B2B, career, business |
| `dark` | modern sleek dark; tech, night, cool personality quizzes |
| `cupcake` | soft pink, cute, rounded; fun, food, kids, lighthearted |
| `pastel` | gentle pastel artsy; lifestyle, aesthetics, soft mood |
| `valentine` | pink romantic hearts; love, relationships, holidays |
| `synthwave` | neon purple/pink retro; gaming, trends, bold personality |
| `luxury` | dark + gold premium; finance, luxury, high-end |
| `forest` | deep green nature; environment, health, outdoors |
| `coffee` | warm brown cozy; food & drink, cafe, lifestyle |
| `autumn` | warm orange/brown seasonal; autumn, cozy, harvest |
| `halloween` | purple + orange spooky; Halloween, horror, festive |
| `night` | deep calm blue; astronomy, mindfulness, calm tech |
| `cyberpunk` | high-contrast neon yellow; tech, esports, gaming |
The full daisyUI theme set also renders (e.g. `emerald`, `dracula`, `retro`, `nord`, `sunset`, `winter`, `lofi`, `garden`, `aqua`, `business`, `lemonade`, `dim`, `bumblebee`, `acid`, `fantasy`, `wireframe`, `black`, `cmyk`) — the table above is just the recommended quiz palette. The server doesn't validate the name, so a typo silently falls back to default styling rather than erroring.
## Question types
Each field is `{ type, code, name, ... }`. In a quiz you can use:
- **Choice** (carry `choices: [{ code, value }]`, where `value` is the option label):
`SingleCheck` (single), `MultiCheck` (multiple; optional `min`/`max`), `DropDown`, `Cascade`, `Ordering`, `TrueFalse` (no choices)
- **Input**: `FillBlank` (optional `multiline`), `NumberField`, `DateField` (needs `precision`), `TimeField`
- **Display only** (never scored): `Statement` (`content`), `Breaker` (page break / divider), `Swiper` (image carousel, `items`)
`Rate` is **not** allowed in a quiz (it's a scorecard type). Optional shared props on any field: `required`, `description`, `explain`, `hidden`, `activeColor` (`primary`/`secondary`/`accent`/`neutral`), `layout` (`list`/`grid`).
## Scoring
Add **`correctAnswer`** + **`exactScoring`** to each scored question:
```jsonc
"correctAnswer": "b",
"exactScoring": { "mode": "exactMatch", "score": 10 }
```
**`correctAnswer` value depends on the question type:** single choice = the correct option's `code` (string); multiple/ordering = array of `code`s; fill-in = string; true/false = boolean.
Optional partial credit: `exactScoring.accuracy` + `extraLevels`, or `partialScoring` on choice questions (using `partialScoring` requires `exactScoring` too). Display-only fields (`Statement`/`Breaker`/`Swiper`) are never scored — leave scoring off them.
## Report configuration
`report.overallAnalysis` is **required**. A quiz **auto-sums** earned points, so **omit `formula`** — just map the total to `levels`.
```jsonc
"report": {
"overallAnalysis": {
"title": "Your Score",
"summaryTemplate": "<p><art-field data-type=\"fieldVariable\" data-cid=\"score\"></art-field> pts · <art-field data-type=\"fieldVariable\" data-cid=\"level\"></art-field></p>",
"levels": [ // map total score → level (see strict rules below)
{ "minScore": null, "maxScore": 10, "label": "Beginner" },
{ "minScore": 10, "maxScore": 20, "label": "Intermediate" },
{ "minScore": 20, "maxScore": null, "label": "Expert", "description": "Top tier!" }
]
}
}
```
**Strict `levels` rules** (violations return HTTP 400): the first level's `minScore` must be `null` (−∞); the last level's `maxScore` must be `null` (+∞); middle levels have finite numbers on both ends; for adjacent levels `current.minScore === previous.maxScore`; `maxScore` strictly increases down the array. A single level has `null` on both ends.
Levels may carry a `cta` (results-page button): `{ enabled, type: "link", text, url, newWindow }`. When enabled, `text` and `url` are required, and `url` must be an `http`/`https` link. `summaryTemplate` defaults to a built-in template if omitted.
## Common mistakes
These wrong patterns get reached for out of habit; the server rejects them with HTTP 400. The formats above are correct — match them exactly.
- **Option lists are `choices`, never `options`.** Every choice question uses `"choices": [{ "code": "a", "value": "Label" }]`. There is no `options` key.
- **Codes must be valid identifiers, not arbitrary text** — no hyphens (`type-a`), spaces, or leading digits (`1q`), and not reserved math words. Watch out for **5+ options coded `a,b,c,d,e`**: `e` (Euler's number) is reserved and rejected — use `o1, o2, …` or another non-reserved identifier.
- **Scoring is field-level, not nested inside each choice.** `correctAnswer` and `exactScoring` sit next to `choices` on the field, not on an individual option.
- **Don't set a `formula` for a quiz** — quizzes auto-sum earned points. `formula` is a scorecard thing; including it here is unnecessary (use the preview-scorecard skill if you actually want a custom total).
- **`report.overallAnalysis` is required** — give it at least a `title` and a valid `levels` array.
## Complete example
The `personalized.theme.name` is matched to the topic (see **Themes** above) — swap in any other name, or pick one at random.
```json
{
"scene": "quiz",
"title": "World Capitals Quiz",
"language": "en_US",
"personalized": { "key": "default", "theme": { "name": "corporate" } },
"indexDisplayMode": "number",
"fields": [
{
"type": "SingleCheck", "code": "q1", "name": "Capital of France?", "required": true,
"choices": [
{ "code": "a", "value": "Berlin" },
{ "code": "b", "value": "Paris" },
{ "code": "c", "value": "Rome" }
],
"correctAnswer": "b",
"exactScoring": { "mode": "exactMatch", "score": 10 }
},
{
"type": "MultiCheck", "code": "q2", "name": "Which are in Asia?", "required": true,
"choices": [
{ "code": "a", "value": "Japan" },
{ "code": "b", "value": "Brazil" },
{ "code": "c", "value": "Thailand" }
],
"correctAnswer": ["a", "c"],
"exactScoring": { "mode": "exactMatch", "score": 10 }
}
],
"report": {
"overallAnalysis": {
"title": "Your Score",
"summaryTemplate": "<p><art-field data-type=\"fieldVariable\" data-cid=\"score\"></art-field> pts · <art-field data-type=\"fieldVariable\" data-cid=\"level\"></art-field></p>",
"levels": [
{ "minScore": null, "maxScore": 10, "label": "Novice" },
{ "minScore": 10, "maxScore": 20, "label": "Good" },
{ "minScore": 20, "maxScore": null, "label": "Perfect" }
]
}
}
}
```
## Notes & limits
- **Expiry:** previews self-destruct after about **1 hour** (`expiresAt`); the link 404s afterward. Recreate the quiz in RooQuiz to keep it permanently.
- **Rate limit:** anonymous creation is capped at about **10 previews per hour** per IP.
- **Validation errors:** a 400 response includes `errors[].path` and `message` — fix the JSON and retry. Most common: a question type or scoring style that doesn't match `scene: "quiz"`, or non-contiguous `levels`.
- **Results page looks empty?** This is a preview (no submission backend); results are computed in the browser from the returned questions + `report`. Make sure `report.overallAnalysis` exists and scored questions carry `correctAnswer` + `exactScoring`.
- **`language` values:** `en_US` `de_DE` `es` `pt_BR` `fr` `zh_CN` (default) `zh_TW` `ja_JP` `ko_KR`.
don't have the plugin yet? install it then click "run inline in claude" again.
added explicit sections for decision points (validation failures, expiry, self-hosted deployments), extracted all external connection details to inputs, reformatted procedure as three sequential steps with clear in/out for each, added edge cases for rate limits and network errors, documented theme selection logic, and clarified report.overallAnalysis and scoring rules to reduce common mistakes.
build and instantly publish a temporary, browser-openable quiz where each question has one correct answer, earns points on match, and contributes to a final bucketed score. use this when someone wants to create, test, or share a graded quiz, test, trivia, or exam with right answers and a score result. the preview link self-destructs in ~1 hour. if you need a scored questionnaire with no right answers (every option adds points), use preview-scorecard instead. if you need a personality or type test where options vote for result types, use preview-outcome instead.
external connection: RooQuiz preview API
https://preview.rooquiz.com (default) or override with env var ROOQUIZ_PREVIEW_BASEhttps://quizster.app (default) or override with env var ROOQUIZ_QUIZ_BASEquiz JSON structure (see procedure step 1 for detailed schema)
scene (fixed to "quiz"), title, fields (array of questions), report.overallAnalysisdescription, language, personalized (theme/layout), indexDisplayMode, secretcode (valid identifier: letter/underscore start, then letters/digits/underscore only, max 64 chars, no reserved math words like e, pi, true, false, null, etc.)code within its questionenvironment (optional, only if self-hosting RooQuiz)
ROOQUIZ_PREVIEW_BASE: override default preview API hostROOQUIZ_QUIZ_BASE: override default quiz link hoststep 1: build the quiz JSON
construct a valid quiz JSON object matching this structure:
{
"scene": "quiz", // required; fixed for quizzes
"title": "Your Quiz Title", // required
"description": "Optional intro text", // optional
"language": "en_US", // optional; default zh_CN
"personalized": { // optional appearance config
"key": "default",
"theme": { "name": "light" }, // see themes table below
"layout": "card" // "list" or "card"
},
"indexDisplayMode": "number", // question numbering: none | number | uppercase | roman
"fields": [ // array of questions; see question types below
{
"type": "SingleCheck", // question type
"code": "q1", // unique identifier
"name": "Question text?", // question label
"description": "Optional helper text", // optional
"required": true, // optional
"choices": [ // for choice types only
{ "code": "a", "value": "Option A" },
{ "code": "b", "value": "Option B" }
],
"correctAnswer": "b", // required for scored questions
"exactScoring": { "mode": "exactMatch", "score": 10 } // required for scored questions
}
],
"report": { // required
"overallAnalysis": {
"title": "Your Score",
"summaryTemplate": "<p><art-field data-type=\"fieldVariable\" data-cid=\"score\"></art-field> pts · <art-field data-type=\"fieldVariable\" data-cid=\"level\"></art-field></p>",
"levels": [ // required; map total score to result level
{ "minScore": null, "maxScore": 10, "label": "Beginner" },
{ "minScore": 10, "maxScore": 20, "label": "Intermediate" },
{ "minScore": 20, "maxScore": null, "label": "Expert" }
]
}
},
"secret": "optional_secret_string" // optional; makes token unguessable
}
input/output of step 1: none external; produces a valid JSON string for step 2.
themes available for personalized.theme.name: light (default, clean neutral bright), corporate (professional blue+gray), dark (modern sleek dark), cupcake (soft pink cute), pastel (gentle artsy), valentine (pink romantic), synthwave (neon retro), luxury (dark+gold premium), forest (deep green nature), coffee (warm brown cozy), autumn (warm orange seasonal), halloween (purple+orange spooky), night (deep calm blue), cyberpunk (neon yellow). any daisyUI theme name also renders (emerald, dracula, retro, nord, sunset, winter, lofi, garden, aqua, business, lemonade, dim, bumblebee, acid, fantasy, wireframe, black, cmyk); typos silently fall back to default styling.
question types allowed in a quiz:
scoring rules for each question:
correctAnswer and exactScoringcorrectAnswer value shape depends on type: single choice = string (the correct option's code); multiple/ordering = array of strings; fill-in/number = string or number; true/false = booleanexactScoring format: { "mode": "exactMatch", "score": <points> } where score is the points earned for a correct answeraccuracy + extraLevels or partialScoring (choice questions only); partialScoring requires exactScoring to be set tooreport.overallAnalysis strict rules:
overallAnalysis is required; must have at least title and a valid levels arrayminScore: null (negative infinity)maxScore: null (positive infinity)current.minScore === previous.maxScore (no gaps)null on both endssummaryTemplate (HTML with art-field tags); if omitted, uses built-in templatecta button: { enabled, type: "link", text, url, newWindow } where url is http/httpsinput/output of step 1 complete: validated quiz JSON string ready for POST in step 2.
step 2: POST the quiz JSON to the preview creation endpoint
make one HTTP POST request to the RooQuiz preview API:
POST https://preview.rooquiz.com/api/preview-forms
Content-Type: application/json
<raw quiz JSON as request body>
if using env var overrides, substitute your ROOQUIZ_PREVIEW_BASE for https://preview.rooquiz.com.
example with curl:
curl -sS -X POST https://preview.rooquiz.com/api/preview-forms \
-H 'Content-Type: application/json' \
--data-binary @/tmp/quiz.json
example with fetch:
const response = await fetch('https://preview.rooquiz.com/api/preview-forms', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(quizJSON)
});
const data = await response.json();
input of step 2: quiz JSON from step 1, content-type header set to application/json, no auth.
output of step 2: HTTP response body (JSON) shaped like:
{
"doc": {
"publicToken": "7k3m9q2p",
"expiresAt": "2026-06-16T09:12:00.000Z"
},
"message": "Form created successfully"
}
extract doc.publicToken for step 3. if the response is HTTP 400, the JSON validation failed; check errors[].path and message to identify the problem (common: non-contiguous levels, invalid codes, unsupported question type for scene: "quiz", or formula included).
step 3: construct and return the shareable preview link
build the preview URL from the token returned in step 2:
https://quizster.app/b/<publicToken>
if you set a secret in the quiz JSON (step 1), append the secret as a query param:
https://quizster.app/b/<publicToken>?secret=<secret>
if using env var ROOQUIZ_QUIZ_BASE, swap https://quizster.app for your override.
input of step 3: publicToken and secret (if set) from step 2 response.
output of step 3: complete shareable preview link (http/https URL). hand this to the user. the link is live for ~1 hour, then 404s. the quiz is not permanently saved; recreate it in RooQuiz if persistence is needed.
if the user asks for a random theme: the server has no randomization option. when the user wants variety or has no preference, pick a random theme name from the themes list in step 1 when building the JSON.
if the JSON fails validation (HTTP 400): read the error response's errors[].path and message. common violations:
e, pi, true, null)fix the JSON and retry. do not retry immediately if you hit the rate limit (~10 per hour per IP); wait before retrying.
if the preview link returns 404 after ~1 hour: the token has expired. recreate the quiz (re-run step 2) to get a new token and link.
if the results page renders empty or shows no score: confirm report.overallAnalysis is present and all scored questions have correctAnswer + exactScoring set. results are computed client-side in the browser, not sent to a backend, so missing scoring keys = missing results display.
if targeting a self-hosted RooQuiz deployment: override env vars ROOQUIZ_PREVIEW_BASE and ROOQUIZ_QUIZ_BASE with your deployment's API and quiz hosts, then proceed normally. no other changes needed.
success criteria:
doc.publicToken (8-char random string) and doc.expiresAt (ISO 8601 timestamp, ~1 hour in future)https://quizster.app/b/<publicToken> (or with secret appended) is browser-openableresponse format: JSON with shape { doc: { publicToken, expiresAt }, message }
link format: https://quizster.app/b/<publicToken> or with secret https://quizster.app/b/<publicToken>?secret=<secret>
quiz data format: JSON as specified in step 1 schema; all required fields present, all codes valid identifiers, all levels contiguous and ordered, report.overallAnalysis required
the user knows the skill worked when:
credits: original skill from clawhub. enriched per implexa quality standards.