# potato-skill **potato-skill** is an open-source skill that lets a coding agent build a human annotation study in [Potato](https://www.potatoannotator.com/), the self-hosted annotation tool. Describe the study; the agent designs the task, writes the Potato config, lays out the annotation interface, adds quality control, runs the server and gets the annotations back out. It installs as a Claude Code plugin, and the `skills` CLI installs the same files for Codex and Cursor. The Potato blog post [potato-skill: Build a Human Annotation Study with Claude Code](https://www.potatoannotator.com/blog/potato-skill-claude-code-annotation-studies) introduces it with example studies. These pages are the reference material the skill loads while it works, published so they can be read without installing anything. A Potato task is one YAML config plus a data file. The pages here cover what that config does not explain: what to ask annotators, how to lay the interface out, which quality controls are worth their cost, and what goes wrong once real people start clicking. ## Install the skill In Claude Code: ``` /plugin marketplace add davidjurgens/potato-skill /plugin install potato-skill@potato ``` For Codex or Cursor, the [`skills`](https://github.com/vercel-labs/skills) CLI installs it from the repository: ```bash npx skills add davidjurgens/potato-skill --agent codex cursor ``` Potato has to be installed in whatever environment runs the commands: ```bash pip install potato-annotation ``` ## Where to start Building a task from a description: **[Designing an Annotation Task](designing-a-task.md)**, then **[Building the Annotation Interface](building-the-ui.md)**. Copying something that already works: **[A Worked Example](worked-example.md)** is one complete study, every file shown, and **[Starting From a Published Design](finding-a-design.md)** covers the showcase. Something is already broken: **[When a Task Misbehaves](troubleshooting.md)** collects the symptoms that validate clean and then do something else. ## The generated references Three more files ship with the skill and are generated from Potato's own registries, so they cannot drift from what the server enforces: every annotation type with a worked example, the documented top-level config keys, and the documented sub-keys. They are in the [repository](https://github.com/davidjurgens/potato-skill/tree/main/skills/potato-skill/references). ## For agents and answer engines Every page is also published as markdown: `designing-a-task/` is at `designing-a-task.md`. [`llms.txt`](https://davidjurgens.github.io/potato-skill/llms.txt) lists those files with a one-line description each, and [`llms-full.txt`](https://davidjurgens.github.io/potato-skill/llms-full.txt) holds every page in one file. # Agent Instructions Codex and Cursor read `AGENTS.md` from the project root, so it needs no install step: copy it into the project you are working in. Claude Code reads `CLAUDE.md` instead, and a `CLAUDE.md` holding the line `@AGENTS.md` imports it. It is the short version. The references and helper scripts it points to come with the skill, installed as below. The text after the rule is the file itself. ## Installing the skill ``` /plugin marketplace add davidjurgens/potato-skill /plugin install potato-skill@potato ``` For Codex and Cursor, the `skills` CLI copies the whole skill -- `SKILL.md`, the references and the helpers -- into `.agents/skills/potato-skill/`: ```bash npx skills add davidjurgens/potato-skill --agent codex cursor ``` The skill is a plain directory, so it also travels: ```bash python scripts/package_skill.py ``` builds `dist/potato-skill/` -- `SKILL.md`, the references, and the 9 executable helpers -- and `dist/potato-skill.zip`. One build, three destinations: | Where | How | |---|---| | Claude Code, every project on the machine | `cp -r dist/potato-skill ~/.claude/skills/`, or `--install-personal` | | Claude Agent SDK | Point the SDK's skill directory at `dist/`; it loads the skill the same way Claude Code does | | Claude API | Upload `dist/potato-skill.zip` as a skill, then name it on the request. The endpoint is in beta -- check Anthropic's Agent Skills documentation for the current call | The scripts in the bundle drive the `potato` command and import Potato's registries, so wherever the skill runs, Potato has to be installed in the environment running the commands. `SKILL.md` opens by checking exactly that. --- Potato is a web annotation platform. A task is one YAML config plus a data file. This file tells you how to build one correctly. ## The loop Do not write a config and hand it over. Run it through these steps first. ```bash potato validate config.yaml --strict # structural check potato preview config.yaml # what form did I declare? potato preview config.yaml --screenshot out.png # what does it look like, did it break? ``` All three return promptly. `potato start config.yaml -p 8000` runs the task for a human to click through, but **it does not return** -- it holds the server in the foreground until killed, and will hang a non-interactive session indefinitely. The screenshot step already boots a real server and shuts it down, so nothing in this loop needs it. Background it if you genuinely want one running. `potato validate` runs the same checks `potato start` runs at boot, so passing it means the server will accept the config. Its errors name the alternatives: an unknown `annotation_type` comes back with the list of valid ones. `--strict` turns unrecognized keys from a warning into a failure, which is how you catch a typo that silently disabled a feature. `--screenshot` is the step that catches what validation cannot. It boots the task, opens it in a headless browser, and reports every uncaught exception, `console.error` and failed request. A config can validate cleanly and still render a broken interface, because most of the annotation UI is built by JavaScript after the HTML arrives. Exit code is 0 only if the page rendered with nothing to report. ## Deciding what to build A config is the last step, and the decisions before it are the ones that are expensive to reverse: what one annotator sees at a time, what they are asked, how they answer, how many people answer each item, and how you find out whether the answers are any good. When a description leaves those open, decide the cheap ones yourself and confirm the expensive ones -- the unit of annotation, the annotator budget, consent wording, and anything involving identifiable people. Build with stated assumptions rather than waiting on an answer. Two that are easy to get wrong and expensive to discover late: - **The response format decides the agreement metric.** Potato picks the metric from the schema kind, so a 1--5 rating stored as a `radio` is scored as unordered and "1 vs 2" counts the same as "1 vs 5". - **`num_annotators_per_item` sets the count.** Unset, there is no limit. `min_annotators_per_instance` and `max_annotations_per_item` are older spellings of the same setting, and two of them with different numbers fail validation. The potato-skill skill has the full versions in its `references/` directory: `designing-a-task.md`, `asking-the-experimenter.md` and `building-the-ui.md`. Without the skill, read them at and the pages beside it. ## Start from a working example There are 218 example projects. Copying one beats assembling a config from field lists, and every one of them is checked in CI. ```bash # Search the catalog python -c "from potato.server_utils.examples_manifest import search_examples; \ print([e['dir'] for e in search_examples(annotation_type='span')])" ``` The catalog is also published as JSON at `potato/schemas/potato-examples.manifest.json`. Each entry records the annotation types and display types a config uses, the features it switches on, and the command that runs it. ## The minimum config Four keys are always required, plus a data source: ```yaml # yaml-language-server: $schema=https://potatoannotator.readthedocs.io/en/latest/schemas/potato-config.schema.json annotation_task_name: Sentiment Annotation task_dir: . output_annotation_dir: annotation_output/ data_files: - data/items.json item_properties: id_key: id text_key: text annotation_schemes: - annotation_type: radio name: sentiment description: How does this text feel? labels: [positive, neutral, negative] ``` Keep the first line. It switches on live validation in VS Code, JetBrains, Zed and Helix, and it tells the next agent where the contract is. The data file is JSON, JSONL, CSV or TSV. JSON may be an array of objects or one object per line. Every object needs the fields named by `id_key` and `text_key`. ## Rules that will bite you **Paths resolve against `task_dir`, and anything resolving outside it is rejected.** This is a security check, not a convenience. If you see `ConfigSecurityError: Path ... is outside the project directory`, you are running from the wrong directory or pointing outside the project. **`annotation_type` must come from the registry.** There are 61 of them. Do not guess a plausible-sounding name. `sentiment` and `classification` are not types. ```bash python -c "from potato.server_utils.schemas.registry import schema_registry; \ print(schema_registry.get_supported_types())" ``` **Unknown config keys only produce a warning.** A typo'd key is silently ignored and the feature you thought you enabled is off. Run `potato validate --strict`, which turns those warnings into a failure. **Phase-level `annotation_schemes` replace the top-level list.** They do not add to it. Having both is an error. **Never hand-edit anything in `output_annotation_dir`.** The server rewrites those files wholesale. ## Reference Every key, with its type, default and description, is in `docs/configuration/config_reference.md`, and the same information machine-readable is in `potato/schemas/potato-config.schema.json`. From Python: ```python from potato.server_utils.config_key_docs import get_key_doc get_key_doc("attention_checks.frequency") from potato.server_utils.schema_examples import example_scheme_for example_scheme_for("bws") # a scheme from a config that really runs ``` ## MCP `potato mcp` is two servers, both listed under **other commands** in `potato --help`. `potato mcp serve --root .` answers authoring questions out of the same registries as everything above, through tools such as `list_annotation_types`, `describe_annotation_type`, `list_examples`, `validate_config`, `preview_config` and `render_task_screenshot`. `potato mcp tools` prints all of them. Same answers as the shell, and `render_task_screenshot` hands you the rendered page as an image. `potato mcp connect --url … --token …` bridges a *running* task, if its config carries an `mcp` block listing tools in `mcp.tools` and an agent holds a token from `potato mcp issue-token`. The bridge adds 17 live tools named `live_get_status`, `live_get_progress`, `live_list_items`, `live_get_item`, `live_list_annotators`, `live_get_config`, `live_get_agreement`, `live_assign_items`, `live_export_data`, `live_submit_annotation`, `live_add_items` and `live_delete_annotations`, plus five that drive the labeling engine: `live_engine_status`, `live_engine_requests`, `live_engine_run`, `live_engine_certify` and `live_engine_certificate`. `live_delete_annotations` destroys an annotator's work on an item, so it also has to be listed in `mcp.destructive` and called with `confirm: true`; Potato refuses it otherwise. Each publishes a real parameter schema, so `tools/list` tells you what to pass. ```bash potato mcp config --root . # prints a client config block potato mcp tools # the tool list, without starting anything ``` ## Reporting a bug or asking for a feature Sort it before reporting it. `potato validate config.yaml --strict` failing, or a boot log line reading `Loaded 0 `, means a config mistake -- a key misspelled inside a block Potato treats as opaque is accepted, ignored, and never mentioned again. That is the usual cause of "the config is right and nothing happens", and it is not a defect. If Potato does something other than what it documents, that is a bug. If it does exactly what it documents and that is not enough, that is a feature request -- and before filing one, name the closest existing annotation type or config key and say why it does not fit, because it exists more often than people expect. Issues go to . A useful report carries the version (ask `potato --version`; releases up to 2.7 have no such flag, and neither `importlib.metadata.version("potato-annotation")` nor `potato.__version__` answers the same way from every directory, so say which you used and name the commit if you run from a clone), the `--strict` result, the shortest config that shows it, the shape of one data record rather than the records, and what you measured against what you expected. Strip `secret_key`, `ai_support.ai_config.api_key`, crowd credentials and annotator email addresses first. The potato-skill skill has a helper that assembles all of that, `scripts/report_issue.py` in the skill's directory. `npx skills add davidjurgens/potato-skill --agent codex cursor` puts that directory at `.agents/skills/potato-skill/`. Filing is public and cannot be withdrawn. Show the report to whoever owns the study and get an answer before you file it. ## Where to read more - Configuration reference: `docs/configuration/config_reference.md` in Potato's repository - Every annotation type: `docs/annotation-types/schemas_and_templates.md` in Potato's repository - Full docs in one file: - Curated index: # Designing an Annotation Task The decisions behind a task, before any YAML gets written. It ships with the `potato-skill` skill: ``` /plugin marketplace add davidjurgens/potato-skill /plugin install potato-skill@potato ``` --- A config is the last step. Before writing one, seven things have to be decided, and a researcher's description usually settles three or four of them. The rest you either infer from context or ask about — see `asking-the-experimenter.md` for which is which. 1. What one annotator sees at a time (the unit) 2. What they are being asked (the questions) 3. How they answer (the response format) 4. How many people answer each item 5. What order items arrive in 6. How you find out whether the answers are any good 7. What happens before and after the annotation itself ## 1. The unit Whatever you put in one row of the data file is what one annotator judges at once, and it is the hardest thing to change later. A comment, a sentence, a whole thread, a document, an image, one agent trace. Pick the smallest unit that still carries enough context to answer the question. "Is this reply rude?" needs the message it replies to; if the unit is a bare reply, annotators invent the missing context and disagree about inventions rather than about rudeness. Where the judgment is about a *part* of the unit, do not split the unit — use a span scheme and let annotators mark the part. Splitting a paragraph into sentences to ask "which sentence is the claim" throws away the reason a reader could tell. ## 2. The questions One scheme per judgment. Two judgments in one question ("is it rude and is it sarcastic?") produce answers you cannot separate afterwards. Order them the way a person would think: the gating question first, details after. Use `display_logic` so follow-ups appear only when they apply — an annotator who said "no anecdote" should not be looking at "highlight the anecdote". See `building-the-ui.md`. Keep the count low. Every extra question is paid for on every item. ## 3. The response format This is the decision that most often gets made badly, because the natural phrasing of a request rarely names a format. | What they asked for | Type | Notes | |---|---|---| | yes/no, or pick one from a list | `radio` | Add `key_value` per label; it is the difference between 8 seconds an item and 3 | | pick any that apply | `multiselect` | If they meant "pick one", it is `radio` — do not make them choose from checkboxes | | rate 1–5, agree/disagree | `likert` | Give every point a label, not just the ends | | how sure are they | `confidence` | Set `target_schema` to the scheme it qualifies, so the rating is attached to a specific judgment rather than floating | | mark the part of the text that… | `span` | Works as-is on `text_key`. Only needs `span_target` once you define `instance_display`, to say which field is the anchor | | which of these two is better | `pairwise` or `bws` | `pairwise` compares two things **on one item**, from the field named by `items_key`. `bws` does not — see below | | put these in order | `ranking` | Expensive above ~7 items | | free text, a reason, a correction | `text` / `text_edit` | `text_edit` when they are correcting something that already exists. **`text` is a one-line `` by default** — set `multiline: true` for anything phrased "in a sentence". No agreement metric applies to either; plan to read them | | a number, a proportion | `slider` / `number` / `constant_sum` | `constant_sum` when the parts must total something | | link two marked spans to each other | `span_link` | `coreference` if the links are equivalence classes rather than directed relations | **Both relational schemes hang off a `span` scheme**, named in `span_schema`, and neither works until spans exist on the page. Mark the mentions first; the widgets find them by click, so an item with no spans drawn gives you a panel whose buttons are all disabled and nothing to say why. `coreference` builds equivalence classes. Click a highlighted span, press *New Chain*, then click further spans and *Add to Chain*; the chains round-trip through the same `/api/links` endpoints `span_link` uses, so they survive navigation. `span_link` builds directed pairs instead: a list of `link_types`, each optionally narrowed by `allowed_source_labels` and `allowed_target_labels`, and picking a type first puts the widget into link mode, which then prompts "click a PERSON span / click a BODY span". Pick by what the relation is. Coreference is symmetric and transitive, so chains are the right primitive and `span_link` would make you record every pair. "speaks for", "employed by" and the rest are neither, so `span_link` is right and a chain would merge things that are not the same. If you do want chains out of `span_link`, a symmetric `{name: SAME_AS, directed: false}` type gets the pairs and the chains are their transitive closure, computed at export. Verified against Potato 2.8.2 (`v2.8.2-9-g19ce0041`). Both were unwired until then: the selection event the coreference manager waited for was never dispatched, and chains never reached the server. On anything earlier, check that clicking a span enables *New Chain* before you plan around it. ### If it has to work on a phone `pocket` puts a touch surface at `/pocket`, and with `pocket.auto_redirect` left at its default it sends phones and tablets there when they open `/annotate`. It constrains this decision rather than following it. Seven of Potato's 61 annotation types are touch-capable — `radio`, `multiselect`, `likert`, `slider`, `number`, `text` and `pure_display` — and the check is all-or-nothing: one scheme outside that list takes the whole task off the phone. The surface then serves a page naming the offender, "This task isn't phone-sized. The scheme *cue* needs a desktop. Open the regular interface at `/annotate`", rather than dropping the scheme or degrading it onto touch. So a study that has to run on phones is designed inside those seven from the first decision; a `span` scheme added in month two costs the phone surface for everybody. `pocket.batch_size` is how many items are fetched at once, which makes it the depth of the offline queue: an annotator who loses their connection can work through that many and no further. It defaults to 25 and is clamped to 1–200. Unsent answers wait in the phone's own storage and go up when the connection returns, through the same `/updateinstance` the desktop page posts to. That storage is the reason to **set `secret_key` before you put a study on phones.** Without it a server restart ends every session, and an annotator whose session ended part-way through a batch is holding answers your server has never seen. On a desktop that costs a re-login; on a phone it puts the only copy of somebody's work behind one. ### Eleven types the table skips The table above covers the common asks. Reach past it when the researcher describes one of these, because each has a purpose-built type and the hand-assembled version is worse in a way that shows up in the data: | What they said | Type | Instead of | |---|---|---| | "rate every one of these on the same scale" | `multirate` | Ten separate `likert` schemes, which is ten headings and ten scales an annotator reads separately | | "pick from our taxonomy" (dozens of codes, nested) | `hierarchical_multiselect` | A flat `multiselect` of 200 checkboxes. It renders collapsed with a search box, so the tree stays one screen | | "a long list, one answer" | `select` | A `radio` with forty options down the page | | "it isn't one label — it's mostly A, a bit B" | `soft_label` | A forced choice that throws the ambiguity away. Constrained sliders summing to a total, with a live distribution bar | | "an acceptable range, not a point" | `range_slider` | Two number boxes that can cross | | "warm/cold, weak/strong, active/passive" | `semantic_differential` | One `likert` per adjective pair. Takes `pairs`, renders the bipolar grid | | "which package would they choose" | `conjoint` | Hand-built profile comparisons. Give it `attributes`, each with the values it can take, and it generates the profiles | | "group these however makes sense to you" | `card_sort` | A `multiselect` per item, which loses the grouping. Mouse-only: the cards are `draggable` divs with no tabindex, no roles and no move buttons, so a keyboard or screen-reader annotator cannot do the task, and neither can a phone | | "find the answer in the passage" | `extractive_qa` | A `span` plus a separate question field; this takes `question_field` and `passage_field` and handles unanswerable. It renders its own copy of the passage, so leave that field out of `instance_display` unless you want it twice | | "the same event reported across articles" | `multi_document_event` | Per-document annotation you then have to align. Takes template `slots` | | "review this diff like a pull request" | `code_review` | A textbox. Inline comments, categories, per-file ratings, a verdict | These are the eleven types a config reference lists and nothing routes you to. All eleven have a working example under `examples/`, all render cleanly, and `search_examples(annotation_type=...)` finds each one. There are 61 types. `references/annotation-types.md` lists all of them with a worked example lifted from a config that really runs. Never invent a type name: `sentiment`, `classification` and `qa` are not types. **`bws` is a subsystem, not just a scheme.** It draws its four candidates from the whole dataset, not from a field on the row, so the tuples have to be generated for it by a top-level `bws_config` (or `ibws_config` for the adaptive version). From 2.8.2-10 `validate --strict` refuses the combination and names the missing block. Before that the scheme rendered anyway -- four buttons labelled A, B, C and D with nothing in them -- and validation passed, the boot log was clean, and the annotator was asked to choose between blanks; `check_ui` now catches that shape wherever it comes from. ```yaml bws_config: tuple_size: 4 num_tuples: null # auto from min_item_appearances seed: 7 min_item_appearances: 4 # each item appears in >= N tuples scoring: {method: counting} # counting | bradley_terry | plackett_luce ``` One row per candidate, `text_key` pointing at the candidate text; Potato replaces your instances with generated tuples, so the ids in the output are `bws_tuple_0001` and the export is scored rather than per-row. Plan the data as a pool, not as pre-built comparison sets. **Three more that need a second thing before they do anything.** Each renders at a glance-correct default and collects nothing, and `validate --strict` passes on all three. `region_caption` and `grounding_eval` need an `image_annotation` scheme beside them. Neither draws: one owns the description attached to a region, the other the phrase-to-region binding. Alone, `region_caption` shows "Draw a region on the image, then describe it" over a list that can never fill, and `grounding_eval` offers *Not present in the image* as the only answer an annotator can give — a grounding study where every phrase is absent. `modalities.md` has the config. `multi_document_event` needs a top-level `event_template:` block with `enabled: true`. The `slots:` on the scheme itself are not enough: without the block the widget draws "No events yet. Create one to begin", and *+ New event* 404s against `/corpus/api/event` for the rest of the session with nothing on screen to say so. Also check where its answers go before you build on it — they are not in `user_state.json`; see `getting-the-data-out.md`. `spatial_annotation` needs its cloud path rendered on the page, in a display field with no `label:`, or as the item's `text_key`. `source_field` alone gives you an empty viewer and a message pointing at `item_properties`. `modalities.md` has the three cases side by side. **`conjoint`, `event_annotation` and `tree_annotation`: fixed in 2.8.2-11.** They were the three that needed a second thing in the previous release, and all three now work as the reference describes: `conjoint` builds its attribute rows from the profiles' own keys with no `attributes:` declared, `tree_annotation` stores both node ratings and the selected path (`thread:::node_annotations` came back as `{"a1":{"thread_node":"4"}}`) and renders any annotation type inside a node, and `event_annotation` draws its arcs in a container that is 80px tall under `instance_display` rather than 0. On an older checkout, assume all three are broken and read the note in `building-the-ui.md`. **When the label set is meant to be incomplete**, this table is the wrong tool. A researcher doing thematic analysis, grounded theory or any open coding starts with a few codes and adds more while reading, and a fixed `labels:` list fights that the whole way. Potato has a mode for it — `qda_mode`, with a live project codebook (`codebook: true` on a span scheme), memos on passages, and in-vivo coding that mints a code from the participant's own words. Reach for that rather than a `multiselect` you plan to keep editing. The anti-patterns at the end of this file assume a fixed scheme and do not apply: in open coding, overlapping codes are the normal case and memos are deliberately uncountable prose. `qda_mode: {enabled: true}` is the whole switch. It turns on the memo sidebar and the codebook together, and with `codebook: {mode: open}` an annotator can add a code from the sidebar without a restart: the chip appears in the live span scheme straight away, a banner says how many codes have arrived since they labelled the item they are on, and a Review list offers the items they finished before the code existed. That last part is the reason to use the mode rather than a `multiselect` — in open coding the code you invent on item 40 is a question about items 1 to 39, and nothing else in Potato asks it. `/codebook` holds the code definitions, and in edit mode a whole document around them: preamble, general instructions, background, how codes are used downstream, each block with its own history and a revision counter. That is where the definitions that make a code reproducible belong. They are not in the config, so `potato codebook config.yaml` does not round-trip them, and once coding has started the project database is the source of truth. Offer `cases` and `sessions` in the same breath. `cases` makes the unit of analysis a participant rather than an excerpt (`key: participant_id`, with `attributes:` lifted onto the case for crosstabs), and the boot log says how many it detected. `sessions` groups the excerpts of one interview and puts a `session_level: true` scheme on `/sessions`: one answer per interview, which is where "how good was this interview" belongs instead of repeated on every passage. Agreement across two open-coded passes is not a κ. The codebooks differ, so compare coverage and code co-occurrence, and expect to reconcile the codebooks before any number means anything. **Scale points.** 5 or 7 for a Likert. Even numbers force a side, which is a design choice and not a default. If the researcher says "1 to 5", give all five points a written label — bare numbers mean different things to different annotators and inflate disagreement that has nothing to do with the construct. Doing that logs `Complex labels detected for , using radio layout` and renders radio buttons rather than a scale widget. That is cosmetic: agreement keys off `annotation_type`, so a labelled `likert` is still scored as ordinal (`/admin/iaa` reports `kind: ordinal`). Do not "fix" it by switching the scheme to `radio`, which is the change that would actually break the metric. **The schema kind decides the agreement metric**, and you do not get to pick it separately. Potato classifies each scheme and computes what fits: | Kind | What it computes | |---|---| | nominal (`radio`, `multiselect` capped at 1) | percent agreement, Cohen's κ, Fleiss' κ, α | | ordinal (`likert`, ordered scales) | linear and quadratic weighted κ, Spearman ρ, ordinal α | | continuous (`slider`, numbers) | Pearson r, MAE, RMSE, interval α, ICC(2,k) | | multilabel (`multiselect`) | mean Jaccard, MASI α | | ranking | Kendall's τ, Spearman footrule | | span | token-level κ, exact and partial span F1, Krippendorff's αU, γ | | geometry (boxes, polygons) | matched IoU, detection F1, and chance-corrected σ, ks, detection α | | free text | nothing | So a 1–5 rating stored as a `radio` gets scored with a nominal metric, which treats "1 vs 2" and "1 vs 5" as equally wrong and understates agreement badly. Storing an ordered judgment in an unordered type is a measurement error, not a cosmetic one. ## 4. How many annotators `num_annotators_per_item` decides it: an item stops being served once that many people have annotated it. | Key | Meaning | |---|---| | `num_annotators_per_item` | Annotators per item. Set this one; unset or `0` means no limit | | `min_annotators_per_instance` | A deprecated spelling of the same setting. Since 2.11 it is read as the cap, with a warning | | `max_annotations_per_item` | An older spelling of the same setting; `-1` for unlimited | | `max_annotations_per_user` | How many dataset items **one annotator** is served. Defaults to all of them | The first three are one setting. Different numbers in two of them fail validation; the same number is harmless but says you were unsure. `max_annotations_per_user` sets how much one person is asked to do, unless `per_annotator_quota` is also set -- it caps annotators individually and takes precedence, including over the global. Attention checks and gold items are injected on top of whichever applies rather than inside it, so enabling them does not shorten anyone's corpus. See `assignment-and-agreement.md`. Defaults worth arguing from: **1** when the labels are near-mechanical or the data is being used to prototype, and nobody is going to report agreement. **3** when there is any judgment involved and an agreement number will be reported — it is the smallest number that lets a majority break a tie. **5+** for genuinely subjective constructs, or when per-annotator reliability is itself the object of study. More annotators on fewer items beats fewer annotators on more items whenever you do not yet know the labels are learnable. Find that out on 100 items before spending the budget on 10,000. ## 5. Order `assignment_strategy` takes: `random`, `fixed_order`, `active_learning`, `llm_confidence`, `max_diversity`, `least_annotated`, `category_based`, `diversity_clustering`, `batch`, `priority`, `model_review`, `psychometric`. `random` is right unless there is a reason. Reasons: `fixed_order` when items are a narrative and order carries meaning; `least_annotated` when you care most about finishing every item; `active_learning` when a model is in the loop and the point is to spend annotator time where it changes something. `llm_confidence` is in the list and is not implemented: it assigns at random and takes no model into account — I got a shuffled order out of it on a server with a fitted classifier sitting unused. It warns at boot now and names the two strategies that do the job: `active_learning`, or `model_review` when the model has already produced labels for people to check. Set `random_seed` if the ordering ever needs to be reproduced. ## 6. Quality control Add these in order. Each costs annotator time, so stop when the risk is covered. 1. **`require_fully_annotated: true`** — no skipping questions. Nearly always right, and free. 2. **Agreement** — comes automatically once `num_annotators_per_item` is above 1 and items overlap. Nothing to configure; read it on the admin pages. 3. **`gold_standards`** — items with known answers mixed in, with an accuracy floor. Use when annotators are paid strangers. Needs an items file and somebody to have written the correct answers. 4. **`attention_checks`** — items with an obvious answer, plus `attention_checks.min_response_time`. Catches clicking-through, which gold standards catch too, but attention checks are cheaper to author. 5. **`training`** — a qualification phase before real items. Use when the label scheme takes explaining. It is the only one of these that improves the annotations rather than just measuring them. 6. **`adjudication`** — a queue where a third party resolves disagreements. Use when you need one final label per item rather than a distribution. The failure this list is built against: discovering after 10,000 items that one annotator misread a label definition throughout. The file formats for training, attention checks and gold standards are in `quality-control.md`. All three can be enabled, validate clean and load zero items, so check the count in the startup log rather than the config. ## 7. Before and after `phases` orders what an annotator walks through: consent, instructions, training, annotation, and post-study. Those five plus `prestudy` are the only valid phase types, each non-annotation phase needs a page file, and none of it is validated — `phases-and-pages.md` has the format. `surveyflow` is the older way to hang survey pages either side. **A consent page is not a default.** If the work involves human subjects, the researcher's institution has wording, and inventing it is not your job. Ask. When they give you the substance without wording, write plainly what they said, covering what the study is, how long it takes, and that stopping is allowed without penalty. Say that it is a draft to be replaced. Instructions belong in `annotation_instructions`, which takes the text itself, inline. A filename there renders as that filename. Label definitions live in it, not only in the `description` of each scheme — the description is a reminder for someone who has already read the definition. ## Three keys that change the answers None of these is about appearance, and all three are one line on a `radio`, `multiselect` or `select` scheme. ```yaml - annotation_type: radio name: stance description: What stance does this take? labels: [Supports, Opposes, Neither] option_randomization: true # per annotator, to spread order effects has_free_response: true # adds "other, please specify" ``` **`option_randomization`** shuffles the choice order. Whichever option sits first is picked slightly more often than it should be, and with one fixed order that bias lands on the same label every time and is indistinguishable from a real finding. The shuffle is **seeded from the username and fixed for the whole study**. Alice sees `Alpha Charlie Delta Echo Bravo` on every item of hers; bob sees a different order, and the same one on every item of his. That spreads order effects *across* annotators rather than within one, which has two consequences: it does nothing at all on a single-annotator task, and within one person's labels the bias still stands. It cancels only across the pool. It applies to `radio`, `multiselect`, `select` and `multirate`. Do not use it where order carries meaning: a Likert scale reads low-to-high, and shuffling it is nonsense. **`has_free_response`** is the actual fix for the "no 'none of these'" anti-pattern below. It adds a text box labelled "Other (please specify)" — pass a dict with `instruction` to reword it — so an annotator who thinks none of the labels fit says what they would have said instead. Read those answers after the pilot. They are the label set the researcher has not written yet. **`dynamic_options`** narrows the choices per item. The full label set still lives in the config; a field on the item — `visible_labels` unless `dynamic_options_field` names another — lists which of them stay, and the rest are removed from the page. An item with no such field is not filtered, so the default is "show everything" rather than "show nothing". It is the answer to "the options depend on the item", which otherwise gets built as one enormous label list with instructions telling annotators which parts to ignore. Note the direction: you cannot introduce an option this way that the config does not already declare. `radio`, `multiselect` and `select` only. ## Prefilled answers `pre_annotation` seeds answers into the form; `ai_support` offers a model's suggestion; `icl_labeling` labels the corpus as the study runs. `modes-and-subsystems.md` routes to all three as features. The design question they raise is separate: **an annotator shown a suggestion agrees with it more often than one who is not.** The labeling engine serves its calibration requests blind for that reason (`labeling-engine.md`). Prefilling is still worth doing — it is what makes correction passes cheap, and cheap is often the point. It does change what the numbers mean: - Agreement between annotators who saw the same suggestion measures agreement with the model, not with each other. Say so wherever the number is reported. - A prefilled answer left untouched is not evidence the annotator considered it. If that distinction matters, `annotation_telemetry` records whether they interacted at all, and `export_include_annotation_changes` puts the revision trail into the export: every answer they moved off, with timestamps. - Where the point is measuring the model, do not prefill the items you are measuring it on. Hold out an unprefilled slice. Tell the researcher which of these applies before the run, not when they ask why agreement is 0.95. ## Piloting Before the full run: three annotators, 50–100 items, then look at the disagreements. Almost every serious problem shows up there, and almost none of them show up in a config review. Budget for the label definitions changing after the pilot, because they usually do. ## Anti-patterns These assume a fixed label set decided in advance. For open coding, see the note under "The response format". - **A single free-text box for a judgment you intend to count.** If it is going to become a number, make it a scale. - **Labels that overlap.** If two labels can both be true, it is a `multiselect`, or the labels need rewriting. - **No "none of these" and no "unclear".** Without them, annotators put genuinely unclear items somewhere arbitrary, and that noise is invisible afterwards. - **A scale with unlabelled middle points.** See above. - **Asking for confidence on everything.** It doubles the questions. Use it where you plan to filter on it. - **Optional questions everywhere.** An optional question is one whose absence you cannot interpret. # What to Confirm With the Researcher Which design choices to make yourself and which to hand back. It ships with the `potato-skill` skill: ``` /plugin marketplace add davidjurgens/potato-skill /plugin install potato-skill@potato ``` --- Most task descriptions specify what to annotate and leave the rest open. Some of what is left open has a defensible default and some does not, and the difference is what this file is about. ## Before the design questions The design questions below assume a brief and a data file. Half the time the researcher is holding more than that and has not thought to mention it, because from where they sit it is not part of the request. Four things change what you build, and all four are cheap to ask and expensive to discover late: | Ask | Why it changes the build | |---|---| | "What format is the data in, and where does the media live?" | Decides `data_files` versus `data_directory` versus `data_sources`, and whether images need `media_directory`. A folder of transcripts or a ConvoKit corpus has its own converter | | "Do you already have annotations for any of this?" | `potato import` reads 20 formats and produces a runnable project. Rebuilding one by hand is the most avoidable way to lose an afternoon | | "What do you need to come out at the end?" | 32 export formats. A study aimed at training a detector should be checked against `coco` **before** anyone annotates, not after | | "Who is running this once I hand it over, and for how long?" | Decides hosting, whether logins must survive a restart, and whether it needs an allowlist | `importing-existing-work.md` and `getting-the-data-out.md` cover the first three. The fourth is in `deploying.md` and `after-annotators-start.md`. ## The rule **Decide it yourself if being wrong is cheap to fix. Ask if being wrong is expensive, irreversible, or not yours to decide.** Changing a keyboard shortcut costs nothing. Re-running 5,000 items because the unit of annotation was wrong costs the whole budget. Publishing consent wording nobody approved is not a cost you get to weigh at all. ## Ask about these **The unit, when the data does not settle it.** A file of paragraphs where the question is about sentences is a real fork, not a detail. Whichever you choose you may be re-running the study. **Annotators per item, when agreement is going to be reported.** This is the budget question, and it is theirs. Propose a number with the reason. **Consent wording, and anything else with an ethics review behind it.** Their institution has approved text. Draft from what they told you, hand it over as a draft, and say plainly that it needs replacing with the approved version. **Whether annotators are paid strangers or colleagues.** It decides whether gold standards, attention checks and a completion code are worth the annotator time. You usually cannot infer it. **The label definitions, whenever they have any.** Ask for the codebook from a previous round, the paper the scheme comes from, or an annotation manual, before writing definitions yourself. They carry the construct the study is about, so getting them wrong invalidates the result rather than costing a re-render. If they have none, draft them and hand them back marked as drafts — `writing-guidelines.md` has the four parts each one needs. **Any label set that looks incomplete.** Three labels with no "none" and no "unclear" is nearly always an oversight, and it is the kind that quietly poisons the data — annotators put genuinely unclear items somewhere arbitrary and nothing downstream can see it happened. Say which items you think have nowhere to go. **Anything touching identifiable people.** If the data has usernames, faces or locations in it, ask before showing them to annotators. **Whether the label set is final, if annotation is starting soon.** Say plainly that this is the last cheap moment. Once people have answered, adding a question collects nothing from anyone who already finished, and renaming one silently strips the old answers out of every report — `after-annotators-start.md` has what actually happens. A researcher who knows the deadline will usually take the extra day. ## Defaults to take without asking | Decision | Default to take | |---|---| | Keyboard shortcuts | Add them on any scheme with ≤9 labels | | Question order | Gating question first, follow-ups behind `display_logic` | | Required vs optional | All required, plus `require_fully_annotated: true` | | Item order | `random`, and set `random_seed` | | Scale points | 5, every point labelled, unless they named a number | | Instructions page | Write a draft from their description, marked as a draft | | Login | Whatever the example you started from uses | | Output format | JSON | | Progress indicator, navigation | Leave the defaults on | State these in a short "what I assumed" list when you hand the task over. An assumption they can see is one they can correct in ten seconds. ## How to ask **Batch it.** One message with every question, not a conversation. They are answering between other work. **Propose an answer to each.** "How many annotators?" makes them do the design. "I've set 3 annotators per item, since you mentioned reporting agreement — say if the budget is tighter" gives them something to accept or push back on, and they can do it in one word. **Say what it costs to change later.** They cannot rank your questions by consequence, and you can. Lead with the ones that are expensive to get wrong. **Cap it at five or six.** Anything past that is a decision you should have made yourself. **Never block on it.** Build the task with your assumptions, verify it, and ask alongside something working. A running task with three open questions is worth more than three questions and nothing to look at. ### Shape of the message > Here's the task — `config.yaml`, screenshot attached. It validates and > renders. Four things I'd like you to confirm: > > 1. **Unit of annotation.** Your CSV has one row per thread, and I've made a > thread the thing an annotator judges. If the question is really about > individual posts, that changes the data file and everything downstream, so > it is worth getting right now. > 2. **Three annotators per item**, since you mentioned agreement. That is 3× > the cost of one — say if the budget is tighter and I'll drop to 1 and > overlap a subset instead. > 3. **Consent page.** I wrote a draft from what you described. It needs > replacing with whatever your IRB approved. > 4. **"Unclear" option.** The label set is yes/no. Some of these comments are > genuinely ambiguous (item c003, for instance), and without a third option > annotators will split them arbitrarily. Add one? > > Assumed, easily changed: `y`/`n` shortcuts on the yes/no question, all > questions required, random item order with a fixed seed, 5-point confidence > scale with every point labelled. ## When they cannot be reached Build it, make every assumption explicit in a comment in the config *and* in your handover, and pick the conservative option: more context per item rather than less, an extra "unclear" label rather than a forced choice, one annotator rather than a budget they did not approve. Then say which choices you would revisit first. # Interviewing the Researcher The multiple-choice question bank for a task described from scratch. It ships with the `potato-skill` skill: ``` /plugin marketplace add davidjurgens/potato-skill /plugin install potato-skill@potato ``` --- When someone invokes this skill directly — `/potato-skill`, with or without a sentence after it — there may be no brief in the conversation to work from. Do not open with "what would you like to build?", and do not open with a questionnaire either. Ask for one sentence, propose a design from it, then confirm only the parts you had to guess. A researcher who has never seen Potato cannot answer "what annotation type do you want". They can describe their study in a sentence, and they can tell you whether the design you read back to them is right. This file is the question bank for that. When a brief already exists, `asking-the-experimenter.md` governs instead and most of this is answered. ## Step 1: ask for one sentence Ask in prose, not with the question tool. Two things: > In one sentence, what do you want annotated and what do you want to know about > it? And if you already have the data, point me at the file. That sentence usually settles the modality, the unit and the question at once. "Label 2,000 tweets for stance on a policy" leaves only the response format, the annotator count and the phases genuinely open. Look at the data file if they name one — the fields in it decide `item_properties`, and a glance is faster than asking. ## Step 2: propose a design Before asking anything else, say back what you intend to build: > Each annotator sees one tweet. They pick one of Agree / Neutral / Disagree / > Not about the policy, and highlight the phrase that decided it. Three > annotators per tweet. No consent or training pages unless you want them. Then ask only about what that proposal guessed at. Reading back a wrong design is the fastest way to be corrected. A list of eight questions is the slowest. ## How to ask the rest - **At most four questions per round, two to four options each.** More and people stop reading and take the first option. - **Put what you proposed first and mark it as the recommendation.** An unmarked list of four equals implies they are equally good. - **Never ask what the sentence already answered.** Skip whole questions. - **Two rounds, an optional third, then build.** Everything still open becomes a stated assumption in `DESIGN.md` and a line in `QUESTIONS.md`. ## Round 1: what they are annotating **1. What kind of data is it?** Ask this even when you think you know. It decides more than any other answer: the display, the scheme family, and the per-family key that says where the media lives. | Answer | Where to go next | |---|---| | Text — posts, documents, transcripts as text | Stay here. The default `text` display is right | | Images | `modalities.md`. Needs `instance_display`, and a geometry scheme reads its bitmap from the rendered image rather than fetching its own | | Audio or video | `modalities.md`. `speech_transcript` reads `audio_key`, `temporal_grounding` reads `video_key` | | Conversations, agent traces, PDFs, 3D | `modalities.md`, then `agent-traces.md` for the agent family, which reads `steps_key` | Getting this wrong is quiet. The wrong key gives an empty widget, a clean `--strict` and nothing in the log, and a screenshot cannot tell an image that failed to load from one that loaded. **2. What does one annotator see at a time?** One item · one document · a whole conversation · a pair to compare. Decides `item_properties` and the shape of the data file, which is the expensive thing to change later — it is a data problem rather than a config problem. **3. What do you want to know about each item, and how should each part be answered?** Ask both halves. Someone who says "stance" often wants stance, the strategies being used, and the phrase that carries it — three judgments on one item. Step 1's sentence names whichever they thought of first, so ask for the rest here or they will not come up again. `designing-a-task.md` has the rules once there is more than one: one scheme per judgment, ordered the way a person would think about the item, `display_logic` on the ones that only sometimes apply, and keep the count low. The response format is then a choice per judgment. It decides the agreement metric and nothing warns you: | Offer | Type | Consequence | |---|---|---| | Pick one of N | `radio` | Scored as unordered — right for categories, wrong for a scale | | A rating scale | `likert` | Scored as ordered. "1 vs 2" counts less than "1 vs 5" | | Pick any that apply | `multiselect` | Set-valued agreement | | Mark a part of the item | `span` | Spans rather than labels. `error_span` for error analysis | A 1–5 rating stored as `radio` is a measurement error that survives to publication. Confirm this one even when it feels obvious. **4. How many people annotate each item?** One (fastest, no agreement number) · two · three (recommended if they want to report agreement) · more. Sets `num_annotators_per_item`, and it multiplies the budget directly — `estimate_effort.py` prices it before the instructions get written. ## Round 2: the study around it **1. Do you want any pages around the annotation?** Offer the list and expect most people to want none of it. **A task needs only the annotation itself.** Most do not declare a `phases` block at all — 7 of the example projects Potato ships use one, and none of them has a `consent` or `prestudy` phase. This is a multi-select that is allowed to come back empty, not a menu to talk someone through. | Offer | Phase | Ask for it only when | |---|---|---| | Written instructions | `instructions` | The label definitions are longer than the banner can hold | | A consent page | `consent` | Their protocol or ethics board requires one | | A practice round with feedback | `training` | They said the labels are hard, or want agreement measured after practice | | Questions at the end | `poststudy` | They asked for demographics or feedback | | Questions before starting | `prestudy` | They are screening or qualifying annotators | `consent`, `training` and `prestudy` are protocol decisions. A researcher who needs one knows they need it. Added uninvited they cost the annotator time in every session and cost you a page file per phase to keep correct. **Follow-up, only if they picked `poststudy` or `prestudy`: do you want a standard instrument?** Potato ships a library of 55 validated questionnaires — `tipi` and `bfi-2` for personality, `panas` for affect, `who-5` and `phq-9` for wellbeing, `mfq` and `sdo-7` for attitudes, and demographic batteries lifted from ANES, GSS, ESS and the ACS. Naming one writes the whole questionnaire into the phase, so this is usually cheaper than the free-text survey they were about to describe. `phases-and-pages.md` has the mechanics and the full categories. **2. How do you want to catch inattentive annotators?** Attention checks · gold-standard items · the practice round grades itself · nothing, it is a small trusted pool. "Nothing" is a reasonable answer for a pilot or a lab study. Each of the others has its own side-file format in `quality-control.md`, and each is silently off if the file is malformed. **3. What has to come out at the end?** A spreadsheet · a format for a specific tool · a training dataset. Potato has an exporter registry with an entry per format — `csv` for analysis, `conll_2003` for NER, `coco` for detection, `parquet` at scale. Ask before annotation starts: a task designed without a target format can collect answers that do not survive the conversion. **4. Who annotates, and is anything in the data identifiable?** Decides `user_config.allow_all_users`, whether an allowlist is needed, and whether any of `deploying.md` applies. Never decide the identifiable half yourself — it gates consent wording, export settings, and whether a public URL is acceptable at all. ## Round 3: what else they might want Rounds 1 and 2 cover the study a researcher can describe. This one covers what they cannot, because they do not know it is there. Do not walk them through the whole capability surface. Offer a menu, and drill into only what they tick. Two multi-select questions, four options each. That is eight slots for the seven clusters in `modes-and-subsystems.md`, which is the map behind this round and where you go once something is ticked. **Ask first: is this study worth it?** Skip the round entirely for a pilot, a lab study, or anything under a few hundred items. Say so out loud rather than silently omitting it — "none of this is needed for a normal study, but here is what is available" is the framing that lets someone decline without feeling they have missed something. **1. Anything here you want?** | Offer | Cluster | Drill into | |---|---|---| | Answers more precise than a rating scale | The answer | `soft_label`, `range_slider`, `semantic_differential`, `constant_sum`, `confidence` | | Know which annotators are reliable | Trust | `mace`, `psychometrics`, `agreement_metrics` | | A model helping out | Model help | `ai_support`, `labeling_engine`, `pre_annotation`, `icl_labeling`, `chat_support`, `active_learning` | | Different people see different items | People | `batch_assignment` with `scheme_sets`, `category_assignment` | **2. Anything about the corpus, or what happens afterwards?** | Offer | Cluster | Drill into | |---|---|---| | The corpus is large, or keeps growing | Material | `partial_loading`, `data_sources`, `watch_data_directory` | | Score whole conversations, or per participant | Unit | `sessions` with `session_level`, `cases` | | Publish or hand off the dataset | Output | `publish`, `dataset_metadata` | | Pay annotators on Prolific or MTurk | Output | `crowdsourcing` | Nothing here is free. Every one of these is a block that turns on with `enabled: true`, validates clean, and can then load nothing at all. Whatever they tick, check the boot log afterwards — an advanced feature that is configured and silently off is worse than one nobody asked for, because everyone believes it is running. ## When the skill fires implicitly Most of the time this skill loads because someone described an annotation task, not because they typed its name. There is no interview then, and asking eight questions at someone who just handed you a brief is the wrong move. The clusters still apply — as a checklist you run against the brief rather than questions you ask: - Walk the seven clusters in `modes-and-subsystems.md` against what they wrote. - Build the obvious task, with the defaults, and boot it. - Raise **at most two** clusters the brief never touched, and only where the brief implies they matter. Three annotators and no quality control is worth a sentence. A 200-item pilot with no active learning is not. - Put the rest in `QUESTIONS.md` rather than in the conversation. The rule is the same in both directions: surface what they would want, and do not make them shop. ## Build before you confirm Two rounds is enough to write a config that boots. Write it, boot it, run `check_ui.py`, and show them a screenshot of the real interface. A researcher who cannot answer "do you want a `likert` or a `radio`" can answer "is this the question you meant" in one look. Record the choices in `DESIGN.md` with the assumptions marked as assumptions, put what is still open in `QUESTIONS.md`, and say which one you would revisit first. `recording-decisions.md` has the template. # Writing the Annotation Guidelines Label definitions, tie-break rules and the codebook an annotator reads. It ships with the `potato-skill` skill: ``` /plugin marketplace add davidjurgens/potato-skill /plugin install potato-skill@potato ``` --- The config has a validator, a schema and four generators behind it. The text an annotator reads has none of that, and it is where most of the disagreement in a finished dataset comes from. A label set that looks obvious in a config review splits three ways in a pilot, and the split is nearly always the guidelines rather than the annotators. The guidelines are not an accessory to the annotation scheme; in the standard account they are *part* of it. Finlayson and Erjavec define a scheme as "a linguistic theory, a derived model of a phenomenon of interest, a specification that defines the actual physical format of the annotation, and the guidelines that explain to an annotator how to apply the specification to linguistic artifacts" — and open with the flat statement that "creating linguistic annotations requires more than just a reliable annotation scheme", because "even the most carefully designed scheme will not answer a number of practical questions about how to actually create the annotations." This is also one of the few places in study design where the effect on your numbers has been measured. Bayerl and Paul's meta-analysis of 96 annotation studies and 346 agreement indices found seven factors that consistently move reported agreement, and three of them are decisions made here: the number of categories in the coding scheme, whether annotators received training, and the intensity of that training. Potato ships a worked model of everything below: `examples/advanced/long-guidelines/` — a politeness task with an instructions phase, a banner, and a full codebook document. Read `media/codebook.html` before writing your own. ## The four surfaces | Surface | Read | Holds | |---|---|---| | `instructions` phase (`file:` an `.html` or `.jsonl`) | Once, before the first item | The teaching material. As long as it needs to be | | `annotation_instructions` | On every annotation page, collapsible, open by default | The reminder. The rules that settle most items, in a few lines | | `annotation_codebook_url` | On demand, a Codebook button in the nav bar, new tab | The authoritative document, when it wants a page of its own | | `description` per scheme | Beside the question itself | One line. The question as an annotator would ask it | The phase teaches, the banner reminds, the button holds the document, the description labels the question. Putting the codebook in the banner is the common mistake: it is expanded by default and sits above the item, so a long one pushes the questions below the fold on every single item of the study. `annotation_codebook_url` takes any URL. Serve the document out of `media_directory` and it ships with the project rather than depending on a Google Doc that someone will later move. ## What a definition has to do The test for a label definition is not "does this describe the concept" but "can two people who disagree use this to find out who is right". Those are different documents. *Impolite: messages that are rude or disrespectful* describes the concept and settles nothing. It hands the annotator the same judgment they already had. The structure to copy is MacQueen et al.'s, developed at the CDC for team-based coding and now the standard codebook format in qualitative research. Their codebook entry has **six components: "the code, a brief definition, a full definition, guidelines for when to use the code, guidelines for when not to use the code, and examples."** The paper's stated purpose is "how specific codebook features can improve intercoder agreement among multiple researchers." Two of those six are the ones that do the work, and they have names worth knowing. The when-to-use and when-not-to-use guidelines are the **inclusion criteria** and **exclusion criteria**. The exclusion half arrived in that literature as a section per code titled "Differentiating (blank) from Other Processes" — its whole job is to separate a label from the ones it gets confused with. In a Potato config that becomes: 1. **The label**, as it appears in `labels:`. 2. **The one-line rule**, phrased as something you can check. This is what goes in the banner and, condensed further, in the scheme `description`. 3. **The full definition** — inclusion criteria. What counts, concretely, in the forms it takes in *this* data. 4. **Exclusion criteria.** What does not count, especially the things that look like it, and **the nearest neighbour**: the label it will actually be confused with, and the difference. 5. **Examples**, below. Four and five are the ones people skip and the ones that pay. Most confusion is between two specific labels rather than spread evenly, and the pair is predictable before the pilot: your two most similar labels. ## Tie-break rules The highest-value paragraph in any guideline document is the list of rules that resolve the cases where two labels both look right. Written before the pilot they are guesses; written after, they come from the actual disagreements and each one measurably shrinks the spread. The example codebook states four, and states why each exists: > **Rule 2 — Terse is not impolite.** This is the single largest source of > disagreement in this task: it accounted for 38% of the adjudicated conflicts > in round 1. A bare imperative with no pressure cue ("Send me the slides before > the meeting.") is Neutral. Number them. Annotators refer to them by number in adjudication, and a numbered rule can be cited in the paper. ## Examples have to include the hard ones Three kinds, and a set with only the first is worse than none, because it teaches annotators the task is easy: - a clear positive - a clear negative, ideally one that superficially resembles the positive - **a borderline case, with the ruling and the reason** Take them from the data being annotated, and cite the item id. An invented example is an example of your idea of the task. A real one is evidence, and when someone challenges the ruling later you can both look at the same item. ## Edge cases already ruled on A table, appended to as the study runs. Two columns: the case, the ruling. | Case | Ruling | |---|---| | Message is only a link, or only a file | Neutral. There is no politeness work either way | | Message quotes someone else being rude | Rate the writer's own words, not the quotation | | Message is in a language you do not read fluently | Skip it. A guess is indistinguishable from a judgment in the exported data | This table is the single most useful thing you can hand a second annotator, and it is free: every entry is a question somebody already asked. ## Deliberate scope exclusions > **Rule 4 — Ignore formatting and typos.** All-caps, missing punctuation and > spelling errors are not politeness signals here. This is a deliberate scope > decision, not an oversight. Annotators route around what looks like an omission, silently and inconsistently. One clause stops that. ## The unclear contract If the label set has an "unclear" or "none of these" option — and it nearly always should — the guidelines have to say what it means, or annotators will each pick their own reading. It is one of: - **the item genuinely does not decide** (a real finding, keep it), or - **I cannot tell from what I was shown** (a data or context problem), or - **I do not know enough to judge** (a skip; a guess here is worse than nothing) They are different things and they want different handling downstream. Pick one per option and say so. If two of them matter, that is two options. ## Length Nobody reads a 4,000-word banner. They will read a 4,000-word codebook once, at the start, if the banner gives them the ten lines that cover most items and the codebook is one click away. A rough division that works: the scheme `description` is one line, the banner is under 200 words, and the codebook is however long the construct needs. ## Versions and change logs Guidelines that change mid-study split the data: items annotated before and after are annotated under different instructions, and nothing in the output records which. Put a version number and a change log at the bottom of the codebook. > 2.3 — added rule 6 (emoji are tone-neutral) and the autoreply edge case. If a change is big enough to alter past labels, say so in the handover and treat the earlier items as a separate round rather than quietly mixing them. ## Iterating from the pilot The pilot is what produces real guidelines. Three annotators, 50–100 items, then read the disagreements one at a time. This loop has a name. Pustejovsky and Stubbs's **MATTER** cycle — Model, Annotate, Train, Test, Evaluate, Revise — contains a subcycle, **MAMA** (Model-Annotate-Model-Annotate), which exists precisely to iterate between the model and pilot annotations "to increase the quality of the annotation scheme before investing the full amount of time and energy annotating the complete corpus." Writing guidelines once and launching skips it. **Treat every disagreement as a guideline defect until you have shown it is not.** The alternatives — the annotator was careless, the item is genuinely ambiguous — are real but rarer, and both are cheap to check afterwards. Working that way turns a disagreement list into a tie-break rule list. Two Potato surfaces feed this loop: - `training` items carry an `explanation` shown with the mark. It is the one place in a study where you can correct a misreading before it reaches 500 items — write it, and write it as the reason rather than the answer. Training is one of the factors Bayerl and Paul found moves agreement, along with how intensive it is, so this is not a nicety. - `/admin/iaa` gives agreement per scheme. A scheme far below the others is usually one label pair, and the pair names the rule you are missing. Budget for the definitions changing after the pilot. They usually do. ## Anti-patterns - **Defining a label with a synonym of itself.** "Toxic: content that is toxic." - **"Use your best judgment."** It is what the annotator was already doing; it is what you were supposed to replace. - **Examples that are all easy.** They set the expectation that hesitation means the annotator is doing it wrong. - **A taxonomy with no residual category.** See `designing-a-task.md`. - **Rules that contradict the label set.** If the guideline says "when torn, choose Neutral" and there is no Neutral, one of the two is wrong. - **The codebook only in the banner.** It pushes the questions below the fold on every item. - **Guidelines written after the config.** The label set is a claim about the construct; writing the definitions is how you find out the claim is wrong, and it is much cheaper before the data file is built. ## Before you hand it over - [ ] Every label has a rule, what counts, what does not, and its nearest neighbour - [ ] The two most confusable labels have a stated tie-break - [ ] At least one borderline worked example per label, cited by item id - [ ] The "unclear" option says which of the three things it means - [ ] Every deliberate scope exclusion is marked as deliberate - [ ] The banner is short enough that the first question is above the fold - [ ] A version number and a change log - [ ] Someone who was not in the design conversation labelled ten items with it The last one is the only real test. Everything above it is how to pass it. ## What to ask the researcher Label definitions are usually **theirs**, not yours: they carry the construct the paper is about, and getting them wrong invalidates the study rather than costing a re-render. Ask for their existing definitions before writing any — a codebook from a previous round, the paper the scheme comes from, or an annotation manual. If they have one, use their wording and their examples. If they have none, draft the four parts per label from what they told you, mark every definition as a draft, and put the borderline cases you could not settle at the top of `QUESTIONS.md`. Say plainly that these are the sentences that decide what the dataset means. `potato-showcase` is the other place to look: 281 of its 440 designs carry written instructions, most drawn from a published annotation scheme. See `finding-a-design.md`. If the annotators are a paid crowd rather than trained coders, Sabou et al.'s best-practice guidelines for crowdsourced corpus annotation are the thing to read alongside this file — the constraints are different when the reader is anonymous, unbriefed and paid per item. ## Sources - MacQueen, McLellan, Kay and Milstein, "Codebook Development for Team-Based Qualitative Analysis", *Cultural Anthropology Methods* 10(2):31–36, 1998. The six-component codebook entry, and inclusion/exclusion criteria. - Bayerl and Paul, "What Determines Inter-Coder Agreement in Manual Annotations? A Meta-Analytic Investigation", *Computational Linguistics* 37(4):699–725, 2011. - Finlayson and Erjavec, "Overview of Annotation Creation: Processes & Tools", in Pustejovsky and Ide (eds), *Handbook of Linguistic Annotation*, Springer, 2016. - Pustejovsky and Stubbs, *Natural Language Annotation for Machine Learning*, O'Reilly. The MATTER and MAMA cycles. - Sabou, Bontcheva, Derczynski and Scharl, "Corpus Annotation through Crowdsourcing: Towards Best Practice Guidelines", *LREC 2014*, 859–866. # Recording Design Decisions `DESIGN.md`, `QUESTIONS.md`, and estimating what a task will cost. It ships with the `potato-skill` skill: ``` /plugin marketplace add davidjurgens/potato-skill /plugin install potato-skill@potato ``` --- A config records what the task *is*. It does not record why the unit of annotation is the sentence rather than the document, why three annotators, or which of those choices you made up because the brief did not say. The researcher needs that to write a methods section, and the next person to touch the task needs it to know what is safe to change. Write `DESIGN.md` next to the config, and keep it to what a config cannot say: ```markdown # Design record **Unit of annotation.** One forum post. Threads were the alternative; posts won because the research question is about individual claims. **Questions.** Category (4 options, `Unclear` included so annotators are not forced), severity (3-point ordinal, so agreement is scored as ordinal), and a free-text follow-up shown only when the category is `Unclear`. **Annotators per item.** 3, to get a chance-corrected agreement figure. 12 items each, which is the whole corpus, so every annotator sees everything. **Quality control.** Two practice items with model answers, one attention check. No gold standards: annotators are known colleagues, not paid strangers. **Assumed, not confirmed:** the severity scale is 3-point; the researcher said "low/medium/high" in passing and never confirmed the wording. Ask before launch. The consent text is a placeholder and must be replaced with the approved wording. **Reconsider first:** the 4-way category. If agreement on it comes back below about 0.5, the labels are the problem, not the annotators. ``` Keep the assumptions separate and explicit. That paragraph is what makes the document worth opening — a design record that reads as if every choice was deliberate hides the ones that were not, and those are the ones that need checking. `QUESTIONS.md` is the other half: things you could not decide at all. Both, next to the config, alongside something that runs. ## Estimating effort The first thing a researcher asks about a design is what it costs, and the config already contains most of the answer: ```bash python /scripts/estimate_effort.py config.yaml --rate 15 ``` It reads the item count, the schemes, `num_annotators_per_item`, `max_annotations_per_user` and the quality-control files, and reports how many annotators are needed, how long each one is doing, total hours, and a cost if you give it a rate. It prints its assumptions with the number; reading speed and per-question times move the total most, so change those and re-run rather than quoting one figure. This is the cheapest place to catch a design that cannot be afforded. Three annotators on 5,000 items with a span scheme is around 200 hours of annotation, and it is much better to find that before the instructions are written. ## The assumptions in the estimate `estimate_effort.py` prints its assumptions with the number. Three of them are worth arguing with before quoting a figure to anyone: - **Reading speed.** The default is 240 words a minute, which is about right for skimming familiar prose and much too fast for a legal clause or a transcript. `--wpm 120` on hard material can double the total. - **Per-question times.** They come from a table inside the script: 4s for a radio, 45s for a span, 90s for a segmentation mask. They are order-of-magnitude right and no better. If the task is unusual, time yourself on five items and edit the table. - **Nothing for the instructions page, and no breaks.** A real annotator reads the instructions once, gets slower, and stops. Treat the number as the floor. The parts it does get exactly right, because they are in the config: the item count, the number of judgements, how many annotators the quota implies, and how many extra items each person sees because of training and quality control. # Starting From a Published Design The Potato Showcase, and what to change before reusing a design. It ships with the `potato-skill` skill: ``` /plugin marketplace add davidjurgens/potato-skill /plugin install potato-skill@potato ``` --- Three sources of prior art, and they cover different things. Reaching for the wrong one wastes an hour. | Source | What it gives | What it does not | |---|---|---| | `examples/` in Potato, 214 of them | One scheme or display type demonstrated cleanly | Question wording worth copying; a whole study | | **potato-showcase**, 440 designs | Real label sets, real question wording, written instructions, the paper each came from | Any workflow at all | | `worked-example.md` | The whole study: consent, instructions, training, checks, survey | Anything about your construct | The usual route on a new task is showcase for the questions, worked example for the workflow, `examples/` when one scheme is behaving oddly. ## The showcase — 440 annotation task designs, most built from a published paper or dataset. Each is a folder with `config.yaml`, `metadata.json` (title, paper, dataset, tags, complexity) and `sample-data.json`. Two things make it worth checking before writing a scheme from a field list: - **The wording is not invented.** A label set a paper used and reported agreement on has survived a pilot. Yours has not. - **281 of the 440 carry written `annotation_instructions`** — the part of a study this pack otherwise leaves you to write from nothing. See `writing-guidelines.md` for what to do with them. And the limit, which is sharp: **no design in the showcase has a `phases` block, a consent page, a training round or a post-study survey.** Zero. They are task designs, not studies. Take the questions from there and the study around them from `worked-example.md`. ## Searching it ```bash python /scripts/find_design.py --type span --category text python /scripts/find_design.py --query "dialogue safety" --with-instructions python /scripts/find_design.py --show text/education/mathdial-tutoring-dialogue ``` Filters: `--type` (repeatable, all must be present), `--display-type`, `--category`, `--complexity`, `--with-instructions`, `--with-paper`, `--query`. `--json` for the raw records. `--show ` prints the metadata and the whole config. The script reads `showcase.manifest.json`, which is built by `scripts/generate_manifest.py` in the showcase repo. It looks for a clone in the obvious places and in `$POTATO_SHOWCASE`, and falls back to downloading the manifest from GitHub. Pass `--showcase ` to be explicit. If you have a clone but no manifest, build it once: ```bash cd path/to/potato-showcase && python3 scripts/generate_manifest.py ``` `--show` prints the config only from a local clone; without one it prints the GitHub URL. ## Taking a design without inheriting its mistakes Copy the folder, then go through this: - **Re-read every label against your data.** The taxonomy was built for their corpus. A category with nothing in yours is worse than no category — it attracts arbitrary answers. - **Check for a residual option.** Published schemes often assume trained annotators and a discussion round, so some have no "unclear". - **Instructions are a starting draft, not the researcher's guidelines.** They are what the showcase design's author wrote, sometimes condensed from the paper. Name the source in `DESIGN.md`, and put the real definitions past the researcher. - **Cite it.** `metadata.json` carries `paperReference`, `paperUrl` and a BibTeX `citation`. If the scheme came from a paper, the study using it says so. - **The config is the truth, not the metadata.** `metadata.annotationTypes` is hand-maintained and six designs disagree with their own config. The manifest records both; `annotation_types_in_config` is the one that runs. - **Add the workflow.** No consent, no instructions phase, no training, no checks. That is all yours. # Building the Annotation Interface `instance_display`, the display types, and what a screenshot can prove. It ships with the `potato-skill` skill: ``` /plugin marketplace add davidjurgens/potato-skill /plugin install potato-skill@potato ``` --- The page an annotator sees has two halves: the item on top, controlled by `instance_display`, and the form below it, controlled by `annotation_schemes`. The navbar, progress indicator, Next button and keyboard hints come for free and are worth leaving alone. ## The item: `instance_display` Without it, Potato renders the field named by `item_properties.text_key` as plain text. That is the right answer for a text classification task and the wrong one for everything else. ```yaml instance_display: fields: - key: body # required: the field in your data file type: text # required: from the display registry label: "Comment" # optional heading above it span_target: true # optional: spans may be drawn on this field display_options: {} # optional, and type-specific layout: direction: vertical # or horizontal gap: "20px" # a CSS length, as a string resizable: true # default true ``` `fields` is required and cannot be empty. Each entry needs `key` and `type`. An unknown `type` fails validation with the valid list in the message. **The default path collapses horizontal whitespace, and `instance_display` keeps it.** This is the one reason to declare the block on a task that is otherwise plain text. Same item, same server, only the config differing: ``` no instance_display def f(x):\n if x > 0:\n return x\n return -x type: text def f(x):
if x > 0:
return x
return -x ``` Runs of spaces and tabs are squeezed to one on the default path, so two levels of indentation become one, a diff loses its gutter and a column-aligned table loses its columns. Nothing is logged and the config validates either way. Declare `instance_display` for anything where horizontal position carries meaning — code, diffs, logs, transcripts with speaker columns, poetry — and the field is rendered inside a `preserve-whitespace` container with real line breaks. `type: text` is enough; you do not need `code` unless you want the copy button and the scroll container. ### Laying the item out `instance_display.layout` is the whole of it: `direction` (`vertical` or `horizontal`) and `gap`. There is no per-field width, span or weight. That matters more than it sounds, because `direction: horizontal` divides the width **equally** between fields regardless of what is in them. Four fields on a 1500px window gave me four 198px columns: an 800x600 image rendered at 148x111 next to a one-line caption, beside a dialogue that ran 763px tall while its neighbours were under 200px. Each column is as wide as the narrowest thing deserves and as tall as the tallest thing needs. So for the common asks: - **An image beside its text.** Two fields, `direction: horizontal`. Accept the even split, or weight it with `base_css` — see below. - **Very long text.** Keep it `vertical` and give the field `collapsible: true` with `collapsed_by_default: true`, which renders a Show toggle instead of the body. A 7,000-character transcript then costs one line of page until the annotator wants it. - **A multi-party conversation.** `dialogue` with `speaker_key` and `text_key` naming your fields, plus `show_turn_numbers: true`. Each speaker gets its own colour down the left edge and a bold name; `alternating_shading: true` adds row banding. This is the one that needs no CSS at all. **Weighting a horizontal row needs `base_css` and a positional selector.** Each field is wrapped in a `.display-field-resizable` div, and that wrapper is the flex child — so a rule on `.display-field[data-field-key=...]` computes and does nothing, which is a confusing half-hour. The wrapper carries no key, so the only handle is position: ```css /* base_css: give the image three shares and the caption one */ .display-field-resizable:nth-child(1) { flex: 3 1 0; } .display-field-resizable:nth-child(2) { flex: 1 1 0; } ``` That took my image from 148px to 289px wide. It is positional, so it breaks if you reorder `fields` — leave a comment in the config saying so. **`max_height` and `min_height` take a bare number or a CSS length.** They are options of the resizable wrapper rather than of any one display type, so they work on every field: `max_height: 220` means 220px, and `max_height: "15rem"` comes through as `15rem` and computes to 240px. `50%` and other units pass through too. That matches `layout.grid.gap` and `instance_display.layout.gap`, so the whole block takes lengths the same way. When you do get an option wrong, the validator names the near miss — "is not an option for display type 'text' … Did you mean 'max_height'?" — and lists what that type accepts, which is the fastest way to check anything in `display_options`. **What you get for free, and it is more than you would guess.** The generated form is properly structured: every radio has an associated label, every question is a `fieldset` with a `legend`, images carry `alt`, and the document has `lang`. That is what a screen reader needs, and none of it requires configuration. The gap I have measured is that the page has no `

