decision-board
A loop for many decisions or answers at once, where the CLI's one-question-at-a-time flow would be tedious:
- Claude writes
board.json(the items and their choices). build.mjsturns it into a single self-contained HTML page.- The page is published as an Artifact with
capabilities: {db: {}, artifact: {}}. The user decides, comments, and asks questions on the page. Everything is saved to the Artifact's database automatically. - The user presses Notify Claude on the page (the page republishes itself, which reaches the publishing session as a republish notification), or says "read the decision board". Claude reads the answers back with
read_db, verifies against real code/files, and writes replies withwrite_db. Replies appear under each item as a purple callout, live.
The same HTML also works as a plain local file. Without the database it saves to localStorage, and the user copies Markdown/JSON from the "Send to Claude" button into the chat; Claude's replies go back as a JSON block the user pastes into "Import".
When to use it
- 20+ items to judge, or fewer items but each needs a comment or question.
- Mixed granularity: "drop this whole group, except these two".
- Several rounds of question and answer per item.
- Requirements gathering with dozens of questions, where free-text answers are needed.
Do not use it for one question with four or fewer options. AskUserQuestion is enough there.
Files in this skill
| File | Role |
|---|---|
SKILL.md |
This guide |
template.html |
The page. UI strings switch between ja and en via board.lang |
build.mjs |
node build.mjs <board.json> <out.html> — validates and generates |
examples/*.json |
Three ready-to-adapt boards: triage, hearing (free text), review findings |
examples/extract-strings.mjs |
Reproducible extractor: user-facing strings from a TS/JS codebase → items |
When this skill is loaded you are told its base directory. Use that path for build.mjs; do not assume ~/.claude/skills/decision-board.
Step 1 — write board.json
{
"id": "copy-review-2026-09", // optional; localStorage key ([A-Za-z0-9_-])
"lang": "ja", // "ja" (default) or "en" — UI language
"title": "SKIN MATE Copy Review", // page name: 2–4 words, specific
"description": "One sentence shown under the title",
"intro": "What to decide, what was already excluded, what happens next",
"options": [ // board-wide choices; default is yes / no / check
{ "value": "keep", "label": "Keep", "tone": "good" },
{ "value": "drop", "label": "Drop", "tone": "bad" },
{ "value": "check", "label": "Check", "tone": "warn" }
],
"items": [
{
"id": "components.chat.ChatInput.tsx:189:0", // becomes the DB doc id: [A-Za-z0-9_.~:@+-], unique, stable
"group": "3. Chat", // section; navigation and group-wide bulk decision
"subgroup": "app/components/chat/ChatInput.tsx", // optional sub-section (file, screen, topic)
"subgroupNote": "optional note shown once under the subgroup header",
"text": "Type a message", // the thing to decide on
"kind": "visible text", // optional category chip; a select filter appears when items carry 2+ kinds
"meta": ["L189"], // optional chips
"note": "optional context, prefixed with ※",
"question": "optional question from Claude to the user, shown as Q.",
"options": [], // per-item override; [] = free text only
"free": true // do not wrap text in quotes (use for question-style items)
}
]
}
Rules that matter:
- Keep
idstable across regenerations (file + line, a ticket key, a slug). Answers are keyed by id; a changed id orphans its answer. - Groups appear in first-seen order. Prefix with numbers to pin the order.
- Up to a few hundred items per board. The database holds 5,000 documents; one item uses one.
- Write
introfrom the user's side: what they are deciding, what is excluded, what happens after. - Use
tonefor meaning:good= positive,bad= negative,warn= needs attention. Labels are free.
Authoring checklist (the board is only as good as its items)
The page cannot show what was never listed, and reviewers cannot see why something is classified the way it is. Before building:
- State the scope in
intro: what is included, what was deliberately excluded and why, and what happens after the decisions. - Make exclusions visible. Put items you excluded with a judgment call (not by a mechanical rule) into a final group such as
"Z. Excluded (check the reasoning)"with"options": []and the reason innote, so the user can pull one back in. - Extract mechanically when the items come from code or files.
examples/extract-strings.mjswalks a TS/JS codebase with the compiler API (no comments, stable ids); adapt it rather than grepping by hand. - Put the evidence in
notefor any classification that is not obvious from the text (e.g. "aria-label on an icon button", "only rendered when the video fails"). - Turn low confidence into a
questioninstead of a silent guess. - Self-check before publishing: pick 10 random items and verify text, location and kind against the source; compare the item count with the raw extraction and account for the difference; run the build and read its warnings.
Step 2 — build
node <skill-dir>/build.mjs board.json out/board.html
The build fails on a missing title, empty items, an invalid or duplicate id, or a missing text. Fix and rerun.
Step 3 — publish
Call the Artifact tool with the generated file, capabilities: {"db": {}, "artifact": {}}, and a favicon on the first publish. db stores the answers; artifact lets the page republish itself so the Notify Claude button can wake this session. Forgetting capabilities silently degrades the page to local-only mode; omitting artifact hides the button (the user must then say "read the decision board").
Then tell the user:
- the URL;
- decisions are saved automatically; press Notify Claude when done (or say "read the decision board" if the session that published the page is no longer running);
- "Send to Claude" is only needed for another chat or for keeping a copy.
If the user wants a local file instead, hand over the HTML as is and explain the copy-and-paste loop.
Step 4 — read the answers back
Start this step when either happens:
- a notification says the artifact was republished from elsewhere and the new version carries
<meta name="decision-board-notified">— that is the page's Notify Claude button (each press adds one version; the content is otherwise unchanged); - the user asks you to read the board.
After a page-triggered republish, the artifact's live version is newer than the file you published. Before your next Artifact publish of the same file, read the artifact (or accept the conflict guidance) so the publish is not refused as stale; the HTML content itself is unchanged, so republishing your local file is safe.
Artifact action:read_db url:<URL> db_op:list collection:answers query:{"limit":1000}
Artifact action:read_db url:<URL> db_op:get collection:board doc_id:meta
For large boards add out_dir and aggregate with a script instead of reading inline.
answers/<id>={choice, comment, updatedAt}. Emptychoicemeans undecided.board/meta.memo= the board-wide notes and questions.
Then:
- Tally by
choice; read every non-emptycomment. - A comment that is a question gets answered after checking the real code or files. Never guess.
- For anything marked with a
bad-tone choice (drop, reject), check what would break if removed: references, tests, accessibility text, CSS that assumes it.
Step 5 — reply
Per item: claude/<id> with {note, updatedAt}. Board-wide: board/claude with {note, updatedAt}. The page subscribes to both, so writes appear immediately.
Artifact action:write_db url:<URL> db_op:batch writes:[
{"op":"set","collection":"claude","doc_id":"<item id>","data":{"note":"...","updatedAt":"<ISO 8601>"}}
]
Batches take up to 50 writes; split larger sets. Keep notes short, conclusion first. Every item the user marked with a warn-tone choice gets a reply (a finding, or the question you need answered to decide).
To ask new questions, add question to the items in board.json, rebuild, and republish to the same URL. Answers survive as long as ids are unchanged.
Without the database (local file, or pasted export)
The Markdown export ends each line with <!-- <id> -->; the JSON export has answers: {id: {choice, comment}}. Match on id.
Reply with a JSON block the user pastes into "Import":
{
"claude": { "<item id>": { "note": "..." } },
"claudeGlobal": "board-wide reply (optional)"
}
Design assumptions
- Writes are last-writer-wins. The page is built for one deciding user, though anyone in the organization with the link can view it. A
dbArtifact cannot be shared publicly. - Notify Claude only reaches a session that is still running and watching the artifact. It is a convenience, not a queue: a press after the session ended is lost, and the fallback is always "read the decision board" in a new session (pass the URL).
- Users write
answers, Claude writesclaude. Neither overwrites the other. - Bulk decisions (group or subgroup) overwrite existing decisions in that scope.
- The page always mirrors state to
localStorage. On connecting to the database, local-only answers are pushed up; answers present in both prefer the database copy.