A Worked Example¶
One complete task, annotated, and what to change first. It ships with the potato-tasks skill:
/plugin marketplace add davidjurgens/potato-skill
/plugin install potato-tasks@potato
Copying something that works beats assembling a config from field lists. Potato ships 214 examples for exactly this reason — but they are text-classification and schema demos, and none of them is a whole study: consent, instructions, a practice round, attention checks, the annotation itself, and a survey at the end.
This is that skeleton. Every block below is the literal content of a file in
examples/advanced/full-study-skeleton/, which validates under --strict and
boots with Loaded 2 training instances, Loaded 1 attention check items and
nothing about phases. A test in the Potato repo fails if this page and those
files stop agreeing, so what you copy is what was run.
Change the labels and the prose; keep the shapes.
project/
├── config.yaml
├── data/
│ ├── items.json # the real items
│ ├── training.json # practice, with model answers
│ └── attention.json # attention checks
├── pages/
│ ├── consent.jsonl
│ ├── instructions.jsonl
│ └── poststudy.jsonl
└── static/
└── study.css
Every file in that tree is below. A phase whose file: is missing does not stop
the boot — the phase is dropped with one ERROR line and the study runs without
it — so copy all three page files, not just the one you plan to edit.
config.yaml¶
# yaml-language-server: $schema=https://potatoannotator.readthedocs.io/en/latest/schemas/potato-config.schema.json
annotation_task_name: Full Study Skeleton
task_dir: .
output_annotation_dir: annotation_output/
data_files: [data/items.json]
item_properties:
id_key: id
text_key: body
instance_display:
fields:
- {key: title, type: text, label: Title}
- {key: body, type: text, label: Item, span_target: true}
layout: {direction: vertical, gap: 10px}
resizable: false # otherwise one line of text gets a 130px card
base_css: static/study.css
annotation_instructions: >-
<p><b>Reminder.</b> One line per rule, not the full definitions. Those live on
the instructions page.</p>
annotation_schemes:
- annotation_type: span
name: evidence
description: Highlight the part that decides your answer.
target_field: body # names the span_target field above
labels: [Evidence, Counter-evidence]
humanize_labels: false
sequential_key_binding: true
show_span_labels: true
- annotation_type: radio
name: category
description: Which category is this?
labels: [A, B, C, Unclear] # always have a "none" or "unclear" option
humanize_labels: false
sequential_key_binding: true
label_requirement: {required: true}
- annotation_type: likert # ordered judgement -> ordinal metric
name: severity
description: How severe is it?
size: 3
min_label: Low
max_label: High
humanize_labels: false
labels: [Low, Medium, High] # label every point, not just the ends
sequential_key_binding: true
label_requirement: {required: true}
- annotation_type: text # follow-up, hidden until it applies
name: why_unclear
description: What made it unclear?
multiline: true # without this a `text` scheme is a one-line <input>
rows: 3
min_chars: 10
display_logic:
show_when:
- {schema: category, operator: equals, value: Unclear}
num_annotators_per_item: 3
assignment_strategy: random
random_seed: 1
max_annotations_per_user: 12 # every annotator sees all 12 items
require_fully_annotated: true
training:
enabled: true
data_file: data/training.json
allow_retry: true
feedback: {enabled: true}
attention_checks:
enabled: true
frequency: 4
items_file: data/attention.json
min_response_time: 5
failure_handling: {action: warn}
agreement_metrics: {enabled: true}
phases:
order: [consent, instructions, training, annotation, poststudy]
consent: {type: consent, title: Consent, file: pages/consent.jsonl}
instructions: {type: instructions, title: Instructions, file: pages/instructions.jsonl}
training: {type: training, title: Practice}
annotation: {type: annotation}
poststudy: {type: poststudy, title: Last questions, file: pages/poststudy.jsonl}
login: {type: password}
user_config: {allow_all_users: true}
data/items.json¶
Any of JSON array, JSONL, CSV or TSV. Fields beyond id_key and text_key are
available to instance_display by name.
[
{"id": "c001", "source": "forum", "title": "Thread 1", "body": "Body text of item 1. It has two sentences so a span has somewhere to go."},
{"id": "c002", "source": "forum", "title": "Thread 2", "body": "Body text of item 2. It has two sentences so a span has somewhere to go."},
{"id": "c003", "source": "forum", "title": "Thread 3", "body": "Body text of item 3. It has two sentences so a span has somewhere to go."},
{"id": "c004", "source": "forum", "title": "Thread 4", "body": "Body text of item 4. It has two sentences so a span has somewhere to go."},
{"id": "c005", "source": "forum", "title": "Thread 5", "body": "Body text of item 5. It has two sentences so a span has somewhere to go."},
{"id": "c006", "source": "forum", "title": "Thread 6", "body": "Body text of item 6. It has two sentences so a span has somewhere to go."},
{"id": "c007", "source": "forum", "title": "Thread 7", "body": "Body text of item 7. It has two sentences so a span has somewhere to go."},
{"id": "c008", "source": "forum", "title": "Thread 8", "body": "Body text of item 8. It has two sentences so a span has somewhere to go."},
{"id": "c009", "source": "forum", "title": "Thread 9", "body": "Body text of item 9. It has two sentences so a span has somewhere to go."},
{"id": "c010", "source": "forum", "title": "Thread 10", "body": "Body text of item 10. It has two sentences so a span has somewhere to go."},
{"id": "c011", "source": "forum", "title": "Thread 11", "body": "Body text of item 11. It has two sentences so a span has somewhere to go."},
{"id": "c012", "source": "forum", "title": "Thread 12", "body": "Body text of item 12. It has two sentences so a span has somewhere to go."}
]
data/training.json¶
An object, not an array. Each instance needs id, text, correct_answers.
{
"training_instances": [
{"id": "t1",
"text": "Worked example one. The model answer is A.",
"correct_answers": {"category": "A", "severity": "Low"},
"explanation": "Why A is right, in a sentence the annotator can act on."},
{"id": "t2",
"text": "Worked example two. Nothing here decides the category, so it is Unclear.",
"correct_answers": {"category": "Unclear", "severity": "Low"},
"explanation": "Unclear is a real answer, not a way of skipping the item."}
]
}
correct_answers keys are scheme names; values are the label strings exactly as
written in labels:. Span schemes cannot be graded here. The training page
renders text only, so fold any context the annotator needs into that string.
data/attention.json¶
An array. Each item needs id and expected_answer, plus whatever fields your
instance_display renders.
[
{"id": "chk1",
"title": "Attention check",
"body": "This is a check, not a real item. Highlight this sentence and mark it Evidence, select A for category, and Low for severity.",
"text": "This is a check, not a real item. Highlight this sentence and mark it Evidence, select A for category, and Low for severity.",
"expected_answer": {"category": "A", "severity": "Low"}}
]
Instruct every required scheme, span schemes included. A check item that leaves a required span unanswered dead-ends the annotator on a 400 with only a corner toast for feedback.
pages/consent.jsonl¶
One annotation scheme per line, same syntax as annotation_schemes. Prose goes
in a pure_display scheme with allow_html: true.
{"annotation_type":"pure_display","name":"consent_text","allow_html":true,"description":"<h3>Consent</h3><p>Draft. Replace with the approved wording.</p>"}
{"annotation_type":"radio","name":"consent_agree","description":"Do you agree to take part?","labels":["I agree","I do not agree"],"humanize_labels":false,"label_requirement":{"required":true}}
pages/instructions.jsonl¶
The full definitions live here, not in annotation_instructions — that banner
sits on top of every item and pushes the questions below the fold.
{"annotation_type":"pure_display","name":"instructions_text","allow_html":true,"description":"<h3>What to do</h3><p>Read the item, highlight the part that decides your answer, then pick a category and a severity.</p><p><b>A.</b> Replace with the real definition.</p><p><b>B.</b> Replace with the real definition.</p><p><b>C.</b> Replace with the real definition.</p><p><b>Unclear.</b> Use it when the item genuinely does not decide. It is an answer, not a skip.</p>"}
pages/poststudy.jsonl¶
A phase page can ask real questions, not just display prose. These land in
annotation_output/<user>/ like any other answer.
{"annotation_type":"pure_display","name":"poststudy_text","allow_html":true,"description":"<h3>Last questions</h3><p>Two questions, then you are done.</p>"}
{"annotation_type":"likert","name":"task_clarity","description":"How clear were the instructions?","size":5,"min_label":"Very unclear","max_label":"Very clear","label_requirement":{"required":true}}
{"annotation_type":"text","name":"comments","description":"Anything that was hard to judge?","multiline":true,"rows":3}
static/study.css¶
Phase-page prose lands in a <legend>, which the theme sets to weight 600, so
consent and instructions render entirely bold without this.
form.pure-display legend { font-weight: 400; line-height: 1.55; }
form.pure-display legend h3 { font-weight: 650; }
form.pure-display legend b, form.pure-display legend strong { font-weight: 650; }
Check it¶
potato validate config.yaml --strict
python .claude/skills/potato-tasks/scripts/boot_and_check.py config.yaml -p 8000
boot_and_check.py backgrounds the server, waits for a 200, and reports every
feature that is configured but loaded nothing — which is the failure the log
lines above exist to catch. By hand:
nohup potato start config.yaml -p 8000 > server.log 2>&1 &
until curl -s -o /dev/null http://localhost:8000/; do sleep 2; done
grep -E "ERROR|Loaded [0-9]+|phase" server.log
Then walk it in a browser — running-a-task.md.
What to change first¶
| Want | Change |
|---|---|
| No practice round | Drop training: and training from phases.order |
| No attention checks | Drop attention_checks: |
| One annotator per item | num_annotators_per_item: 1, drop agreement_metrics |
| Images, audio, PDFs, dialogues | instance_display.fields[].type — see building-the-ui.md |
| Screening before the task | A prestudy phase with its own page file |
| Different questions per cohort | scheme_sets + batch_assignment |