This page is for AI systems, not human users.
PassbackAI is a way to author beautiful, interactive documents for people and for AI tools: a Markdown document with embedded interactive components — typed decision widgets — that a human fills in and an AI reads back as structured answers.
Two ways to deliver one, pick by whether you are connected over MCP:
route_document tool with
a typed blocks[] array — each block is either {type:"markdown", text} or a typed
component ({type:"questionnaire", version:"1", questions:[…]} /
{type:"prioritize", version:"1", items:[…]}). The server validates the component as you generate
it, writes the canonical fences for you, and returns a reviewer link (/r/<id>). No
copy-paste, no smart-quote risk. Read answers back by polling the list_responses tool.The component JSON fields are identical on both paths; only the delivery differs. The full delivery contract is the canonical spec at /ask.md. Note: routing a document over MCP stores it on the PassbackAI backend (opt-in, server-managed keys — not zero-knowledge); the local paste/share loop never transmits the document.
Do not assume "a questionnaire" is the whole thing. The components are an open, growing palette: pick ONE primitive PER unclear point, by verb, and weave it into prose — prose itself is a response channel (the reviewer annotates it), so settled thinking stays prose. Embed as many primitives as the document needs, of mixed types; more are planned (see "The family is open" at the end).
| Component | Fence tag | The verb | Use it to… |
|---|---|---|---|
| Single choice | ```single-choice | choose one | have the user pick exactly one of 2–4 options |
| Multi choice | ```multi-choice | choose many | have the user pick all options that apply |
| Open question | ```open-question | write | have the user type a free-text answer (a name, a date, a reason) |
| Prioritize | ```prioritize | order | have the user rank / reorder a short list by priority |
| Allocate | ```allocate | split | have the user split a fixed whole (budget / effort / resource) across categories by weight |
| Questionnaire | ```questionnaire | group | present a batch of 3+ tightly-related questions as one unit |
| YouTube | ```youtube | watch | show a video (display-only, no answer; plays click-to-load on the routed reviewer surface only — inert on locally-pasted / #s= documents) |
| Mermaid | ```mermaid | watch | show a diagram rendered client-side from a source field on every surface; the reviewer can comment on a flowchart node, edge, or the whole diagram (authoring payload unchanged) |
```single-choice), with bare JSON inside.
They render inline, in document order.") — never curly “ ”. Many chat
surfaces "smart-quote" straight quotes, which breaks JSON.parse and makes the block render as
raw text instead of the widget.```json fence — is auto-detected by its shape (a
questions array → questionnaire; items → prioritize; question +
options → single choice; and so on) and wrapped into its fence automatically.version is the string "1" for every component block (the
per-component wire schema; a number 1 is rejected).The renderable block types are exactly the palette in the index above. Nothing else renders. A fenced block with any other info-string is shown as a plain code block — silently, with no error — so a wrong tag looks like it "did nothing".
| ✅ Valid fence tags (the whole set) | ❌ NOT block types — these render as raw code |
|---|---|
```single-choice```multi-choice```open-question```prioritize```questionnaire |
```multiple-choice (the real tags are single-choice / multi-choice),
```match, ```quiz, ```poll, ```survey, ```form,
```rank, ```question, ```answer — and anything else. The renderer knows
only the tags on the left. |
```questionnaire only to GROUP 3+ tightly-related questions into one unit; a lone question
travels as its own ```single-choice / ```multi-choice / ```open-question
block with a prose lead-in. (Inside a questionnaire, the same three forms exist as question modes —
multi: true, options: [].)```prioritize component, never a question mode:
what a user chooses is a choice/questionnaire; the one thing they order is prioritize.allocate vs prioritize — magnitude, not order. prioritize
answers which comes first; allocate answers how much each gets (weights summing to a
whole). Percentages/dollars → allocate; rank → prioritize.```open-question — never demoted back to prose. A doc with real open
points is never component-less; one that settles everything weaves none.```mermaid diagram maps a structure the
reader must hold in their head (a process / flow / mechanism / sequence / state machine / architecture
/ dependency order) to a picture. Reach for one when the prose turns structurally dense, and for
two side by side to compare (before → after, A vs B) only when each side is
itself a real structure — a plain two-option pick stays a single-choice + a
sentence. It never replaces a component and
never counts against the one-primitive-per-point rule — but three linear steps stay prose; diagram only
what's genuinely hard to hold. See Mermaid.```single-choice) on its own line.The per-ambiguity forms of a question: ONE decision per fence, flat fields, no envelope. All three share
the questionnaire's answer semantics (the renderer normalizes each to a one-question form internally), so
recommended, open_field, notes, and the export shape behave exactly as documented
under Questionnaire.
| Field | single-choice | multi-choice | open-question |
|---|---|---|---|
version | required, "1" | required, "1" | required, "1" |
question | required, non-empty | required, non-empty | required, non-empty |
options | required, 2–4 strings | required, 2–6 strings | — (no options; the answer field IS the answer) |
recommended | optional, ONE option label | optional, ARRAY of option labels | — |
placeholder | — | — | optional, example text in the empty field |
open_field / context / id / routing / skill_version | optional, as under Questionnaire | context/id/routing/skill_version optional | |
```single-choice
{ "version": "1", "question": "Which auth method ships in v1?", "options": ["Magic link", "Room number + PIN"], "recommended": "Magic link" }
```
```multi-choice
{ "version": "1", "question": "Which locales at launch?", "options": ["English", "Hebrew", "Arabic"], "recommended": ["English", "Hebrew"] }
```
```open-question
{ "version": "1", "question": "What is the data-retention constraint from legal?", "placeholder": "e.g. delete 30 days after checkout" }
```
Here's the Q3 plan. Rank the regions, then answer the two open calls.
```prioritize
{ "version": "1", "items": [
{ "id": "eu", "label": "EU" },
{ "id": "us", "label": "US" },
{ "id": "apac", "label": "APAC" }
] }
```
```questionnaire
{ "version": "1", "questions": [
{ "id": "q1", "question": "Which auth method?", "options": ["PIN", "Room number", "Magic link"], "recommended": "Magic link" },
{ "id": "q2", "question": "Which regions need localization first?", "multi": true, "options": ["EU", "US", "APAC"] },
{ "id": "q3", "question": "Target launch date?", "options": [], "open_field": { "label": "Target date:" } }
] }
```
That single document renders, top to bottom: a line of prose, a draggable priority list, then a three-question form (single-select with a recommended badge, a multi-select, and a free-text field) — all from the two block types above.
| Field | Versions | Values | Gates rendering? |
|---|---|---|---|
version | The wire schema this page describes (per component block). | Always the string "1". | Yes — a block whose version isn't "1" is rejected. |
skill_version | The authoring-skill build that emitted the JSON (questionnaire only). | "1.5" today; "1.2", "1.4", … are older builds of the same skill. | No — optional metadata only. |
So "1.2" and "1.5" are not two schemas — they are two builds of the same authoring skill against the
one schema (version: "1"). New capabilities (like recommended) ship as additive,
optional fields: an older payload that omits them still renders.
A list of questions. Each question is a pick-from-options control (single- or multi-select) with an
always-present free-text "Other" escape hatch; a question with no options (options: []) is a
pure free-text field. Embed as a ```questionnaire fence, or paste a bare questionnaire JSON
object on its own (auto-detected).
| Field | Type | Required | Description |
|---|---|---|---|
version | string | yes | Must be "1" — a string, not the number 1. Any other value is rejected. |
questions | array | yes | At least one question object. See below. |
title | string | no | Shown in the form header. |
source_summary | string | no | One-line subtitle shown under the title. |
routing | object | no | Shows a return banner. If present, both sub-fields below are required. |
routing.from | string | if routing | Non-empty string. Your name or system name shown in the banner. |
routing.return_prompt | string | if routing | Instruction shown after the form, e.g. "When done, copy your answers and send them back to Claude." Must contain the literal value of routing.from — the renderer bolds it. |
skill_version | string | no | Build tag of the authoring skill that produced this JSON — currently "1.5". This is a different field from version (see "Two version fields" above). Echoed as a hidden comment in the exported answers; drives an optional "your skill is outdated" banner. Never gates rendering. |
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique identifier within this questionnaire (e.g. "q1"). Non-empty. |
question | string | yes | The question text. Non-empty. |
options | string[] | yes | Answer choices rendered as radio buttons. All must be strings. Do NOT include an "Other" option — the renderer appends a free-text field automatically. May be [] for a pure free-text question, in which case an explicit open_field is required (see below). |
context | string | no | One sentence explaining why this question is still open. Shown as a subtitle. |
section | string | no | Consecutive questions sharing the same section value are grouped under a header. Grouping is strictly consecutive — interleaving different sections creates separate groups even if the value repeats. |
open_field | object | no* | Overrides the default "Other:" free-text field label. Omit for the default. If present, label must be a non-empty string. *Required when options is [] — it becomes the question's sole input. |
open_field.label | string | if open_field | Non-empty. Examples: "I need to check with:", "Depends on:", "The reason is:". |
open_field.placeholder | string | no | Placeholder text for the free-text input, e.g. "e.g. legal team". |
multi | boolean | no | If true, renders checkboxes instead of radios. The user may pick multiple options. The export emits an Answers: bullet list. Must be a boolean — "true" or 1 are rejected. |
recommended | string | string[] | no | The option label you suggest — a string for single-select, an array of labels when multi is true. Renders a "Recommended" badge on the matching option(s). It is mark-only: it never pre-selects, and is never echoed into the answers the user sends back. The value must exactly match an existing option label; a value that doesn't match is silently ignored (no badge), and a bad recommended never rejects the questionnaire. Put the why in context. |
allow_note | boolean | no | Always omit. The app always shows a notes field. A note alone counts as a valid answer. |
{
"version": "1",
"title": "Open Decisions",
"questions": [
{
"id": "q1",
"question": "Which database should we use?",
"context": "Spec mentions PostgreSQL and MySQL but does not commit.",
"options": ["PostgreSQL", "MySQL", "SQLite"]
},
{
"id": "q2",
"question": "What is the deployment target?",
"options": ["AWS", "GCP", "Self-hosted"]
}
]
}
Exercises sections, routing, multi-select, recommended, custom open_field, and skill_version.
{
"version": "1",
"skill_version": "1.5",
"title": "Guest Check-In, Open Decisions",
"routing": {
"from": "Elad",
"return_prompt": "When done, copy your answers and send them back to Elad."
},
"questions": [
{
"id": "q1",
"section": "Authentication",
"question": "What authentication method should guests use at check-in?",
"context": "Brief: PIN vs room number not yet decided.",
"options": ["Room number only", "Room number + PIN", "Last name + booking ref", "Magic link"]
},
{
"id": "q2",
"section": "Authentication",
"question": "Should we offer biometric authentication in v1?",
"context": "Team split between v1 and defer-to-v2; deferring keeps v1 scope tight.",
"options": ["No, defer to v2", "Optional opt-in", "Required"],
"recommended": "No, defer to v2"
},
{
"id": "q3",
"section": "Compliance",
"question": "What is the data retention policy?",
"context": "Brief notes legal sign-off pending.",
"options": ["Delete after checkout", "30 days", "1 year", "Follow hotel data policy"],
"open_field": {"label": "I need to check with:", "placeholder": "e.g. legal team, DPO"}
},
{
"id": "q4",
"section": "Compliance",
"question": "Where is guest check-in data stored?",
"context": "Brief lists region-local vs centralized as TBD.",
"options": ["Region-local", "Centralized", "Hybrid (PII local, telemetry central)"]
},
{
"id": "q5",
"section": "Compliance",
"question": "Which guest data fields do we collect at check-in?",
"context": "Brief is silent on the field list.",
"multi": true,
"options": ["Full name", "ID/passport number", "Phone", "Email", "License plate", "Estimated arrival time"]
}
]
}
version must be the string "1". The number 1 is rejected.questions must be a non-empty array. An empty [] is rejected.id, non-empty string question, and an options array (which may be empty — see next rule).options must be strings. A non-string item rejects the entire questionnaire. An empty options: [] is valid only when an explicit open_field is also present (a pure free-text question); empty options with no open_field is rejected.options. The renderer always appends a free-text field automatically. Including one creates a duplicate.open_field is present, open_field.label must be a non-empty string. An empty string "" is rejected.routing is present, routing.from must be a non-empty string and routing.return_prompt must be a string. routing.return_prompt should contain the literal value of routing.from (the renderer bolds it).section is present on a question, it must be a string.multi is present, it must be a boolean — not a string or number.recommended is never required and never rejects a questionnaire. When set, it must be a string (or, for a multi question, an array of strings) that exactly matches an option label; any value that doesn't match an option is silently dropped (the badge just won't show). It is mark-only and is not echoed into the answers.id should be unique. Duplicate ids are not rejected, but the answer state is keyed by id, so two questions sharing an id collide onto one answer — keep them unique.skill_version is present, it must be a string. It does not affect validation or rendering.section value that are not adjacent produce separate groups.After completing the form, the user copies a Markdown block. Format per answered question:
**Q: <question text>** Answer: <picked option> ← or "Answers:\n- pick1\n- pick2" for multi-select Note: <free text> ← only when the user added a note ---
Unanswered questions are omitted. Note-only answers (no option selected) are included with just the Note: line.
A short list the user ranks by dragging or with ▲/▼ controls, top = highest priority,
plus one optional note for the whole list. The user can also edit the list itself —
delete an item (reversible; the deletion reports back explicitly) and add items of their
own, ranked inside the same list. Unlike a questionnaire, a prioritize block carries a
default — the order you author — so it always reports back what the user
did, including "kept the order unchanged" (an affirmative endorsement). Embed it as a
```prioritize fence inside a document; there is no bare-JSON shortcut.
| Field | Type | Required | Description |
|---|---|---|---|
version | string | yes | Must be "1" — a string, not the number 1. |
items | array | yes | Non-empty array of item objects with unique ids (below). The array order is your suggested order — the starting state the user edits. |
title | string | no | Shown in the header and the exported header. |
instruction | string | no | Helper line under the title, e.g. "Order by priority — top ships first." |
note_field | object | no | Config for the one optional block-level note. Defaults to enabled with the label "Add a note (optional)". Set {"enabled": false} to hide it. |
note_field.enabled | boolean | no | Whether the note field shows. Default true. |
note_field.label | string | no | Prompt for the note field. Default "Add a note (optional)". |
routing | object | no | Same shape as the questionnaire's routing (from + return_prompt). Include only when sending to someone else. |
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Stable, unique across items (duplicate ids are rejected). Keys the answer order. |
label | string | yes | The item text the user sees and reorders. Non-empty. |
context | string | no | A quiet one-line caption; also appended to the exported line as — {context}. |
{
"version": "1",
"title": "Q3 rollout order",
"instruction": "Order by priority — top ships first.",
"items": [
{"id": "eu", "label": "EU", "context": "largest existing waitlist"},
{"id": "us", "label": "US", "context": "highest revenue per seat"},
{"id": "apac", "label": "APAC", "context": "needs localization first"}
],
"note_field": {"label": "Anything blocking this order?"},
"routing": {"from": "Elad", "return_prompt": "Act in this order."}
}
version must be the string "1".items must be a non-empty array. Each item must have a non-empty string id and a non-empty string label; context, if present, must be a string.ids must be unique — a duplicate id rejects the whole block (ids key both the answer order and the reorder animation).note_field is present, note_field.enabled (if set) must be a boolean and note_field.label (if set) must be a string.routing is present, routing.from must be a non-empty string and routing.return_prompt must be a string.A prioritize block always serializes its resolved list, tagged with what the user did:
reader edited the list (deleted and/or added items), reader reordered (moves only),
or reader kept the order unchanged.
Priority order (reader edited the list) → Elad 1. US — highest revenue per seat 2. Localization staffing — (added by reader) 3. EU — largest existing waitlist Removed by reader: APAC Note: APAC is off until localization is staffed. Act in this order.
The header carries the tag (Priority order ({tag}) → {routing.from} with routing,
## {title} ({tag}) with a title, else ({tag}) on its own line). The numbered list
is the user's final list — your items minus any they deleted, plus their own additions — each line
appending — {context} when the item has one; a user-added line is marked
— (added by reader). A Removed by reader: … line appears only when the user deleted
authored items — treat those as explicitly declined. A trailing
Note: … line appears only when the user wrote a note; routing.return_prompt is the
footer when routing is present. Act on the list either way — "kept unchanged" is an affirmative
endorsement of your suggested order.
A short list of categories the user splits a fixed whole across by dragging weighted bars
that always sum to total (100% by default), plus one optional note. The user can also
edit the list itself — delete a category (reversible; its weight redistributes proportionally
so the sum stays total, and the deletion reports back explicitly) and add categories of
their own, entering at weight 0 inside the same split. The magnitude sibling of
Prioritize (order → weight): like Prioritize it carries a default (the split you authored),
so it always reports what the user did — including "kept unchanged". Use it when the point is
the proportions — a budget across teams, effort across workstreams, headcount across functions.
Embedded only inside a ```allocate fence.
Each item carries id (stable, unique), label, and a numeric weight
(its share of total; rescaled so the set sums to total), plus an optional
context. The envelope also takes an optional total (default 100), unit
("%" default, "$", "pts", …), title,
instruction, note_field, and routing.
```allocate
{
"version": "1",
"title": "Q3 engineering capacity",
"unit": "%",
"total": 100,
"items": [
{ "id": "product", "label": "Product — new features", "weight": 35 },
{ "id": "growth", "label": "Growth & GTM", "weight": 25 },
{ "id": "infra", "label": "Infra & reliability", "weight": 15, "context": "accumulated tech debt" },
{ "id": "support", "label": "Customer support", "weight": 15 },
{ "id": "hiring", "label": "Hiring", "weight": 10 }
]
}
```
An allocate block always serializes its resolved split, tagged
(reader edited the list — deleted and/or added categories — / reader reweighted /
reader kept the allocation unchanged): one bullet per category in the user's final list —
your categories minus any they deleted, plus their own additions — with its final weight + unit and a
(was N{unit}) / (unchanged) delta on your categories; a user-added line is marked
— (added by reader) instead of a delta. A Removed by reader: … line appears only
when the user deleted authored categories — treat those as explicitly declined. Then a closing
Total: {total}{unit} line, an optional Note: …, and the routing.return_prompt
footer when routing is present. Act on the resolved numbers either way.
An embed rich media component: a YouTube video shown inside the
document. Like Mermaid it asks nothing — it carries no decision and produces no answer;
the reviewer's feedback arrives as ordinary comments on its caption and the prose around it. Reach for it
whenever the document references a video; never hand-write a Markdown image link
or an img.youtube.com URL — the block renders a real player.
The payload is { "version": "1", "id": "<11-char id>", "title": "…" } — pass the
11-character video id only (matches ^[A-Za-z0-9_-]{11}$), never a URL or
query string; the site builds the embed URL. An optional thumbnail may carry a small base64
data: image (~320×180, under ~40 KB) as the preview facade — never a live image URL.
Routed-only playback: the player loads click-to-load on the routed /r/<id>
reviewer surface (via the privacy-preserving youtube-nocookie origin); on locally-pasted and
#s= documents the block stays an inert facade and the page never contacts YouTube.
```youtube
{
"version": "1",
"id": "dQw4w9WgXcQ",
"title": "Onboarding walkthrough (v2) — the 90-second tour"
}
```
A Mermaid diagram the reviewer reads inside the document — a flowchart, sequence,
ER, state, or gantt diagram. Unlike the response components it asks nothing: it is a
comprehension aid. The authoring payload is source-only —
{ "version": "1", "source": "graph TD; A-->B;", "title": "…" } — rendered client-side on
every surface; the reviewer can comment on a flowchart node, edge, or the
whole diagram (all derived from your source; you never mark nodes).
When to reach for one — the reach trigger. Emit a ```mermaid block
whenever you are describing something the reader must hold in their head as a structure —
a process, flow, mechanism, sequence, state machine, decision tree, architecture, dependency order, or
timeline. Place it at the moment the prose turns structurally dense, right after the
sentence it clarifies. The power of two: when the doc weighs a change or options,
two diagrams side by side (before → after, current vs proposed, A vs B) is often the
highest-value move — but earn it: each side must be structurally non-trivial (a real
flow / architecture / state per side). A plain two-option pick ("lead with A or B", "Tue/Thu vs Mon/Wed")
stays a single-choice + a sentence, not two boxes-and-arrows; the decision itself still rides
a component after the pair.
Restraint: a diagram earns its place only when the structure is genuinely hard to hold
in prose — three linear steps stay prose. The payoff: because the flowchart is
commentable per node and edge, "this mechanism is unclear" becomes a comment pinned to the exact box or
arrow that's wrong.
```mermaid
{
"version": "1",
"source": "graph TD; R[Read request] --> C{In edge cache?}; C -->|hit| S[Serve cached]; C -->|miss| O{Origin healthy?}; O -->|healthy| F[Fetch, cache, serve]; O -->|slow| B[Serve stale + refresh in background]; O -->|no cached copy| W[Block on origin];",
"title": "Read path — cache, origin, and the stale fallback"
}
```
The reviewer can then comment on node B ("the 'slow' branch should also
cover a 5xx, not just latency") — pinned to the exact box, not a vague note. A diagram the reviewer left
untouched contributes nothing to the copy-back. Full guidance + the comparison-pair example:
/ask.md §4.7.
New component types ship as new fence tags with their own JSON schema, additively — without changing the ones above. Code to the family, not to one example: check the component index near the top of this page for the tags that exist today, and pick the component that matches the decision you need. When a new component lands, it appears here as another section; your integration picks it up by adding the new tag, nothing else changes.
See also: /skills/question-extractor.md — the question-extractor skill that generates the questionnaire component from any input text. (Other components are authored directly against this schema.)