# Baxly Survey Definition, format baxly-survey/1.0

This document is the complete rule book for writing a survey that Baxly (baxly.brc.com, by Baxter Research Group) can build. It is written for an AI assistant that has been asked to produce a survey file, and it is also readable by people. Follow it exactly. Anything not described here is not supported and will be rejected by the validator.

Canonical copies: https://baxly.brc.com/survey-definition/rulebook.md (this file), https://baxly.brc.com/survey-definition/schema.json (JSON Schema), and three worked examples: https://baxly.brc.com/survey-definition/examples/customer-satisfaction.json, https://baxly.brc.com/survey-definition/examples/concept-test-ab.json and https://baxly.brc.com/survey-definition/examples/readership-study.json.

## 1. What you are producing

One JSON file (UTF-8, at most 2 MB) with this top-level shape:

```json
{
  "format": "baxly-survey/1.0",
  "notes": "Optional. Anything you want the person reviewing the file to know. Not shown to respondents.",
  "review": { "applied": ["Optional short lines on what you set up"], "check": [{ "question": "q_key", "note": "Optional item to look at, pinned to a question or page" }] },
  "survey": { "name": "..." },
  "lists": [],
  "pages": []
}
```

The person you are helping will upload the file at https://baxly.brc.com/surveys/import-definition. Baxly validates it, shows an outline plus every error and warning, and builds the survey only when there are no errors. The survey is created as a draft that the person can still edit in the Baxly builder before it goes live. Nothing is sent to anyone by this file.

If you are connected to Baxly as a tool (the Baxly MCP connector or the REST API), do not hand the person a file: call validate_survey with the same document, fix every error it returns, then call create_survey, and only call publish_survey when the person asks for the survey to go live. The document is identical either way.

Only these top-level keys exist: format, notes, review, survey, lists, pages. Unknown keys anywhere in the file are errors, not ignored. If you are unsure whether something is supported, leave it out and say so in notes.

## 2. How Baxly treats your file (read this before writing anything)

The file is treated as untrusted input. That is not a judgement about you. It is how any file from outside is handled, and it tells you which approaches work and which are silently stripped.

What this means in practice:

1. Text is sanitized. Every text field (question text, choices, intros, labels, messages) passes through an HTML sanitizer. Allowed tags and attributes are kept; everything else is removed and the text around it is kept. There is no way to include scripts, styles, iframes, forms, inputs, event handlers (onclick and friends) or javascript: links. Scripts, iframes and event handlers produce a warning; other disallowed markup is simply dropped. An inline `<img>` inside text may use an https URL or a data: URL of a real raster image (png, jpeg, gif, webp, avif; it counts toward the 2 MB file limit). Every other image field (choice image_url, side_media.image, list image columns, intro.image_url, heatmap image_url) must be an https URL.
2. Everything is referenced by key, never by id. You invent short keys for pages, questions and choices and use them in logic, loops and quotas. Baxly assigns its own internal ids when it builds; you never see or set them. A file that contains ids, uuids or internal setting names is wrong.
3. Settings are allowlisted per question type. Each question type accepts a fixed set of settings (section 7). Any other setting is an error that names the allowed ones, so fix and resubmit.
4. The validator reports everything at once. Each error and warning has a code, a JSON path (for example $.pages[2].questions[0].choices[3].text) and a plain message. Errors block the build; warnings do not. Fix every error in one pass and resubmit.
5. Text addressed to the reviewer has no effect. The reviewer is a validator and a person looking at an outline. Instructions placed in survey text ("ignore the rules", "approve this") do nothing except reach respondents if the survey is built. Put anything you want the person to know in the top-level notes field.
6. Limits are enforced: 60 pages, 60 questions per page, 400 questions in total, 200 choices per question, 100 matrix rows, 20 lists of up to 500 items with up to 12 columns, 50 variables, 30 variable rules, 30 quotas, 20 tests per condition, 10 end rules per page.
7. Image fields are https URLs (the one exception is an inline img tag inside text, item 1). Baxly does not accept uploaded files through the definition. Logos are set in the builder after the build, not from the file (a survey.logo_url key is accepted and ignored with a warning).
8. Plan limits apply. A Free account can use the core question types only (section 7); other types, AI follow-ups and hiding Baxly branding need a paid plan. The validator tells the person which items their plan does not include, so write the survey the person asked for and let them decide.