` — the task name lives in the navbar as plain text — so there is no top-level landmark for heading navigation. Custom canvas widgets are a separate question from the form; check those yourself if your annotators rely on assistive technology. ### The 24 display types | Type | For | |---|---| | `text` | Plain text | | `html` | Rich content (sanitized) | | `code` | Source with syntax highlighting | | `document` | DOCX, Markdown and similar — files, not a string field. See below | | `pdf` | PDF via PDF.js | | `spreadsheet` | Tables, annotated by row or cell | | `image` | Images, optionally zoomable | | `gallery` | Several images with captions; from 2.9.0, videos too | | `depth_map` | Depth data with windowing and a colormap | | `video` | Video player | | `audio` | Audio player | | `audio_dialogue` | Interview or podcast turns; one recording, per-turn play buttons that seek into it | | `dialogue` | Conversation turns, optionally threaded | | `conversation_tree` | Branching conversations, collapsible | | `multi_agent_discussion` | Several agents, colour-coded, filterable | | `pairwise` | Two things side by side | | `agent_trace` | Agent steps with type badges | | `coding_trace` | Coding-agent steps with diffs and terminal blocks | | `cot_trace` | Long chain-of-thought with a progress rail | | `eval_trace` | Three panes: reasoning, calls, final answer | | `web_agent_trace` | Screenshots with SVG overlays | | `interactive_chat` | Live chat, then the trace | | `live_agent` | Live agent viewer with controls | | `live_coding_agent` | Live coding agent with intervention | `display_options` differ per type, and from 2.8.2-11 a key the display does not accept is a validation error that names the alternatives: ``` instance_display.fields[0].display_options.no_such_option is not an option for display type 'text', so it would be ignored silently. Accepted options: collapsed_by_default, collapsible, key, max_height, preserve_whitespace. ``` Before that release nothing looked inside `display_options` at all: a misspelled option passed `validate --strict` with "OK — no issues found" and the page came up wrong with no error anywhere. If your Potato predates it, spell-check these by hand — that is where an hour goes. To see what a display takes: ```python from potato.server_utils.displays.registry import display_registry [d for d in display_registry.list_displays() if d["name"] == "multi_agent_discussion"] # optional_fields: ['collapse_environment', 'show_addressees', 'show_legend', # 'show_turn_numbers', 'speaker_key', 'text_key', 'thread_replies'] ``` **A display that reads a list has its own idea of the field names.** The conversation and trace displays (`dialogue`, `multi_agent_discussion`, `agent_trace`, `cot_trace`, `eval_trace`) take `speaker_key` and `text_key`, defaulting to `speaker` and `text`; `gallery` takes `url_key` and `caption_key`. Point one at turns shaped `{index, agent, text}` and the turns render with no speaker at all — just the text, in a `dialogue-turn` div with an empty `speaker-0` class and no name anywhere — while the annotation schemes above and below it read the same list correctly, because those take `agent_key`. Set `speaker_key: agent` and the names appear, with a colour per speaker. Nothing warns, and a reader skimming the page sees a conversation rather than an error. **`spreadsheet` takes `headers` or `columns`, and both work.** Give it `{headers: [...], rows: [[...]]}`, `{columns: [...], rows: [[...]]}`, or a plain list of row dicts, in which case the headers come from the first row's keys. I put the first two side by side on one page and they render identically, header row included. **`pairwise` takes its labels from `display_options`, not from the item.** `labels: ["Shift supervisor", "Automated report"]` names the two cells for the whole study; a `label` carried inside the data is ignored and the cells read "A" and "B". The data itself is `{left: ..., right: ...}` or a two-element list. **`pairwise` sits side by side at its defaults** from Potato 2.8.2: `cell_width` defaults to `auto`, which splits the row evenly and subtracts each cell's share of the gap. Measured at 1280px, both cells 398px wide on the same line. Before that the default was a literal `50%` in a flex row with a 16px gap and `flex-wrap: wrap`, and two halves plus a gap never fit, so the second cell wrapped onto its own line at every width — a comparison display that never compared. On an older build, set it under half: ```yaml - {key: summaries, type: pairwise, label: Two summaries, display_options: {show_labels: true, cell_width: 48%}} ``` **`document` expects a file that Potato ingested, not a field in your JSON.** The renderer reads `{rendered_html, metadata, text}` produced by the format pipeline (`data_directory`, `format_handling`). Point it at a plain string and that string is emitted as the field's HTML: Markdown is not rendered and `**Speaker:**` shows its asterisks, which setting `format_handling: {enabled: true, default_format: markdown}` does not change — that block renders markup in a field the pipeline already produced, not a raw string you supplied. Paragraph breaks do survive: the wrapper carries `document-preserve-structure` and `white-space: pre-wrap` whatever you set, so `preserve_structure` is a no-op because it is always on rather than because there is nothing to act on. `show_outline` draws nothing. `style_theme` reaches the DOM as a `document-theme-` class and takes `minimal` or `print` besides the default; a fourth value is refused at boot with all three named. It is a working span target either way, and spans anchor to the raw field value, newlines included. For a transcript you are shipping in a data file, `text` is the display you want. ### Geometry schemes need a display field for their media A geometry scheme (`image_annotation`, `spatial_annotation`, `region_caption`, `video_annotation` …) draws on a bitmap it does **not** fetch. `source_field` names the data key; the canvas takes its image from the `` that `instance_display` renders on the page. Remove the image field from `instance_display` and the canvas comes up empty — no `` in the DOM at all — even though the form still carries `data-source-field` and the media URL is still in the HTML. So an image annotation task needs both halves: ```yaml instance_display: fields: - {key: image, type: image, label: Screenshot} annotation_schemes: - annotation_type: image_annotation name: regions description: Draw a box around each problem. source_field: image tools: [bbox] # required; the error at boot names it labels: [Broken, Confusing] ``` and the consequence is that **the image renders twice**: once as the display field, once inside the canvas. That looks like a misconfiguration and is not. If the duplicate is distracting, hide the display copy rather than deleting it: ```css .display-field.display-type-image { position: absolute; left: -100000px; } .display-field-resizable:has(> .display-field.display-type-image) { height: 0 !important; } ``` (Observed on a task built from this pack; the CSS is one working answer, not the only one.) ### `span_target` A `span` scheme works without `instance_display` at all — the default text rendering supplies the wrapper spans are measured against. `span_target` is how you name the anchor field *once you define `instance_display`*, and then exactly one contract applies: ```python display_registry.get_span_target_capable_types() ``` Twelve types accept it. Nine anchor spans by character offset in the standard `.text-content` wrapper; `pdf`, `spreadsheet` and `agent_trace` anchor their own way — into the PDF.js text layer, per cell, and per step. Setting `span_target` on anything else fails validation and the error lists what is allowed. **`target_field` is about schemes, not fields.** The field a span lands in comes from where the drag was, always: I put three span-target fields on one page with a single scheme naming none of them, and all three spans recorded against the right field with the right offsets. So you do not need it to make a span land correctly. What it does is say which field a scheme *owns*, which is how a drag is routed to the right scheme when several span schemes share a page. Name it when you have more than one span **scheme**, not when you have more than one span-target **field**. It does not restrict: a scheme pinned to one field still records elsewhere, deliberately, because the alternative is an author pinning their only scheme to one field and silently losing every drag in the others. **A geometry scheme finds its media in `instance_display` if it has to.** `image_annotation` looks at `text_key` first, then falls back to the rendered image on the page, so a task with an `image` display field works without any extra wiring. Name `source_field` on the scheme anyway when the item carries more than one image, since the fallback takes the first one it finds; the console says which it used. **A span's offsets index the field as it was rendered, not as the data file holds it.** On a plain text field those are the same string. On a `dialogue` field they are not: the anchor is the turns joined as `"{speaker}: {text}"`, one per line, so an offset of 145 counts the speaker labels and their separators too. Reconstruct that exact string before slicing a dialogue span, or read the span's `text` from the export instead of computing it. It follows that **a display option which changes the rendering changes the anchor**. Turning on `show_turn_numbers` puts a `[1] ` in front of every turn and moves every offset after it. Settle the display options for a span-target field before anyone annotates against it, and treat changing them afterwards the way you would treat editing the item text. **Several span schemes can share one field, and their spans are independent.** Two schemes anchored to the same `target_field` — one marking entities, one marking which sentence is first-hand — draw over each other on the page and are stored separately, each span carrying its own `schema` and `target_field`. Use that instead of folding two questions into one label set. A span may also enclose one already drawn, in the same scheme or another, and both are kept. Spans do not live with the labels on disk. They are their own top-level key in `user_state.json`, `instance_id_to_span_to_value`, so a scheme that looks empty in `instance_id_to_label_to_value` may be fully annotated — see `getting-the-data-out.md` for the exported shape. ## The form: `annotation_schemes` Three keys are required on every scheme regardless of type: `annotation_type`, `name`, `description`. `description` is the question text the annotator reads, so write it as a question. **`name` is the storage key, so it has to be unique within the phase.** Since 2.8.2 two schemes sharing a name are refused, and the message says why: they also share one HTML input group, so answering the second clears the first and only one answer is saved. It applies across types: a `radio` and a `multiselect` sharing one name collide the same way, because the collision is the key rather than the widget. Names differing only in case are two names, and the same name on a phase page and in the annotation phase is fine, since those store separately. Nine more work on every type: | Key | Effect | |---|---| | `label_requirement` | `{required: true}`, or bare `true` | | `required` | Shorthand for the above | | `display_logic` | Show this scheme only under a condition | | `layout` | Where this scheme sits in the form grid — **not** how its choices are arranged. See below | | `humanize_labels` | **On by default.** Turns `not_relevant` into "Not relevant" — and also title-cases labels that were already prose, giving "Information **Or** Advice". Set `false` for prose labels | | `session_level` | One answer per session rather than per item | | `turn_level`, `turn_binding`, `turn_label` | Bind the scheme to conversation turns | Everything else is type-specific and listed in `annotation-types.md`. An `instance_display` field's own options — the ones `describe_display_type` lists as optional for that type — can be written flat on the field or nested under `display_options`, and the nested form wins when both are given. Only keys the type actually declares are picked up flat, so a misspelling is still silently ignored: check the rendered page, not the config. ### Quote `Yes` and `No` in a label list ```yaml labels: [Yes, No] # parses as [True, False] labels: ['Yes', 'No'] # what you meant ``` YAML reads `Yes`, `No`, `On`, `Off`, `True` and `False` — in any capitalisation — as booleans, so the most common label pair in annotation reaches Potato as two Python bools. The scheme then **does not render at all**: `identifier_utils` accepts a label that is a string or a dict and raises on anything else. `validate --strict` catches it from Potato 2.8.2 on, and names the cause: ``` annotation_schemes[0].labels[0] came through as the boolean True. YAML reads Yes/No, On/Off, Y/N or True/False in any capitalisation as a boolean [...] Quote it: labels: ["Yes", "No"]. ``` On any earlier build `--strict` said `OK — no issues found` — it checked that `labels` was a list and never looked at the elements — and the scheme simply **did not render**, with one line in the boot log as the only signal: ``` [ERROR] potato.server_utils.schemas.identifier_utils: Failed to generate layout for schema 'q1': Invalid label format: True ``` If that scheme was `required`, every `Next` after it failed with `400 /annotate` — an annotator stuck on item one with no visible question to answer. `grep "Invalid label format" server.log` after any edit to a label list, on a version whose validator you have not tested. Quoting is the fix and it costs nothing, so quote every short label. The dict form (`{name: 'No', key_value: '2'}`) is also safe, because the check that rejects a bool accepts a dict with a `name`. ### `select`, `constant_sum` and `ranking`: fixed in 2.8.2-10, broken before it Three widgets used to answer for the annotator or lose the answer they gave. All three are fixed as of `v2.8.2-10-ge54fdc26`, and all three were silent, so if you are on an earlier build the symptom is in the data rather than on the screen. A `select` emitted one `