- name
- officecli-word-form
- description
- Use this skill to create fillable Word forms (.docx) with real Content Controls (SDT) + legacy FormField checkboxes + MERGEFIELD mail-merge placeholders + document protection. Trigger on: 'fillable form', 'form fields', 'content controls', 'SDT', 'word form', 'fill in', 'only editable fields', 'protect document', 'onboarding form', 'HR intake', 'survey template', 'contract / SOW template', 'mail-merge template', 'compliance checklist', 'medical intake questionnaire'. Output is a single .docx where specific fields are editable and the rest is locked. This skill is INDEPENDENT, not a scene layer on docx - payload is `<w:sdt>` + `<w:ffData>` + `<w:fldChar>` + `documentProtection`, none of which docx base skill covers. Do NOT trigger for regular reports, letters, memos, academic papers, pitch decks, or any document with no user-fillable fields - route those to officecli-docx or its scene layers.
# OfficeCLI Word-Form Skill
**This skill is INDEPENDENT, not a scene layer on docx.** A form's payload - `<w:sdt>` controls, `<w:ffData>` legacy fields, `<w:fldChar>` mail-merge, `documentProtection` - is a distinct element class from docx's paragraph/heading/style primitives. Its QA is different too: docx's Delivery Gate cares about visual layout and live PAGE fields, this skill's cares about data plumbing (protection enforced / alias+tag / items injected / name ≤ 20 / no underscore anti-pattern). **Reverse handoff:** if the user's document has no fillable fields (report, letter, memo, thesis, proposal), route to `officecli-docx` or a docx scene skill - don't use this one.
## Runtime contract (critical)
Wayland Desktop supplies a checksum-pinned native `officecli` runtime. Do not install, update, download, or replace it from this skill. Run `officecli --version`; native authoring requires the exact verified `1.0.136` version. Verify the required form elements with `officecli help docx`. If either check fails, stop and use Wayland's reinstall/update recovery path. Never bootstrap mutable code or silently switch to hosted-credit generation.
## Help-First Rule
This skill teaches what a real form needs, not every CLI flag. When a prop / alias / enum is uncertain, consult help BEFORE guessing: `officecli help docx [element] [--json]` (e.g. `sdt`, `formfield`, `field`). Help is pinned to installed version - when this skill and help disagree, **help wins**. Every `--prop X=` below was verified against `officecli help docx <element>` on v1.0.63.
## Mental Model & Inheritance
A Word form is a `.docx` plus four OpenXML payload layers plain-docx skills do not touch: **`<w:sdt>`** content controls (5 types: text / richtext / dropdown / combobox / date), **`<w:ffData>`** legacy FormField (ONLY way to get a real checkbox on v1.0.63), **`<w:fldChar>`** complex fields (MERGEFIELD, REF, PAGEREF, SEQ, IF - template-time, not user-fill), and **`documentProtection`** (the lock that makes non-field text read-only in Word).
**No inheritance from docx v2.** docx's Delivery Gate (cover-fill %, live-PAGE check) does NOT apply - form QA is `view forms` + `query sdt alias+tag` + `protectionEnforced`.
**Reverse handoff to docx.** Route back to `officecli-docx` for reports / letters / memos / thesis / pitch decks / any document with no editable fields. Use **this** skill when the document's purpose is data capture or template merge.
## Shell & Execution Discipline
**One command at a time. Read output before the next.** OfficeCLI is incremental - every `add` / `set` / `remove` immediately mutates the file. All recipes below use `FILE=form.docx` as a shell variable.
**Three shell-escape layers:**
1. **Quote every path with `[N]`** - zsh/bash glob-expand brackets. `officecli get "$FILE" /body/sdt[1]` fails with `no matches found`. Correct: `officecli get "$FILE" '/body/sdt[1]'`.
2. **Single-quote any prop containing `$`** - `"Total: $50,000"` becomes `"Total: ,000"` after `$50` variable expansion. Correct: `'Total: $50,000'`.
3. **`--after find:<text>` uses outer single quotes, never inner double quotes** - `--after find:"Client Signature:"` makes the quotes part of the search string; match fails. Correct: `--after 'find:Client Signature:'`.
**`WARNING: UNSUPPORTED` (exit 2) is a silently-wrong element.** The CLI created the element _without_ the rejected prop - dropdown with no items, date with default format, SDT with no lock. Any UNSUPPORTED in your build log means your command was wrong: stop, rewrite to Path B (raw-set) or a separate `set`. Do not ship on top.
**`protection=forms` is the LAST command.** Not CLI-enforced - `add` / `set` / `raw-set` still run under any protection mode - but finishing with protection gives Word users a consistent locked experience on first open.
### `--after find:` micro-playbook
`--after find:<text>` matches the **first** occurrence. Bad anchor = wrong insertion location, expensive to debug. Three rules:
1. **Anchor must be globally unique.** In a two-party contract a label like "Signature" matches both parties - use a unique phrase like "Signature (Service Provider)" or a full role-qualified title.
2. **After insert, `/body/p[last()]` is unreliable** - the find insertion changes `<w:body>` child order. To continue operating on the new paragraph, read its real paraId: `officecli query "$FILE" paragraph --json | jq -r '.data.results[-1].format.paraId'`.
3. **Non-ASCII characters and full-width parens `()`** match literally in `find`, but when unsure, `officecli view "$FILE" text | grep -n "anchor"` first to confirm the exact bytes in the file.
```bash
# Trap: first-match hits Party A only, Party B missed
officecli add "$FILE" /body --type sdt --after 'find:Signature'
# Fix: two signatories, two unique anchors
officecli add "$FILE" /body --type sdt --prop alias=Party_A_Name --prop tag=party_a \
--after 'find:Signature (Service Provider)'
PID_A=$(officecli query "$FILE" paragraph --json | jq -r '.data.results[-1].format.paraId')
officecli add "$FILE" "/body/p[@paraId='$PID_A']" --type sdt --prop alias=Party_A_Title --prop tag=party_a_title
```
Inline SDT via `--after find:` is added as a child of the matched paragraph, not as a new paragraph - use this when label + SDT must share a line.
## What makes a real form (identity)
A real fillable form requires **structured fields** + **document protection**.
| Approach | Word user sees | CLI-readable | Real form? |
| --------------------------------------- | -------------------------------------------- | -------------------------------- | ----------------------- |
| SDT controls + `protection=forms` | Gray-bordered fields; rest locked | `query sdt` / `view forms` | **YES** |
| FormField checkbox + `protection=forms` | Real clickable checkbox; rest locked | `query formfield` / `view forms` | **YES** (checkbox only) |
| MERGEFIELD placeholders | `«CustomerName»` merged by downstream engine | `query field` | **YES** (template-time) |
| Underscores `___` / blank lines | Visual-only; whole doc editable | No - no structured fields | **NO** |
**Do not simulate fields with underscores.** `Name: _______________` produces zero structured data and leaks past every verification. Always use `--type sdt` or `--type formfield`.
**Checkbox is formfield, NOT SDT.** `--type sdt --prop type=checkbox` exits 1 (`SDT type 'checkbox' is not implemented`). Every checkbox in every recipe uses `--type formfield --prop type=checkbox`.
**MERGEFIELD is a separate track.** `view forms` lists SDT + formfield only; `query field` lists complex fields only. Two disjoint inventories; both valid in one file.
## Requirements for Outputs (hard floor)
Every form must satisfy these - Delivery Gate enforces each as an executable check.
1. `protection=forms` enforced (`get $FILE /` → `protectionEnforced=True`).
2. Every SDT has both `alias` + `tag`.
3. Every dropdown/combobox has non-empty `items=...` in `view forms`.
4. Every date SDT shows the intended `format=...`.
5. Every locked SDT shows `lock=sdtLocked` / `contentLocked` / `sdtContentLocked` as intended.
6. Zero `WARNING: UNSUPPORTED` in build log.
7. Zero `type=checkbox` on any SDT.
8. Every formfield `name` ≤ 20 characters.
9. Zero underscore-line / blank-line placeholders.
10. Field types match user intent (short text / paragraph / fixed list / list+custom / date / boolean).
## Three Paths (core decision)
CLI v1.0.63 exposes exactly **four canonical props** on SDT: `{type, tag, alias, text}`. Everything else - `items`, `format`, `lock`, `placeholder`, `name`, `maxlength` - is UNSUPPORTED at add-time and silently discarded. The skill therefore splits every SDT need into three paths. **Pick the path before writing a single command.**
### Path A - Pure CLI (simple forms)
**Use when**: the field only needs a label, an initial text, and a type. Acceptable if dropdown/combobox items can be empty at first and dates can default to `yyyy-MM-dd`.
```bash
officecli add "$FILE" /body --type sdt \
--prop type=text \
--prop alias="Full Name" --prop tag=full_name \
--prop text="Enter full name"
# Canonical follow-ups (not on add):
# officecli set "$FILE" '/body/sdt[N]' --prop lock=sdtlocked
# officecli set "$FILE" / --prop protection=forms
```
### Path B - CLI + `raw-set` bridge (complex attrs)
**Use when**: dropdown/combobox needs options, or date needs a non-default format. `raw-set` is OfficeCLI's universal OpenXML fallback - `officecli --help` lists it as a top-level command.
```bash
# Step 1 - Path A skeleton (generates <w:dropDownList/> automatically)
officecli add "$FILE" /body --type sdt \
--prop type=dropdown --prop alias="Department" --prop tag=dept
# Step 2 - raw-set injects <w:listItem>s
officecli raw-set "$FILE" /document \
--xpath "//w:sdt[w:sdtPr/w:tag/@w:val='dept']/w:sdtPr/w:dropDownList" \
--action append \
--xml '<w:listItem xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" w:displayText="Engineering" w:value="Engineering"/><w:listItem xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" w:displayText="Finance" w:value="Finance"/>'
```
### Path C - Word template (beyond raw-set)
**Use when**: `picture` SDT (signature image), real SDT checkbox (`type=checkbox` exits 1), `placeholderDocPart` prompt text, grouped SDTs wrapping multiple paragraphs, or custom richtext appearance. These involve cross-part relationships or nesting beyond `--prop` reach.
```bash
# One-time in Word: Developer tab → Insert Content Control → Save as template.docx
cp templates/onboarding_with_signature.docx "$FILE"
officecli open "$FILE"
officecli view "$FILE" forms # inspect embedded controls + paths
officecli set "$FILE" '/body/sdt[@sdtId=3]' --prop text="Jane Smith"
officecli set "$FILE" / --prop protection=forms
```
### Decision table
| Need | Path | Note |
| ------------------------------------------------ | ---------------- | --------------------------------------------------------------- |
| text / richtext SDT with default string | **A** | four canonical props cover it |
| text SDT that must be locked | **A + set lock** | `lock` only takes effect via `set`, not `add` |
| dropdown / combobox **with options** | **B** | raw-set append `<w:listItem>` |
| date SDT with non-default format | **B** | raw-set setattr `w:dateFormat/@w:val` |
| real checkbox | **FormField** | `--type formfield --prop type=checkbox` (see §Legacy FormField) |
| mail-merge placeholder | **MERGEFIELD** | `--type field --prop fieldType=mergefield` (see §MERGEFIELD) |
| signature picture, grouped SDT, placeholder part | **C** | build skeleton in Word, fill via CLI |
## Quick Start - Path A + FormField (minimal intake form)
Two SDT text fields, one checkbox, protection. Paste and adapt; this is the smallest form worth shipping.
```bash
FILE=intake.docx
officecli close "$FILE" 2>/dev/null; rm -f "$FILE" # preflight: clear stale resident / prior file (cold-start after CLI upgrade commonly leaks a resident)
officecli create "$FILE"
officecli open "$FILE"
officecli set "$FILE" / --prop title="Employee Onboarding Intake" \
--prop docDefaults.font="Calibri" --prop docDefaults.fontSize="12pt"
officecli add "$FILE" /body --type paragraph \
--prop text="Employee Onboarding Intake" --prop style=Heading1 \
--prop size=20 --prop bold=true --prop spaceAfter=18pt
officecli add "$FILE" /body --type paragraph \
--prop text="Full Name:" --prop size=11 --prop bold=true --prop spaceAfter=4pt
officecli add "$FILE" /body --type sdt --prop type=text \
--prop alias="Full Name" --prop tag=full_name --prop text="Enter full name"
officecli add "$FILE" /body --type paragraph \
--prop text="Start Date:" --prop size=11 --prop bold=true --prop spaceAfter=4pt
officecli add "$FILE" /body --type sdt --prop type=date \
View on GitHub