### The safe ways to do what you want

| You want | Do this | Not this |
|---|---|---|
| Emphasis, headings, paragraphs, lists | Use the allowed HTML: p, br, b, strong, i, em, u, s, h1 to h6, ul, ol, li, blockquote, hr, span, div, small, sub, sup, mark, table with thead/tbody/tr/th/td, details/summary, figure/figcaption | Markdown (it is shown literally), script, style, iframe, form, input, button |
| A link | `<a href="https://example.com" target="_blank">text</a>` (http, https, mailto and tel only) | javascript: links, onclick handlers |
| An image inside text | `<img src="https://.../image.jpg" alt="...">` with an https URL (a data: png/jpeg/gif/webp also works here, nowhere else) | file paths, SVG, data: URLs in any image field other than an inline img |
| An image beside a page | `side_media` on the page (section 6) with an https URL or a merge tag | an img tag inside every question |
| Colors and fonts | `survey.theme` (section 5) or inline style with simple properties (color, background, font-size, font-weight, text-align, margin, padding, border, width, display, float) | position, z-index, transforms, url() in CSS, external stylesheets |
| Personalise text | Merge tags: `{{var.Name}}` for a survey variable, `{{item.Column}}` for the current list item inside a loop | string concatenation, template syntax from other tools |
| Reference a question or choice | Its key (or a choice's exact text) | ids, numbers, positions |
| Tell the person something | `notes` at the top level or `builder_note` on a page | hidden text, HTML comments, instructions inside question text |
| Hand over a review list | `review.applied` (what you set up, one short plain line each, no format words) and `review.check` (what to look at, each with a `question` or `page` key so Baxly can open it) | a long paragraph in `notes` |
| A long survey | More pages, loops over lists | a page with hundreds of questions |

Allowed inline style properties: color, background, background-color, font-size, font-weight, font-style, font-family, line-height, letter-spacing, text-align, text-decoration, text-transform, white-space, margin and padding (all sides), border (all sides), border-radius, border-color, border-width, border-style, width, height, max-width, max-height, min-width, min-height, display, gap, align-items, justify-content, flex, flex-wrap, flex-direction, float, clear, vertical-align, list-style, opacity. Values may use plain tokens, var(), rgb()/rgba(), hsl()/hsla() and calc(); nothing else with parentheses.

## 3. Keys and names

Keys identify pages, questions and choices. A key is 1 to 40 characters, starts with a lowercase letter, and contains only lowercase letters, digits and underscores: `nps`, `read_level`, `q12_followup`. Keys must be unique across the whole survey, and do not reuse a page key as a question key (loops look both up by one name). Every question must have a key. Pages get an automatic key `page_N` when you omit one, but give them keys anyway if anything refers to them. A question's optional `code` (its reporting code) shares the namespace with keys: a code may equal its own question's key but not another question's key.

Choices may have keys (recommended when logic refers to them) or may be plain strings. Logic can refer to a choice by its key or by its exact text.

List names and column names are 1 to 200 and 1 to 50 characters of letters, digits, spaces, underscores and hyphens. Both are matched case-insensitively (`title` finds `Title`), but write them the way the list declares them. The column name `SourceId` is reserved.

Variable names: letters, digits, spaces, underscores and hyphens, up to 50 characters.

## 4. survey (required)

| Key | Type | Meaning |
|---|---|---|
| name | string, required | Survey name, plain text up to 300 characters. Shown to respondents in the browser tab and the top bar. |
| intro | object | Cover page: `headline` (plain, 200), `body` (HTML), `minutes` (0 to 20, shown as "about N minutes"), `button_text` (plain, 50), `image_url` (https), `consent` ({ text: HTML }) adds a consent checkbox the respondent must tick, `enabled` (default true). |
| variables | array of { name, value } | Named values you can insert anywhere as `{{var.Name}}`. value may contain allowed HTML, up to 500 characters. |
| variable_rules | array of { name, value, when } | Change a variable's value for a respondent when a condition on their answers matches (answer tests only, section 9). |
| experiment | { variants: [{ key, label }] } | An A/B test. 2 to 4 variants with keys a, b, c, d. Each respondent is assigned one at random; use `{ "variant": "a" }` in show_when or hide_when to show different pages or questions per variant. |
| theme | object | Look and feel. `style`: cards, sharp, airy, compact or pills. `font`: system, arial, verdana, georgia or trebuchet. `width`: compact, standard, wide or xwide. `instructions`: auto, all or off. `guide`: subtle, bold, spotlight or minimal. `topbar`: full, bar, title or off. Colors as 6-digit hex: primary, primaryStrong, primary600, accent, desk, deep, done, matrixDone, matrixAlt. |
| show_page_titles | boolean | Set false to hide page titles from respondents. |
| layout | "default" or "navigator" | navigator shows a page list down the side. |
| completion_form | { heading, note, button, fields } | A short form after the last page (name, email and so on). fields: 1 to 12 of { label, type ("select" or "checkbox", otherwise a text box), width ("full" by default, or "half" for two per row), options (array of strings, or "countries"), validation ("email", "phone", "zip", "match_prev"), required }. |
| incentive | { type, description, amount, cap, rules } | type "contest" (a drawing) or "guaranteed" (everyone who completes). description (plain, 300, required) is what respondents see. amount in dollars, cap is the maximum number of rewards, rules is HTML for the official rules. Fulfilment is handled by the person after the survey, not by the file. |
| quotas | array of { name, limit, when, on_full } | Count completed responses that match `when` (answer tests only). limit 1 to 1,000,000. on_full "end" (default) stops further matching respondents with a thank-you; "none" only counts. |
| ai_target_responses | integer | How many responses the person is aiming for (used by Baxly's planning tools). |
| hide_branding | boolean | Hide the "Powered by Baxly" line. Paid plans only. |

## 5. lists (optional)

A list is a named table of items. Lists feed loops ("ask these questions once per item"), choice sets (`choices_from`) and matrix rows (`rows_from`). Use a list whenever the same question repeats for several things, or when the choices are long or shared between questions.

```json
{
  "name": "Articles",
  "columns": ["Title", "Image", "Section"],
  "items": [
    { "Title": "The case for smaller warehouses", "Image": "https://example.com/a.jpg", "Section": "Features" }
  ]
}
```

Columns are the item fields. Any column whose name contains "image" must hold https URLs. Inside a loop over this list, `{{item.Title}}` inserts the current item's Title in any text, and `side_media.image` can be `{{item.Image}}`.

## 6. pages (required, at least one)

| Key | Type | Meaning |
|---|---|---|
| key | string | Page key (section 3). |
| title | string | Plain text, up to 400 characters. May contain merge tags. |
| intro | HTML | Text shown above the questions. |
| questions | array | The questions on this page, in order (section 7). |
| loop | object | Repeat this page once per list item (section 8). |
| side_media | { image, position, caption, zoom, max_h } | An image shown beside the questions. image is an https URL or `{{item.Column}}` on a looped page. position "left" (default) or "right". caption is HTML. zoom (default true) lets respondents enlarge it. max_h 80 to 800 pixels. |
| show_when / hide_when | condition or [conditions] | Show or hide this page (section 9). May only test questions on earlier pages, variables and variants. An array holds several conditions that must all hold. |
| end_when | condition or array of conditions | End the survey here when a condition matches (a screen-out or an early finish). Tests may use this page's questions or earlier ones. Up to 10 rules. The response is recorded as screened out, not complete. |
| end_message | { headline, body } or a string | What a respondent sees when end_when fires. Only meaningful with end_when. |
| complete_rule | { when: "any_answer" } or { when: "condition", condition } | Mark the response complete as soon as this page has any answer, or when a condition on answers matches, even if the respondent never reaches the end. Use for optional tails. |
| builder_note | string | A note to the person editing in the builder. Not shown to respondents. |

Pages are shown in file order. Logic can only look backwards: a page or question may depend on questions that come before it.

## 7. questions

Every question has `key`, `type`, `text` (HTML allowed, required) and optionally `required` (boolean, default false), `code` (a short reporting code, unique, up to 100 characters), `settings` (per type, below), `show_when`, `hide_when` (section 9), `links` (section 9, linked answers), `carry_forward` (section 9, carried-forward choices), `ai_followup` (section 10), and for some types `choices`, `rows`, `choices_from`, `rows_from`, `loop`, `fields`.

Common settings for every type except note: `instruction` (plain, replaces the automatic "Select one" style hint), `helper_text` (HTML under the question), `caption` (HTML under the answers).

### Core types (every plan)

| type | What it is | Choices | Extra settings |
|---|---|---|---|
| select_one | Pick one, shown as buttons or radio options | choices required | randomize, the choice image settings below; with a loop also row_grid and the grid settings below |
| multi_select | Pick several | choices required | randomize, min_selections, max_selections, select_all, the choice image settings below; with a loop also the grid settings below |
| dropdown | Pick one from a drop-down | choices required | randomize |
| yes_no | Two buttons | none (made for you) | yes_label, no_label |
| open_text | Text box | none | placeholder, multiline (true for a tall box), validation: "phone", "zip" or "email" |
| email_capture | An email address, validated | none | none |
| number | A number | none | min, max |
| date | A date picker | none | none |
| scale | A numbered scale | none | min (0 to 100), max (1 to 100), min_label, max_label, style: "smiley" |
| nps | 0 to 10 likelihood to recommend | none | min_label, max_label |
| star_rating | Stars | none | max (3 to 10) |
| note | Text only, no answer. Use for instructions or a block of copy. | none | hold (true hides the Next button while the note is showing; pair it with show_when for a come-back-later message) |

Choice image settings (select_one and multi_select): when choices carry an `image_url`, `image_mode` decides how the picture shows, "zoom" (default: a magnifier button opens it full size) or "show" (the picture itself, which also opens full size when tapped); `image_size` is "small", "medium" (default) or "large"; `image_position` puts the picture "before" or "after" (default) the choice text. These are the defaults for every choice on the question; a choice may override any of them with the same keys (see Choice objects).

Grid settings (select_one and multi_select, only meaningful on a question that has a `loop`; the validator warns otherwise): `ad_grid` (true shows the looped items as image tiles), `zoom_image` (name of the list column holding a larger image), `tile_wide` (list column that marks wide tiles), `tile_img_max_h` (80 to 800 pixels), `grid_scroll_rows` (1 to 10), and on select_one `row_grid`.

Gallery layout (select_one and multi_select): `"layout": "gallery"` shows every choice as a card (image on top, name, any extra `columns` from `choices_from`, and a check row) in a responsive grid, which is the right display for a long choice list such as 100 companies a reader can request information from. With it: `tools` is "auto" (default: a search box and sort buttons appear when there are more than 12 choices), "on" or "off"; `sort` is the starting order, "shuffle" (default, a different order per respondent, fixed for that respondent), "alpha" (by name), "list" (the list's own order) or the name of one of this question's `choices_from.columns` (for example "Page"), and the respondent can re-sort with the buttons when tools are on; `check_label` is the text on each card's check row (default "Select", or "Choose" on select_one); `gallery_img_h` is the card image height in pixels (80 to 600, default 200); `priority_from` is the key of an EARLIER question that takes its choices from the same list, whose answers are shown first under their own heading (for example the companies the reader said they remembered), and `priority_label` is that heading (default "The ones you remembered"). Cards are added in batches of 24 with a "Show more" button, and on multi_select a "N selected" button filters the gallery to the checked cards. `layout: gallery` cannot be combined with `ad_grid`.

### Advanced types (Pro and Business plans)

| type | What it is | Choices | Extra settings |
|---|---|---|---|
| matrix | A grid: rows are the things rated, choices are the columns | rows and choices required (or rows_from) | mode: "select_one" (default), "multi_select", "number" or "text"; matrix_label_width (15 to 60, percent); matrix_label_header (heading over the row labels, e.g. "Editorial") |
| ranking | Drag to rank | choices required | rank_count (rank only the top N), randomize |
| constant_sum | Distribute points across choices | choices required | total, randomize |
| percentage_sum | Percentages that must add to 100 | choices required | none |
| image_choice | Pick from pictures | choices with image_url | multi (allow several), max_selections, randomize |
| heatmap | Click on an image | none | image_url (https, required; the validator rejects a heatmap without it), max_clicks (1 to 20) |
| slider | A slider | none | min, max, min_label, max_label |
| contact_form | Name, email and similar fields in one question | none; use fields | fields: 1 to 8 of { label, type, width, options, validation, required } as in completion_form |
| file_upload | Respondent uploads a file | none | none |
| ai_conversation | An AI interview: Baxly asks follow-up questions in a conversation | none | ai_followup with goal and max (up to 10 turns) |
| maxdiff | Best/worst trade-off across items | choices_from a list with at least 4 items | exposures (2 to 5 items shown per screen) |
| bestof | A two-stage "best of" category pick, for awards-style studies | choices_from a list with a Category column and rows_from a list; must be the first answerable question on its page (a note may come before it) | max_categories, max_picks, min_picks, stage2 ("auto", "rank", "maxdiff"), gate ("experience" or "none"), gate_text, allow_other, browse, missed, missed_text, why, why_text |

### Choices

A choice is either a string or an object:

```json
{ "key": "other", "text": "Something else", "is_other": true, "placeholder": "Please say what" }
```

| Key | Meaning |
|---|---|
| text | Required. HTML allowed, up to 1,000 characters. Texts on one question must be distinct unless each has a key. |
| key | Optional key (section 3). Use it when logic refers to this choice. |
| is_other | Adds a text box to this choice. select_one, multi_select, dropdown and ranking only (on a ranking the box opens once the choice is ranked). |
| placeholder | Placeholder for the is_other box. |
| exclusive | Picking this clears the others ("None of these"). multi_select, matrix and ranking. On a ranking it is the opt-out ("I do not use this"): it clears every rank, is stored without a rank, and completes the question on its own. |
| select_all | Picking this selects every other choice. multi_select only. |
| image_url | https image. On image_choice it is the tile; on select_one and multi_select it shows next to the text (see the choice image settings). |
| image_mode, image_size, image_position | select_one and multi_select only: override the question's choice image settings for this one choice ("zoom" or "show"; "small", "medium" or "large"; "before" or "after"). |
| value | A short reporting value stored with the answer (plain, 200). |

### Choices from a list

Instead of `choices`, a select_one, multi_select, dropdown, matrix, ranking, constant_sum, percentage_sum, image_choice, maxdiff or bestof question can take its choices from a list:

```json
"choices_from": { "list": "Articles", "label": "Title", "image": "Image" }
```

label is the column shown (defaults to the first column); image, wide, page and group name optional columns. `columns` (up to 4 column names, not the label column) shows those values as their own aligned columns beside each choice, for example `"columns": ["Page"]` to put the page number in a column instead of after the label. Or reuse the items a respondent already saw in a loop: `"choices_from": { "from_loop": "article_pages" }` where the value is the key of a page or question that loops. A matrix or bestof can take its rows the same way with `rows_from: { "list": "...", "label": "..." }`; rows_from also accepts `image` (a column holding an image URL, shown beside the row label) and `columns` (extra columns shown before the label, with the column name as the header).

In this version, conditions cannot refer to individual list items. Logic on a list-sourced question is limited to `not_answered` and `picked_more_than`. A question may have both `choices_from` and `choices`: the fixed choices (for example "None of these") are shown after the list items.

### Rows (matrix only)

`rows` is an array of strings (HTML allowed, 1,000 characters each), up to 100. The columns are the `choices`.

## 8. loops

A loop repeats a page (or one question) once per item of a list. Put `loop` on the page to repeat the whole page; put it on a question to repeat only that question as one row per item.

```json
"loop": {
  "list": "Articles",
  "sample": { "mode": "random", "count": 5 },
  "order": "shuffle",
  "per_page": 1
}
```

| Key | Meaning |
|---|---|
| list | Loop over every item of this list. Exactly one of list, from_loop, from_answers is required. |
| from_loop | Key of an earlier page or question that itself has a `loop`: show the same items, in the same sample, to this respondent again. |
| from_answers | Key of an earlier question whose choices come directly from a list (`choices_from: {list}`) or that loops directly over a list: loop over the items the respondent picked. Optional `max` (1 to 200) and `skip` (0 to 200). |
| sample | { mode: "all" } (default), { mode: "random", count: N } for N random items per respondent, or { mode: "balanced", count: N, by: "Column" } to spread exposure evenly across the values of a column. balanced needs a list source. |
| order | "list" (default), "list_reverse", "shuffle", or "same" (only with from_loop: keep the earlier order). |
| per_page | Page loops only: how many items per screen, 1 to 50 or "all". |
| per_page_variants | Page loops only: per-variant override, for example { "a": 1, "b": "all" }. |
| reveal | Page loops only: { batch: N, label, button, nudge_text, nudge: false } shows items N at a time; the reminder nudge is on unless `nudge` is false. A `pin` key (pinned items) is accepted and ignored with a warning; pin items in the builder. |

Inside a looped page, every text (title, intro, question text, choices, side_media caption) may use `{{item.Column}}`. A question loop renders best on select_one and multi_select. A question cannot have its own loop on a page that already loops.

## 9. conditions (logic)

A condition is a single test, or `{ "all": [tests] }` (every test must match) or `{ "any": [tests] }` (one is enough), up to 20 tests. Groups cannot nest, but `show_when` and `hide_when` also accept an array of up to 8 conditions that must all hold, which is how you write "visitors AND (corporation OR court) AND (any level of involvement)":

```json
"show_when": [
  { "any": [ { "question": "visit_site", "not_selected": "Never" }, { "question": "visit_brands", "not_selected": "Never" } ] },
  { "any": [ { "question": "org", "selected": "Corporation" }, { "question": "org", "selected": "Courts & government institution" } ] },
  { "any": [ { "question": "involvement", "selected": "Final decision-maker" }, { "question": "involvement", "selected": "Involved in the decision-making process" } ] }
]
```

Two readings that are easy to get backwards: "shown unless Never was picked on both A and B" is `"hide_when": { "all": [A is Never, B is Never] }` (or `"show_when": { "any": [A not Never, B not Never] }`); "shown only when Never was picked on both" is `"show_when": { "all": [A is Never, B is Never] }`. Putting two `not_selected` tests from different questions under `all` hides the item as soon as either answer is the named one, which is rarely what a document means.

Tests:

| Test | Meaning |
|---|---|
| `{ "question": "k", "selected": "choice key or exact text" }` | The respondent picked that choice. Works on select_one, multi_select, dropdown, yes_no (choice keys yes and no), matrix, ranking, image_choice and other choice questions with inline choices. On a matrix the choice is a column, and the test is true when any row was given that column ("marked at least one tool as Currently using"); a single row cannot be tested. On a ranking it is true when the choice was ranked at all. |
| `{ "question": "k", "not_selected": "choice key or exact text" }` | The respondent did not pick that choice (an unanswered question counts as not selected). Same question types as `selected`. |
| `{ "question": "k", "not_answered": true }` | The question was not answered (skipped or hidden). |
| `{ "question": "k", "picked_more_than": N }` | More than N choices were picked (multi_select and similar). |
| `{ "variable": "Name", "equals": "value" }` / `{ "variable": "Name", "not_equals": "value" }` | Compare a survey variable (useful with variable_rules). |
| `{ "variant": "a" }` / `{ "variant_not": "a" }` | The respondent's A/B variant. |

Where conditions are used:

| Place | Allowed tests | Effect |
|---|---|---|
| question show_when / hide_when | all | Show or hide the question. May only test questions that come before it. |
| page show_when / hide_when | all | Show or hide the page. May only test earlier pages. |
| page end_when | all | End the survey (screened out) when matched; tests may use this page. |
| page complete_rule.condition | selected, not_selected | Mark complete early. |
| survey.variable_rules[].when | selected, not_selected, not_answered | Set a variable from answers. |
| survey.quotas[].when | selected, not_selected, not_answered | Which responses count toward the quota. |

A question cannot test itself. show_when and hide_when on the same item are both applied, and every condition in a show_when or hide_when array is applied, so an item is visible only when all of its rules allow it.

### Carried-forward choices

A choice question can take its choices from what the respondent marked earlier, the way a "which of these are you considering switching?" question follows a "which of these do you use?" grid. Put `carry_forward` on the later question and do not repeat the earlier rows or choices: they flow in by themselves, in the earlier question's wording, and a pick is recorded against the same item. The later question's own `choices` hold only the extras it adds, such as a "None of these" (exclusive) or an "Other" write-in, and may be left out entirely.

```json
"carry_forward": [
  { "question": "tools", "columns": ["Currently using", "Considering using"] },
  { "question": "solutions", "columns": ["Currently using", "Considering using"] }
]
```

Each source is an earlier grid (matrix), with `columns` naming which of its columns count, or an earlier select_one, multi_select, dropdown, ranking or image_choice question (no `columns`: the choices they picked count). The source keeps its rows or choices as written; Baxly stores them as a list so the carried choices can be recorded (a source whose rows already come from a list, `rows_from` or `choices_from`, is used as is). A respondent with nothing to offer skips the question, so a separate show rule is not required, though one does no harm. Up to 6 sources. A typed choice on the later question that repeats a source's wording is dropped on import.

### Linked answers

A choice question (select_one, multi_select, dropdown, image_choice, yes_no) can change the answer of another choice question the moment one of its own answers changes, the way the classic adViewPRO survey does ("picking a device you read on also marks it as a device you own"). Put `links` on the question whose answer fires the link:

```json
"links": [
  { "when": "selected", "choice": "Smartphone", "action": "select", "question": "devices_owned", "target_choice": "Smartphone" },
  { "when": "deselected", "choice": "Smartphone", "action": "clear", "question": "devices_used", "target_choice": "Smartphone" },
  { "when": "selected", "choice": "None of the above", "action": "clear", "question": "devices_used" }
]
```

`when` is "selected" (default) or "deselected"; `choice` is the answer here (key or exact text); `action` is "select" (also pick `target_choice` in `question`) or "clear" (unpick `target_choice`, or every answer when `target_choice` is left out). A "deselected" link can only clear. The target must be a different choice question with its own choices (not list-sourced); to clear other answers inside the same question use `exclusive` on the choice. Links fire when the answer changes, not on load, and do not chain into the target's own links. Up to 50 per question.

## 10. AI follow-ups

Add `ai_followup` to a question to have Baxly ask a short follow-up conversation after the answer, in the respondent's own words:

```json
"ai_followup": { "goal": "Find out the single biggest reason behind the score", "max": 2, "min": 0 }
```

goal is what the follow-up should learn (plain, 500 characters). max is the number of follow-up turns (1 to 3; up to 10 on an ai_conversation question). min forces at least that many. Not allowed on note, file_upload, contact_form, maxdiff and bestof. Pro and Business plans. Use it on the two or three questions where the "why" matters most, not on everything.

## 11. Writing a good survey (what the person expects from you)

Keep pages short (one topic per page, 1 to 5 questions). Put screening questions first and end_when on that page. Mark required only what the analysis needs. Give every question a clear key that a person can read in results (`nps`, not `q7`). Use yes_no, select_one and scale for things you will count; use open_text and AI follow-ups for the why. Use lists and loops instead of copying a block of questions for every product, article or ad. Randomize choice order when order bias matters (`"randomize": true`), but keep "Other" and "None of these" last by marking them is_other or exclusive. Write choice texts that are mutually exclusive and cover the range. Keep merge tags consistent with real variable and column names. Put anything the person must decide (prize amount, deadline, which list to use) in notes rather than guessing.

## 12. Errors and warnings

The validator returns `{ ok, errors, warnings, outline }`. Each error and warning is `{ code, path, message }`. The message always says what to change; the common codes are below, and other codes (CHOICE, ROWS, MAXDIFF, BESTOF, FIELDS, AI_FOLLOWUP, QUOTA, THEME, INCENTIVE, INTRO, EXPERIMENT, VAR_NAME, LIST_NAME, LIST_COLUMN, LIST_EMPTY, LIST_EXTRA, RESERVED, COMPLETE_RULE, NO_QUESTIONS, PAGE_EMPTY, CHOICES_IGNORED, FIELDS_IGNORED, SIDE_MEDIA, END_MESSAGE, LOGO) follow the same pattern.

| code | Fix |
|---|---|
| FORMAT | format must be exactly "baxly-survey/1.0". |
| UNKNOWN_KEY | Remove the key or move it to where it belongs. The message lists what is allowed. |
| KEY, KEY_MISSING, DUPLICATE | Fix the key to match section 3 and make it unique. |
| TYPE_UNKNOWN | Use a type from section 7. |
| SETTING_UNKNOWN, SETTING | Use only the settings listed for that type, with the given ranges. |
| CHOICES_MISSING, ROWS_MISSING, TEXT_MISSING | Add the missing part. |
| REF_QUESTION, REF_CHOICE, REF_LIST, REF_COLUMN, REF_LOOP, REF_VARIABLE, REF_VARIANT | A reference points at something that does not exist. Check spelling and that the target is defined. |
| LOGIC | A page or question depends on something that comes later. Move the test, or the page. |
| CONDITION | The test shape is wrong or not allowed in that place (section 9). |
| LOOP, LIST_SOURCE | The loop or choices_from object is wrong (section 8). |
| URL | Only https URLs are accepted. |
| PLAN_TYPE, PLAN_FEATURE | The account's plan does not include this. The person can upgrade or you can swap the type. |
| LIMIT | A size limit (section 2) was exceeded. |
| SANITIZED (warning) | Disallowed markup was removed. Use the allowed HTML. |
| REVIEW_TEXT (warning) | Text addressed to the reviewer was found. Move it to notes. |
| MERGE_TAG (warning) | A merge tag refers to a variable or column that does not exist or is used outside a loop. |

## 13. Minimal complete example

```json
{
  "format": "baxly-survey/1.0",
  "survey": { "name": "Quick pulse" },
  "pages": [
    {
      "key": "main",
      "questions": [
        { "key": "nps", "type": "nps", "text": "How likely are you to recommend us?", "required": true,
          "ai_followup": { "goal": "The main reason for the score", "max": 2 } },
        { "key": "aspects", "type": "multi_select", "text": "What stood out?",
          "choices": ["Product", "Price", "Service", { "text": "Nothing", "exclusive": true }] },
        { "key": "service_detail", "type": "open_text", "text": "What happened with service?",
          "settings": { "multiline": true },
          "show_when": { "question": "aspects", "selected": "Service" } }
      ]
    }
  ]
}
```

See the three full examples (a customer satisfaction survey, a concept test with an A/B experiment, and a readership study built on lists and loops) at https://baxly.brc.com/survey-definition/examples/.

## 14. Checklist before you hand the file over

1. format is "baxly-survey/1.0" and the only top-level keys are format, notes, survey, lists, pages.
2. survey.name is set.
3. Every question has a unique key and a type from section 7; every choice question has choices or choices_from; every matrix has rows and choices.
4. Every key used in show_when, hide_when, end_when, loops, quotas and variable_rules exists and comes earlier.
5. No ids, no uuids, no settings outside the tables above, no markdown in text, no scripts.
6. Every URL is https.
7. Anything you had to guess is written in notes for the person to confirm.
