CLI¶
wordlive ships a Click-based CLI designed for LLM tool-use loops: JSON in,
JSON out, deterministic exit codes, one structured object per invocation on
stdout. The CLI is a thin wrapper over the Python API — same
politeness, same atomic-undo.
Overview¶
Flags and exit codes that apply to every invocation.
Global flags¶
wordlive [--json|--text] [--doc DOC_NAME] [--save-dir DIR]... [--image-dir DIR]... <subcommand> [args]
| Flag | Default | Purpose |
|---|---|---|
--json/--text |
--json |
Output format. --text prints a per-command human form (indented outline tree, bare text for reads, one-line acks for writes); JSON stays the LLM-friendly default. |
--doc DOC_NAME |
active doc | Target a specific open document by name (e.g. Report.docx). |
--save-dir DIR |
none (deny) | Allow save/save-as/export-pdf to write under DIR (repeatable; merges with WORDLIVE_SAVE_DIRS). Default-deny: with no directory configured, saving is off. |
--image-dir DIR |
none | Restrict insert-image --path to files under DIR (repeatable; merges with WORDLIVE_IMAGE_DIRS). Non-local paths (UNC, URLs) are always rejected regardless. |
-h, --help |
— | Show help for the command or subgroup. |
-v, --version |
— | Print wordlive <version> and exit. |
-A, --about |
— | Print the about screen — banner, version, author, license, repo — and exit. |
Exit codes¶
The CLI's error boundary classifies every WordliveError into a
deterministic exit code so an LLM tool-use loop can branch on the failure
mode without parsing strings:
| Code | Meaning | Source exception |
|---|---|---|
0 |
OK | — |
1 |
Other / unclassified | WordliveError (default), DocumentNotFoundError, ImageSourceError, PathNotAllowedError (save/image policy denial) |
2 |
Anchor or style missing | AnchorNotFoundError / StyleNotFoundError (also used for zero-match find/replace --find) |
3 |
Word busy / modal | WordBusyError (retryable) |
4 |
Word not running | WordNotRunningError |
5 |
Ambiguous match | AmbiguousMatchError (multiple find hits without --all/--occurrence) |
6 |
Excel not available | ExcelNotAvailableError (insert-chart needs Excel installed) |
See the Errors page for the full exception taxonomy and retry guidance.
Inspecting a document¶
Read-only probes that report what the document is and where things sit.
status¶
List all open documents and mark which is active. Each entry carries a name
(always non-empty — Document1 for a document never saved), the on-disk path
(empty until the document is saved), a saved flag, and is_active.
$ wordlive status
[{"name": "Report.docx", "path": "C:\\Users\\me\\Report.docx", "saved": true, "is_active": true},
{"name": "Document2", "path": "", "saved": false, "is_active": false}]
Failures: 4 if Word isn't running (returns [] to stdout and the error
on stderr). Useful as a probe before issuing other commands.
outline¶
Heading outline of the target document, with addressable anchor IDs. Pass
--all to list every paragraph (headings and body text and list items)
as para:N — identical to paragraphs.
$ wordlive outline
[{"level": 1, "text": "Introduction", "anchor_id": "heading:1"},
{"level": 2, "text": "Context", "anchor_id": "heading:3"},
{"level": 1, "text": "Risks", "anchor_id": "heading:8"}]
This is the entry point for LLM workflows that need to discover what's
addressable in the document. The emitted anchor_id strings are exactly
what replace, go-to, insert, and exec consume.
paragraphs¶
List every paragraph in document order — headings, body text, and list
items alike — each with a para:N anchor, its outline level, the applied
Word style name, an is_heading flag, character start/end offsets, and
its text. outline --all is an alias. Use style to mirror an existing
document's formatting: level is 10 for every non-heading paragraph, so
only the style name tells a List Number item apart from Normal body text.
$ wordlive paragraphs
[{"index": 1, "anchor_id": "para:1", "level": 1, "style": "Heading 1", "is_heading": true, "start": 0, "end": 13, "text": "Introduction"},
{"index": 2, "anchor_id": "para:2", "level": 10, "style": "List Number", "is_heading": false, "start": 13, "end": 29, "text": "First item."},
{"index": 3, "anchor_id": "para:3", "level": 2, "style": "Heading 2", "is_heading": true, "start": 29, "end": 35, "text": "Risks"}]
para:N shares its index space with heading:N — paragraph 1 is both
para:1 and (because it's a heading) heading:1. The emitted offsets feed a
range:START-END target for an offset-precise, mid-paragraph
insertion via replace.
locate --anchor-id ID¶
Report where an anchor sits in the laid-out document — a non-visual layout
read that answers "what page is this on" without a snapshot vision pass.
Returns the anchor's page span (page / end_page — the pages its first and
last characters fall on, equal for a single-line anchor), its first character's
line and column, and whether it's in_table.
Page numbers are only meaningful in print layout, so the document is
repaginated first — content-neutral, so the user's selection, scroll, and
view are left untouched (the same guarantee snapshot gives). Non-mutating.
$ wordlive locate --anchor-id heading:8
{"anchor_id": "heading:8", "page": 2, "end_page": 2, "line": 1, "column": 1, "in_table": false}
Scan paragraphs and watch page step up to find "which paragraph starts page
2"; compare a table/section anchor's page and end_page for its page span.
Failures: 2 anchor not found, 3 Word busy, 4 Word not running.
stats¶
A one-call document summary — the "what am I looking at before I act" read.
Returns pages, words, characters, paragraphs, and lines (Word's own
ComputeStatistics), plus the structural counts sections, headings,
tables, images, comments, revisions (from wordlive's discovery
collections, so they agree with table list / images / outline), and
saved. Page/line counts are print-layout truth, so the document is
repaginated first (selection/scroll/view untouched). Non-mutating.
$ wordlive stats
{"pages": 2, "words": 312, "characters": 1840, "paragraphs": 24, "lines": 48,
"sections": 1, "headings": 5, "tables": 1, "images": 0, "comments": 1,
"revisions": 0, "saved": false}
Composes with locate: stats.pages + an anchor's locate page answers "why
is this 2 pages" structurally, no image pass.
Failures: 3 Word busy, 4 Word not running.
proofing¶
Run Word's proofing tools over the document. Returns spelling and grammar,
each {count, errors} — the exact error count plus a (capped) list of flagged
runs as {text, anchor_id, para}, so a range:START-END can be fed back into
read / comment add — and readability, Word's readability statistics
(flesch_reading_ease, flesch_kincaid_grade_level, passive_sentences,
words_per_sentence, …). Heavier than stats (it asks Word to (re)check the
document) but still a pure read; if proofing is disabled or the document is
protected, the affected section reports a null count / empty readability.
$ wordlive proofing
{"spelling": {"count": 1, "errors": [{"text": "teh", "anchor_id": "range:14-17", "para": "para:2"}]},
"grammar": {"count": 0, "errors": []},
"readability": {"flesch_reading_ease": 65.5, "flesch_kincaid_grade_level": 7.2}}
Failures: 3 Word busy, 4 Word not running.
Reading content¶
Pull text out of the document — by anchor, by section, or whole-document serialisations.
read bookmark NAME / read bookmark --list¶
wordlive read bookmark NAME [--doc DOC_NAME]
wordlive read bookmark --list [--include-hidden] [--doc DOC_NAME]
Read the text of a bookmark, or with --list emit every bookmark name in
document order (--include-hidden also returns Word's internal bookmarks —
_Toc…, _Ref…).
$ wordlive read bookmark Address
{"text": "123 Main St"}
$ wordlive read bookmark --list
["Address", "Intro"]
Failures: 2 if the named bookmark doesn't exist. Pass exactly one of NAME
or --list.
read cc NAME¶
Read the text of a content control. NAME matches the control's Title
first, then Tag.
Failures: 2 if no content control with that Title/Tag exists.
read section HEADING¶
wordlive read section HEADING [--doc DOC_NAME]
wordlive read section --anchor-id heading:N [--doc DOC_NAME]
Read the body text under a heading — from the end of the heading paragraph up to the next heading whose level is less than or equal to this one's (or to the end of the document if there's no such boundary).
$ wordlive read section "Introduction"
{"heading": "Introduction",
"anchor_id": "heading:1",
"level": 1,
"text": "This document covers the Q2 risk register …"}
$ wordlive --text read section "Introduction"
This document covers the Q2 risk register …
--text mode emits only the section body — handy for piping into an LLM
prompt without ceremony. Use --anchor-id heading:N to disambiguate when the
same visible heading text appears more than once.
Failures: 2 heading not found.
read markdown / read html¶
wordlive read markdown [--within ANCHOR] [--doc DOC_NAME]
wordlive read html [--within ANCHOR] [--doc DOC_NAME]
Serialise the whole document — or one anchor's range — to clean Markdown
(or an HTML fragment). The read mirror of insert-markdown: headings,
bullet / numbered lists (nested), **bold** / *italic* / `code`
(monospace runs), GFM pipe tables,
inline images as , and hyperlinks as [text](url). Export is
lossy by design (underline, colours, and merged table cells don't survive),
so it round-trips the constrained subset import speaks and reads the rest richer.
--within scopes to an anchor's literal range — a range:START-END (e.g.
from find), or any anchor id. A heading:N covers only the heading line, not
its section body (use read between or a range: for "the section under X").
$ wordlive read markdown --within heading:3
{"markdown": "### Pricing\n\nOur tiers are **flexible** …"}
$ wordlive --text read markdown > document.md # pipe the raw Markdown out
--text emits the raw Markdown / HTML (no JSON envelope) — handy for piping
into a file or an LLM prompt. Failures: 2 anchor not found.
read digest [--budget N] [--depth D]¶
A token-budgeted, structure-aware read of the whole document — load a large
document into context cheaply while every anchor stays addressable. Headings
are verbatim (each tagged with its <!-- heading:N --> anchor — the navigation
spine), tables become one-line shape stubs (> table:N — R rows × C cols: …),
and body text is sampled to fit --budget (an approximate token count,
~4 chars/token), weighted so shallower sections keep more than deep ones.
Overflow is elided to markers that still name the para: range, so you can drill
into any region with read markdown --within …. --depth D caps how deep a
section keeps body (deeper sections collapse to a marker).
$ wordlive --text read digest --budget 4000
# Q3 Report <!-- heading:1 -->
The quarter closed ahead of plan …
…(para:5–para:18, 1240 words elided)…
> table:2 — 9 rows × 4 cols: Quarter, Region, Revenue, Growth
…
A pure read. For the full text of any region, use read markdown --within.
read between --start ID --end ID [--inclusive]¶
Read the content between two anchors — the headline use is two heading:N
ids (the block between two headings), but any anchors work. By default the span
is strictly between them (e.g. the body between two headings, excluding both
heading lines); --inclusive covers both bounding paragraphs. Returns the
spanning range:START-END id plus its text.
$ wordlive read between --start heading:1 --end heading:3
{"start": "heading:1", "end": "heading:3", "inclusive": false,
"anchor_id": "range:13-352", "text": "Body of the first section …"}
The returned offsets are live — use them before further edits shift the
document. Failures: 1 if end begins before start; 2 anchor not found.
read nearest-heading --anchor-id ID [--direction before|after]¶
Find the heading nearest to a position. --direction before (default) returns
the enclosing / preceding heading — the section the anchor sits in; after
returns the next heading past it. Emits the heading row ({level, text,
anchor_id}) under heading, or null when there is none in that direction.
$ wordlive read nearest-heading --anchor-id para:42 --direction before
{"anchor_id": "para:42", "direction": "before",
"heading": {"level": 2, "text": "Risks", "anchor_id": "heading:17"}}
Failures: 1 bad --direction; 2 anchor not found.
read format --anchor-id ID¶
Read the effective formatting at an anchor — the read mirror of
format-paragraph / format-run. Returns {anchor_id, style, paragraph,
font}, where style is the applied paragraph style's name and paragraph /
font each map a field name to {value, style, override}: value is the
effective value, style is the value the applied style contributes as its
baseline, and override is true when value differs from style (a direct
override Word would show with the style cleared). font also carries a mixed
key — the font fields that read wdUndefined because they vary across the
range's runs (their value is null and they're never flagged as overrides).
Lengths are in points (floats); color is #RRGGBB or "auto";
alignment is left/center/right/justify; line_spacing is
single/1.5/double, "1.15" (a multiple), "14pt" (exactly), or
"at_least:14pt". Paragraph fields: alignment, left_indent, right_indent,
first_line_indent, space_before, space_after, line_spacing,
page_break_before, keep_together, keep_with_next, widow_control. Font
fields: name, size, bold, italic, underline, strikethrough, color,
subscript, superscript, small_caps, all_caps, spacing, hidden, and
highlight (a keyword or "none"; effective-only, style is null).
Non-mutating.
$ wordlive read format --anchor-id heading:3
{"anchor_id": "heading:3",
"style": "Heading 2",
"paragraph": {"alignment": {"value": "center", "style": "left", "override": true},
"space_before": {"value": 6.0, "style": 12.0, "override": true}},
"font": {"name": {"value": "Calibri Light", "style": "Calibri Light", "override": false},
"bold": {"value": true, "style": true, "override": false},
"size": {"value": null, "style": 13.0, "override": false}},
"mixed": ["size"]}
Diff override: true to see what a paragraph carries beyond its style — the
input for regularize, which writes those back to the style's own value.
Failures: 2 anchor not found, 3 Word busy, 4 Word not running.
read watermark¶
Read the text watermark behind the pages — the read mirror of watermark set /
watermark remove. Emits {text, sections} (the watermark's text and the 1-based
section indices carrying it), or null when the document has no text watermark.
Only text watermarks (the Design → Watermark kind) are reported. Non-mutating.
Failures: 3 Word busy, 4 Word not running.
read text --anchor-id ID [--view raw|final|original|segments]¶
Read an anchor's text, optionally resolving tracked changes. raw (default) is
Range.Text as Word reports it — already the final view (inserted runs
present, deleted runs gone). final reconstructs the accept-all view explicitly;
original the reject-all view (the deleted wording, which lives only on the
delete revisions, spliced back in); segments returns the ordered
{text, change} breakdown (change ∈ insert / delete / null).
$ wordlive read text --anchor-id para:5 --view original
{"anchor_id": "para:5", "view": "original", "text": "the quick brown fox"}
Failures: 2 anchor not found, 3 Word busy, 4 Word not running.
Finding & navigating¶
Locate text, then move the cursor or the view.
cursor read / cursor write --text "…"¶
wordlive cursor read [--doc DOC_NAME]
wordlive cursor write --text "..." [--replace | --no-replace] [--doc DOC_NAME]
The explicit cursor surface. Every other command targets a semantic anchor
and preserves the user's cursor; cursor is the deliberate exception, for when
the user genuinely wants to read or write at their current position. It is not
addressable by --anchor-id — that separation is intentional, signalling it's
the non-preferred mode.
cursor read reports the selection's start/end, whether it's collapsed
(an insertion point with no selected text), the selected text, and the
containing para:N so you can pivot back to anchored edits:
$ wordlive cursor read
{"start": 142, "end": 142, "collapsed": true, "text": "", "paragraph": {"anchor_id": "para:7"}}
cursor write types at the cursor and — unlike anchor writes — deliberately
leaves the cursor after the inserted text. With a spanning selection, the
default --replace overwrites it (like typing); --no-replace inserts at the
selection start without removing it.
Failures: 3 Word busy, 4 Word not running.
find --text "…"¶
Locate every occurrence of the given text in the document (read-only). The
default --mode fuzzy is forgiving of cosmetic differences that show up when
LLMs re-emit text — whitespace runs collapse, smart quotes/dashes fold to ASCII,
NBSPs become spaces, and the strings are NFKC-normalized before comparison.
--mode literal matches exactly (no folding); --mode regex treats --text as
a Python regular expression.
$ wordlive find --text "the risk register"
[{"anchor_id": "range:412-429",
"start": 412,
"end": 429,
"text": "the risk register"}]
Use --in ANCHOR_ID to restrict the search. Headings expand to their
section (the body under the heading), which is the common case for
"replace this phrase, but only inside the Risks section":
text in each match is the actual original substring (with smart quotes,
NBSP, etc.) — that's the form Word will preserve when you replace it.
Failures: returns [] with exit 0 for no matches; 2 if --in ANCHOR_ID
refers to a missing anchor.
find-paragraph --text "…" [--limit N] [--min-score F]¶
Fuzzy paragraph search — the typo-/paraphrase-tolerant counterpart to
find. Where find does an exact substring match (on normalized text) and
returns range:START-END hits, find-paragraph scores every paragraph
against the query with a similarity ratio (difflib.SequenceMatcher, over the
same NFKC + smart-quote/dash/whitespace normalization) and returns the best
para:N candidates. Use it when you have approximately-remembered text and want
the paragraph it belongs to.
$ wordlive find-paragraph --text "the quick brown fox jumped over the dog"
[{"anchor_id": "para:12", "index": 12, "score": 0.9438,
"text": "The quick brown fox jumps over the lazy dog.",
"level": 10, "is_heading": false}]
Rows are sorted by descending score; only those with score >= --min-score
(default 0.6) are kept, capped at --limit (default 5). Headings are
included (flagged by is_heading / level), addressed by para:N. An empty or
whitespace-only query returns [].
Failures: returns [] with exit 0 for no matches above the threshold; 1 for
--limit < 1 or --min-score outside [0, 1].
go-to --anchor-id ID¶
Move the user's cursor to an anchor. This is the one CLI command that deliberately disturbs the user's selection — every other write preserves it.
$ wordlive go-to --anchor-id bookmark:Address
{"ok": true,
"anchor_id": "bookmark:Address",
"anchor": {"kind": "bookmark", "name": "Address"}}
--no-scroll collapses the selection at the anchor without scrolling the
view to it. Failures: 2 anchor not found, 3 Word busy.
Editing content¶
Insert, replace, and delete paragraphs and blocks.
insert --anchor-id ID (--text "…" | --runs JSON)¶
wordlive insert --anchor-id ID (--text "..." | --runs JSON) [--before | --after] [--style "Body Text"] [--doc DOC_NAME]
Insert a new paragraph relative to any anchor — addressed the same way
every other command addresses things, with --anchor-id (heading:N,
para:N, bookmark:NAME, a cell, a range). --after (the default) lands the
new paragraph just below the anchor; --before lands it just above. --after
works even when the anchor is the document's last paragraph — the new paragraph
is appended before the final mark — so you can build a document top-down from a
single empty paragraph.
Give exactly one of --text (a literal string — no markup) or --runs (a JSON
array of inline-formatted spans, or - to read it from stdin). Each run is
{text, bold?, italic?, underline?, code?, style?}, so a bold lead-in is one op:
$ wordlive insert --anchor-id heading:8 --text "New risk identified."
{"ok": true, "anchor_id": "heading:8", "where": "after", "style": null}
$ wordlive insert --anchor-id para:3 --text "Section preamble." --before
{"ok": true, "anchor_id": "para:3", "where": "before", "style": null}
$ wordlive insert --anchor-id end --runs '[{"text":"Bold lead","bold":true},{"text":" — rest"}]'
{"ok": true, "anchor_id": "end", "where": "after", "style": null}
--style is optional; if given it must be a Word style name that exists in
the document — the style is validated before the paragraph is inserted, so a
typo never partially mutates the document. Use wordlive style list to see
the available names. Failures: 2 anchor not found or style not found, 3
Word busy.
To insert text inside a paragraph at a precise offset rather than as a new
paragraph, target a collapsed range instead — replace --anchor-id
range:120-120 --text "…" — using offsets from paragraphs or find. To insert
several styled paragraphs at once, use insert-block.
insert-block --anchor-id ID --items JSON¶
Insert a contiguous run of styled paragraphs at an anchor in one op, in
natural reading order — the multi-paragraph counterpart to insert. Use it to
drop a whole styled section (a feature list, a heading plus its body) without a
reverse-ordered storm of insert calls dodging positional-anchor renumbering.
--items is a JSON array (or - for stdin). Each item is one paragraph, given
as either a plain string or an object {text | runs, style?}:
textcarries a tiny inline markdown:**bold**,*italic*,***both***,`code`(a monospace run). Escape a literal delimiter with a backslash —\*,\`— for any character the Markdown export escapes.runsis the structured form —[{text, bold?, italic?, underline?, code?, style?}]— for unambiguous control or a per-run character style.stylenames the paragraph style for that item.
It reports the spanning range:START-END of the inserted block, so you can act
on the whole run next — e.g. bullet the section you just inserted:
$ wordlive insert-block --anchor-id heading:1 --items \
'[{"text":"**Politeness** first.","style":"List Bullet"},
{"runs":[{"text":"Atomic undo","bold":true},{"text":" — one Ctrl-Z."}],"style":"List Bullet"},
"Plain third bullet."]'
{"ok": true, "anchor_id": "range:412-470", "paragraphs": 3, "where": "after"}
$ wordlive list apply --anchor-id range:412-470 --type bulleted
Styles are validated before anything is inserted, so a bad name fails the whole
block cleanly. Failures: 2 anchor not found or style not found, 3 Word busy.
insert-section --anchor-id ID --heading TEXT --body JSON¶
wordlive insert-section --anchor-id ID --heading TEXT --body JSON [--level N] [--before | --after] [--doc DOC_NAME]
The opinionated common case over insert-block: a Heading {level} paragraph
followed by its body, in reading order and one op. --heading carries the same
inline markdown an item's text does; --body is the same JSON items shape
insert-block takes (or - for stdin). --level is 1–9 (default 1). Reports
the section's spanning range:START-END.
$ wordlive insert-section --anchor-id end --heading "Results" --level 2 --body \
'["We observed a **20%** lift.", {"text":"Caveats apply.","style":"Body Text"}]'
{"ok": true, "anchor_id": "range:512-560", "where": "after"}
insert-markdown --anchor-id ID --markdown TEXT¶
Drop a chunk of constrained Markdown as real Word structure. The dialect is a documented subset, not CommonMark:
#/##/###→Heading 1/Heading 2/Heading 3.-/*→ a bulleted list;1.→ a numbered list (each list is numbered 1..N over its own span).- a blank line separates paragraphs; consecutive plain lines join into one
Normalparagraph. - inline
**bold**/*italic*/***both***/`code`are honoured. A code span becomes a monospace run;read markdownemits it back as backticks.
Out of scope in v1: code fences (the block form — inline `code` is
supported), nested lists, block quotes, tables — anything unrecognised becomes
literal paragraph text. Multi-line input is easiest from stdin with
--markdown -. Reports the spanning range:START-END.
Starting a document from blank? Lead with insert-markdown (or insert-block /
insert-section): they reuse the blank document's lone empty paragraph. append
promises a new final paragraph, so it strands that empty one above your text.
$ printf '# Plan\n\nKick-off notes.\n\n- scope it\n- staff it\n' | \
wordlive insert-markdown --anchor-id end --markdown -
{"ok": true, "anchor_id": "range:600-640", "where": "after"}
replace-section --anchor-id heading:N (--body JSON | --markdown TEXT)¶
Rewrite a heading's body — everything from after the heading up to the next
same-or-higher heading — while keeping the heading paragraph. The "rewrite
section X" workflow. Give exactly one of --body (the insert-block items shape)
or --markdown (the constrained subset above); either accepts - for stdin.
Reports the new body's spanning range:START-END. Needs a heading:N anchor
(exit 1 otherwise).
$ wordlive replace-section --anchor-id heading:3 --markdown "Updated findings.
- point one
- point two"
{"ok": true, "anchor_id": "range:300-340"}
delete-paragraph --anchor-id ID¶
Delete the paragraph(s) at an anchor — text and the trailing mark — so the
surrounding text closes up with no empty line left behind (unlike
replace --text "", which empties the paragraph but keeps it). Handy for a
stray leading empty para:1. Deleting the document's last paragraph clears it
but keeps Word's mandatory final mark.
Failures: 2 anchor not found, 3 Word busy.
insert-break --anchor-id ID [--kind …] [--before | --after]¶
wordlive insert-break --anchor-id ID
[--kind page|column|section_next|section_continuous]
[--before | --after] [--doc DOC_NAME]
Insert an explicit page, column, or section break at any anchor — the clean,
discoverable replacement for appending a paragraph whose text is a literal
form-feed. --kind defaults to page (the common case); column breaks a
multi-column layout, and the two section_* kinds start a new document section
(which can carry its own headers/footers and page setup — see section).
--after (default) drops the break just past the anchor; --before, just
before it.
$ wordlive insert-break --anchor-id para:12
{"ok": true, "anchor_id": "para:12", "kind": "page", "where": "after"}
$ wordlive insert-break --anchor-id heading:3 --kind section_next --before
{"ok": true, "anchor_id": "heading:3", "kind": "section_next", "where": "before"}
To make a style (e.g. every Heading 1) open a new page without a stray
break character that drifts on reflow, prefer format-paragraph --anchor-id ID
--page-break-before instead — it's a paragraph property, not an inserted mark.
Failures: 1 unknown --kind (usage error), 2 anchor not found, 3 Word
busy.
prepend --text "…" / append --text "…"¶
wordlive prepend --text "..." [--paragraph | --inline] [--style "Body Text"] [--doc DOC_NAME]
wordlive append --text "..." [--paragraph | --inline] [--style "Body Text"] [--doc DOC_NAME]
prepend is the mirror of append: it adds to the very start of the
document (a new first paragraph by default, or --inline to join the opening
paragraph) — equivalent to insert --anchor-id start --text "…". Everything
below applies to both; just swap "end" for "start".
$ wordlive prepend --text "DRAFT — not for distribution"
{"ok": true, "mode": "paragraph", "style": null}
Append text to the very end of the document — the high-level "end of doc"
helper, no anchor needed. --paragraph (the default) makes text a new final
paragraph; --inline continues the document's last paragraph instead. This is
exactly insert --anchor-id end --text "…" (the end anchor names the
position past the last paragraph), spelled as its own verb.
$ wordlive append --text "Closing note added by automation."
{"ok": true, "mode": "paragraph", "style": null}
$ wordlive append --text " (verified)" --inline
{"ok": true, "mode": "inline", "style": null}
--style is optional, paragraph-mode only, and must name a style that exists
in the document — it's validated before anything is written, so a typo never
partially mutates the document (wordlive style list shows the names).
Failures: 2 style not found, 3 Word busy.
replace¶
wordlive replace --anchor-id ID --text "..." [--doc DOC_NAME] # anchor mode
wordlive replace --find OLD --text NEW [--in ID] [--all|--occurrence N] [--mode M] [--doc DOC_NAME] # find mode
Two modes share the verb:
Anchor mode — replace an entire range¶
Replace the text at an anchor identified by anchor ID. Works across all three anchor kinds.
$ wordlive replace --anchor-id heading:3 --text "Updated section text"
{"ok": true,
"anchor_id": "heading:3",
"anchor": {"kind": "heading", "name": "Context"}}
The response's anchor.name resolves the ID back to a human-readable name.
Failures: 2 anchor not found, 3 Word busy.
Fuzzy mode — find + replace a substring¶
Locate --find OLD (same fuzzy match as find) and replace with --text NEW.
Word's native range replacement preserves the character formatting of the
matched range — bold stays bold, italics stay italic — so you don't need to
re-state formatting on the replacement.
$ wordlive replace --find "Q1 2025" --text "Q2 2025"
{"ok": true,
"replacements": [{"anchor_id": "range:412-419",
"start": 412, "end": 419, "text": "Q1 2025"}]}
| Flag | Meaning |
|---|---|
--in ID |
Restrict search to the given anchor's range (headings expand to their section). |
--all |
Replace every match. Mutually exclusive with --occurrence. |
--occurrence N |
Replace only the Nth match (1-based). Mutually exclusive with --all. |
--mode M |
Matcher: fuzzy (default; Unicode/whitespace-tolerant) · literal (exact) · regex (Python; --text may use \1 backreferences, expanded per match). |
Failures:
- Exit
2— zero matches. Same code as anchor-not-found because the agent's recovery is identical: re-fetch state and retry. - Exit
5— multiple matches and neither--allnor--occurrencewas given. Stdout still emits a JSON payload listing all matches so the agent can pick an occurrence and retry:
{"ok": false, "error": "ambiguous_match", "find": "Q1",
"matches": [{"start": 412, "end": 414, "text": "Q1"},
{"start": 887, "end": 889, "text": "Q1"}]}
- Exit
3— Word busy.
Formatting & styles¶
Apply paragraph/character formatting and manage styles.
style list¶
Enumerate every style defined in the document.
$ wordlive style list
[{"name": "Normal", "type": "paragraph", "builtin": true, "in_use": true},
{"name": "Body Text", "type": "paragraph", "builtin": true, "in_use": true},
{"name": "Heading 1", "type": "paragraph", "builtin": true, "in_use": true}]
type is one of "paragraph", "character", "table", "list". Built-in
Word styles set builtin: true; user-defined styles set false. Failures:
3 Word busy, 4 Word not running.
style apply --anchor-id ID --name NAME¶
Apply a style to the anchor's range. Atomic-undo. The style must already
exist in the document — define one first with style add
if needed.
$ wordlive style apply --anchor-id heading:3 --name "Heading 2"
{"ok": true,
"anchor_id": "heading:3",
"anchor": {"kind": "heading", "name": "Risks"},
"style": "Heading 2"}
Word picks paragraph- vs. character-style behaviour from the style's own
Type; you don't need to model that distinction. Failures: 2 anchor or
style not found, 3 Word busy.
format-paragraph --anchor-id ID [...]¶
wordlive format-paragraph --anchor-id ID
[--alignment left|center|centre|right|justify]
[--left-indent POINTS] [--right-indent POINTS] [--first-line-indent POINTS]
[--space-before POINTS] [--space-after POINTS]
[--line-spacing MULTIPLE|single|1.5|double|LENGTH]
[--page-break-before | --no-page-break-before]
[--keep-together | --no-keep-together]
[--keep-with-next | --no-keep-with-next]
[--widow-control | --no-widow-control]
[--doc DOC_NAME]
centre is accepted as a synonym for center (UK spelling).
Set paragraph-formatting properties on the anchor's range. At least one
formatting flag is required. Indent and spacing values are in points —
the unit Word's COM API uses natively for these fields. --line-spacing sets
the leading within the paragraph (distinct from --space-before/--space-after,
which space paragraphs apart): a multiple of single spacing (1, 1.5,
2), the keywords single/1.5/double, or an exact length (14pt,
1.5cm) for a fixed line height.
--page-break-before forces the paragraph to begin on a new page (and
--no-page-break-before clears it) — the clean, reflow-safe way to
page-break, leaving no stray break character (contrast insert-break, which
inserts an explicit one-off break). The remaining flags are Word's
pagination controls for clean multi-page layout: --keep-together keeps all
lines of a paragraph on one page, --keep-with-next keeps it with the following
paragraph (e.g. a heading with its first body line), and --widow-control
prevents a lone first/last line stranded at a page boundary. Atomic-undo.
$ wordlive format-paragraph --anchor-id heading:3 \
--alignment center --space-before 6
{"ok": true,
"anchor_id": "heading:3",
"anchor": {"kind": "heading", "name": "Risks"},
"applied": {"alignment": "center", "space_before": 6.0}}
Only the flags you pass are written; everything else on the paragraph is
left alone. If the anchor spans a partial paragraph (e.g., a bookmark
covering five words inside a longer paragraph), Word applies the formatting
to the enclosing paragraph — that's the COM behaviour, not a wordlive
quirk. Failures: 2 anchor not found, 3 Word busy.
format-run --anchor-id ID [...]¶
wordlive format-run --anchor-id ID
[--bold | --no-bold] [--italic | --no-italic]
[--underline | --no-underline] [--strikethrough | --no-strikethrough]
[--font NAME] [--size POINTS|UNIT] [--color NAME|HEX|R,G,B]
[--highlight NAME] [--subscript | --no-subscript]
[--superscript | --no-superscript] [--small-caps | --no-small-caps]
[--all-caps | --no-all-caps] [--spacing POINTS|UNIT]
[--doc DOC_NAME]
Set character-formatting (run-level) properties on the anchor's range —
the bold this phrase layer, distinct from style apply (named styles) and
format-paragraph (paragraph scope). Pairs naturally with a range:START-END
anchor to style a sub-paragraph span. At least one flag is required; only the
flags you pass are written. Atomic-undo.
--color takes a colour name (red, navy, …), a hex string (#FF0000 or
FF0000), or comma-separated R,G,B. --highlight is a named text-highlight
palette colour (yellow, green, …, or none to clear). --size/--spacing
accept a bare number (points) or a unit string (12pt, 1.5mm).
$ wordlive format-run --anchor-id range:120-145 --bold --color FF0000 --highlight yellow
{"ok": true,
"anchor_id": "range:120-145",
"anchor": {"kind": "range", "name": "range:120-145"},
"applied": {"bold": true, "color": "FF0000", "highlight": "yellow"}}
Failures: 2 anchor not found, 1 bad colour/size/highlight value, 3 Word busy.
shading --anchor-id ID --fill COLOR¶
Set the background-fill shading of the anchor's range. Because a table cell is
an anchor (table:N:R:C), this is also how you shade a cell. Atomic-undo.
borders --anchor-id ID [...]¶
wordlive borders --anchor-id ID
[--sides all|box|top|bottom|left|right|horizontal|vertical]
[--style single|double|dot|dash|dash-dot|none]
[--weight POINTS] [--color NAME|HEX|R,G,B] [--doc DOC_NAME]
Draw borders on the anchor's range or cell. --sides is comma-separated for
several edges (default all = the four outer edges). --weight is in points,
snapped to Word's discrete line-width set (0.25/0.5/0.75/1/1.5/2.25/3 pt).
Page-wide and table-wide borders are out of scope (this sets per-range/per-cell
borders). Atomic-undo.
drop-cap --anchor-id ID [...]¶
wordlive drop-cap --anchor-id ID
[--position dropped|margin|none] [--lines N]
[--distance POINTS|UNIT] [--font NAME] [--doc DOC_NAME]
Turn the first letter of the anchor's paragraph into a drop cap — the
editorial oversized initial, a real Word DropCap so the body text wraps around
it natively (not a faked big-font run). --position is dropped (default — the
letter sits into the text), margin (it hangs in the left margin), or none
(remove an existing drop cap; the other flags are then ignored). --lines is
how many lines tall the letter is (default 3), --distance the gap from the body
text, and --font an optional font for the dropped letter. Word rejects a drop
cap on an empty paragraph (no letter to drop) — that surfaces as a 3/COM
error. Atomic-undo. Failures: 2 anchor not found, 1 bad value.
tab-stop --anchor-id ID --position POS [...]¶
wordlive tab-stop --anchor-id ID --position POINTS|UNIT
[--align left|center|right|decimal|bar]
[--leader none|dots|dashes|lines|heavy|middle-dot] [--doc DOC_NAME]
Add a tab stop to the anchor's paragraph(s) — --leader dots gives the dotted
fill of a price list or table-of-contents row without a table. --position
accepts points or a unit string (3in). Atomic-undo.
style add NAME [...]¶
wordlive style add NAME
[--type paragraph|character|table|list]
[--based-on NAME] [--next-style NAME] [--doc DOC_NAME]
Define a new style. --based-on is the inheritance parent; --next-style is
the style applied to the paragraph after one in this style. Style its defaults
with style set, then style apply it. Atomic-undo. Failures: 1 bad
--type, 2 unknown --based-on/--next-style.
style set NAME [...]¶
wordlive style set NAME
[--bold|--no-bold] [--italic|--no-italic] [--underline|--no-underline]
[--font NAME] [--size POINTS|UNIT] [--color NAME|HEX|R,G,B]
[--alignment left|center|right|justify]
[--space-before POINTS|UNIT] [--space-after POINTS|UNIT]
[--line-spacing MULTIPLE|single|1.5|double|LENGTH]
[--based-on NAME] [--next-style NAME] [--doc DOC_NAME]
Set the font / paragraph defaults of an existing style (built-in or one you
created). At least one property is required. The brand/template workflow:
style add once, style set its look, then style apply it everywhere.
Atomic-undo. Failures: 2 style not found, 1 bad value, 3 Word busy.
Lists & numbering¶
Apply, inspect, and author list formatting.
list show¶
Enumerate every bullet/numbered list in the document, top to bottom. Each row
carries a range:START-END anchor_id covering the whole list, so you can
feed it straight into list restart, replace, or comment add.
type is bulleted / numbered / outline / number-only / mixed.
Failures: 3 Word busy, 4 Word not running.
list apply --anchor-id ID --type …¶
Turn the anchor's paragraphs into a list. --type defaults to bulleted.
Numbering starts fresh at 1 unless --continue is given, which continues from
a list immediately above. Atomic-undo.
$ wordlive list apply --anchor-id heading:6 --type numbered
{"ok": true, "anchor_id": "heading:6",
"anchor": {"kind": "heading", "name": "Steps"},
"type": "numbered", "continue_previous": false}
Failures: 2 anchor not found, 3 Word busy.
list info --anchor-id ID¶
Report the list state at an anchor (read-only): {type, level, number, string},
where string is the rendered marker ("1.", "a)", "•"). type is
"none" when the anchor isn't in a list.
$ wordlive list info --anchor-id range:512-540
{"type": "numbered", "level": 1, "number": 3, "string": "3."}
Failures: 2 anchor not found, 3 Word busy.
list format --anchor-id ID --levels JSON¶
Author a custom multi-level list and apply it — the richer counterpart of
list apply (which only applies a gallery default). --levels is a JSON array
of per-level specs (1-based); each object's keys are all optional except a bullet
level's glyph:
kind—"number"(default) or"bullet".format— a number level's marker template ("%1.","%1)","%1.%2";%Nreferences level N), default"%{level}."; for a bullet, the glyph.style— a number scheme:arabic,upper-roman,lower-roman,upper-letter,lower-letter,ordinal, … .bullet/font— a bullet's glyph and marker font (defaultSymbol).start_at,number_position,text_position(points or"0.5in"),trailing(tab/space/none),alignment(left/center/right),bold,italic,color.
More than one level mints an outline template. Atomic-undo.
$ wordlive list format --anchor-id range:0-40 \
--levels '[{"kind":"number","format":"%1)","style":"lower-letter","trailing":"space"}]'
{"ok": true, "anchor_id": "range:0-40", "levels": 1}
Failures: 1 --levels not a JSON array / a bad spec, 2 anchor not found,
3 Word busy.
list levels --anchor-id ID¶
Read the per-level format of the list at an anchor (read-only) — the read mirror
of list format. Returns one {level, kind, format, number_style, style,
trailing, number_position, text_position, font} per level of the applied
template, or an empty list if the anchor isn't in a list.
$ wordlive list levels --anchor-id range:0-40
{"anchor_id": "range:0-40", "levels": [{"level": 1, "kind": "number", "format": "%1)", "style": "lower-letter", "trailing": "space", "number_position": 18.0, "text_position": 36.0, "font": ""}]}
Failures: 2 anchor not found, 3 Word busy.
list remove | restart | indent | outdent --anchor-id ID¶
wordlive list remove --anchor-id ID [--doc DOC_NAME] # strip list formatting
wordlive list restart --anchor-id ID [--doc DOC_NAME] # restart numbering at 1
wordlive list indent --anchor-id ID [--doc DOC_NAME] # demote one level (1 -> 2)
wordlive list outdent --anchor-id ID [--doc DOC_NAME] # promote one level (2 -> 1)
All four are atomic-undo. restart re-applies the list's own template starting
at 1; it errors if the anchor isn't part of a list.
$ wordlive list restart --anchor-id range:512-540
{"ok": true, "anchor_id": "range:512-540", "anchor": {"kind": "range", "name": "range:512-540"}}
Failures: 2 anchor not found, 3 Word busy.
Tables¶
Create, read, and restructure tables.
table list¶
Enumerate every table in the document, in top-to-bottom order.
$ wordlive table list
[{"index": 1, "title": "Budget", "rows": 4, "columns": 3},
{"index": 2, "title": "", "rows": 2, "columns": 2}]
index is the 1-based position used to address the table and its cells.
title is the table's Title (empty string if unset). Failures: 3 Word busy,
4 Word not running.
table read INDEX¶
Read table INDEX (1-based) as a grid. Each cell carries its anchor_id
(table:N:R:C) so you can feed it straight into replace, style apply, or
format-paragraph.
$ wordlive table read 1
{"index": 1, "title": "Budget", "rows": 2, "columns": 2,
"cells": [[{"row": 1, "col": 1, "text": "Item", "anchor_id": "table:1:1:1"},
{"row": 1, "col": 2, "text": "Cost", "anchor_id": "table:1:1:2"}],
[{"row": 2, "col": 1, "text": "Travel","anchor_id": "table:1:2:1"},
{"row": 2, "col": 2, "text": "$400", "anchor_id": "table:1:2:2"}]]}
Cell text is stripped of Word's internal end-of-cell markers. To write a
cell, use its anchor id with replace:
$ wordlive replace --anchor-id table:1:2:2 --text "$450"
{"ok": true, "anchor_id": "table:1:2:2", "anchor": {"kind": "cell", "name": "table:1:2:2"}}
Failures: 2 table index out of range, 3 Word busy.
table records INDEX¶
Read table INDEX as records — the read mirror of table create --data
from a list of objects. Row 1 is taken as the header; each row below becomes a
{header: cell_text} object. Non-mutating.
A duplicate header label collapses (rightmost column wins); a blank header cell
yields an empty-string key — both the caller's responsibility, matching the
write path. Failures: 2 table index out of range, 3 Word busy.
table create¶
wordlive table create --anchor-id ID [--rows R] [--cols C]
[--style NAME] [--header] [--before|--after]
[--data JSON | --data -] [--doc DOC_NAME]
Create a new table at a position anchor (heading:, para:, start,
end, range: — not a bare table:N, which addresses an existing table).
Every other verb edits existing structure; this is how you build a table from
nothing. Atomic-undo. Reports the new table's 1-based index for an immediate
follow-up set-cell / add-row.
--data populates the cells at creation and accepts two shapes:
- a row-major 2-D array (
[[r1c1, r1c2], …]); or - records — a list of objects (
[{"Tier":"Wobble","Monthly":"$9"}, …]), whose keys become a header row (each object is one body row); this implies--header.
When --data is given, --rows/--cols are optional — they're inferred
from the data's shape, so the common case is just --data …. Pass them
explicitly to pad the grid larger than the data (a short/partial array leaves
trailing cells empty; an array that overflows the grid is a clean error, exit
1). Without --data, both --rows and --cols are required. Pass --data -
to read the JSON from stdin, which sidesteps Windows quoting/backslash fights
(mirrors exec --ops -).
--style names a table style defined in the document; it defaults to the
built-in Table Grid so a new table has visible borders rather than only
faint gridlines. A style name not in the document fails (exit 2). --header
bolds the first row.
$ wordlive table create --anchor-id end --header \
--data '[["Tier","Monthly","SLA"],["Wobble","$9","best effort"],["Finch","$99","99.9%"]]'
{"ok": true, "table": 2, "rows": 3, "columns": 3}
$ wordlive table create --anchor-id end \
--data '[{"Tier":"Wobble","Monthly":"$9"},{"Tier":"Finch","Monthly":"$99"}]'
{"ok": true, "table": 3, "rows": 3, "columns": 2}
A table appended where another already sits flush against it (e.g. two tables in a row at the end of the document) is kept distinct: Word would otherwise merge adjacent tables, so a separating paragraph is inserted automatically.
Failures: 1 bad dimensions / --data shape, 2 anchor or style not found,
3 Word busy, 4 Word not running.
table add-row¶
Append a row at the end of the table. --values is an optional JSON array of
cell values, matched to columns left-to-right (extras ignored, short lists
leave trailing cells empty). Atomic-undo.
$ wordlive table add-row --table 1 --values '["Lodging", "$600"]'
{"ok": true, "table": 1, "rows": 3}
Failures: 2 table index out of range, 3 Word busy.
table append-record¶
wordlive table append-record --table INDEX --record '{"Item":"Lodging","Cost":"$600"}' [--doc DOC_NAME]
Append a row from a JSON object, mapping its keys to the header columns
(row 1). A header with no matching key gets an empty cell and an extra key is
ignored — the same lenient mapping table create --data (records) uses. The new
row inherits the table's existing formatting/banding. Atomic-undo. The header-
name companion to add-row (which is positional).
$ wordlive table append-record --table 1 --record '{"Item":"Lodging","Cost":"$600"}'
{"ok": true, "table": 1, "rows": 3}
Failures: 1 --record not a JSON object, 2 table index out of range, 3 Word busy.
table update-row¶
wordlive table update-row --table INDEX --key VALUE --values '{"Cost":"$450"}'
[--column HEADER] [--doc DOC_NAME]
Update the first row whose key-column cell equals --key, setting cells by
header name (--values is a {header: new_value} object). The key column is the
first column by default, or the header named by --column. First match wins on
duplicate keys. Atomic-undo — addresses a row by content instead of a fragile
1-based index.
$ wordlive table update-row --table 1 --key Travel --values '{"Cost":"$450"}'
{"ok": true, "table": 1, "key": "Travel"}
Validation happens before any edit: an unknown --column, or a --values key
that isn't a header, is a clean error. Failures: 1 --values not a JSON
object / unknown header or column, 2 table index out of range or no row matches
--key, 3 Word busy.
table delete-row¶
Delete the 1-based row R from the table. Atomic-undo.
Failures: 2 table index or row out of range, 3 Word busy.
table add-column¶
Append a column at the right edge of the table — the column mirror of add-row.
--values is an optional JSON array of cell values, matched to rows
top-to-bottom (extras ignored, short lists leave trailing cells empty).
Atomic-undo.
$ wordlive table add-column --table 1 --values '["Status", "ok", "ok"]'
{"ok": true, "table": 1, "columns": 3}
Failures: 1 --values not a JSON array, 2 table index out of range, 3 Word busy.
table delete-column¶
Delete the 1-based column C. Atomic-undo. A table with merged / mixed-width
cells has no per-column model in Word, so this fails with a clean error
pointing at per-cell deletion via table:N:R:C — delete those cells instead.
Failures: 1 merged / mixed-width table (no per-column model), 2 table index
or column out of range, 3 Word busy.
table merge-cells¶
Merge two cells (and the rectangle they span) into one. --from / --to are
1-based R:C coordinates. Word joins the cells' text; the merged cell keeps the
--from cell's id (table:N:R:C). The table becomes non-uniform afterwards
(table read reports "uniform": false) — table:N:R:C then indexes physical
cells, so re-read the table to see the new shape. Atomic-undo.
$ wordlive table merge-cells --table 1 --from 1:1 --to 1:2
{"ok": true, "table": 1, "anchor_id": "table:1:1:1"}
Failures: 1 malformed coordinate, 2 table index or cell out of range, 3 Word busy.
table split-cell¶
Split one cell into a --rows × --columns grid (default 1 × 2) — the
inverse of merge-cells. Makes the table non-uniform (see above). --cell
is a 1-based R:C coordinate. Atomic-undo.
$ wordlive table split-cell --table 1 --cell 2:1 --columns 3
{"ok": true, "table": 1, "anchor_id": "table:1:2:1"}
Failures: 1 malformed coordinate / count below 1, 2 table index or cell out
of range, 3 Word busy.
table set-heading-row¶
wordlive table set-heading-row --table INDEX [--row R]
[--heading | --no-heading] [--allow-break | --no-allow-break] [--doc DOC_NAME]
Mark a 1-based row (default 1) as a repeating heading row — it repeats at
the top of every page a multi-page table spans (Row.HeadingFormat). --row
selects which row; --no-heading clears the flag. --allow-break controls
whether the row may split across a page (Row.AllowBreakAcrossPages), defaulting
to off for a heading row so the header stays intact. Atomic-undo.
Failures: 2 table index or row out of range, 3 Word busy.
table delete INDEX¶
Delete table INDEX (1-based) and all its cells — the structural mirror of
table create / delete-row. Atomic-undo. The indices of any tables below it
shift down by one afterwards.
Failures: 2 table index out of range, 3 Word busy.
table autofit¶
Resize a table's columns. --mode content (the default) shrinks/grows each
column to fit its cell contents, window stretches the table to the page width,
and fixed pins the current widths so Word stops auto-sizing. Atomic-undo —
a clean way to tidy a table whose columns drifted after edits.
Failures: 2 table index out of range, 3 Word busy.
table set-style¶
Restyle an existing table — the post-creation counterpart of table create
--style. --style is any built-in or custom table style (style list filtered
to type==table discovers them). Applying a style reapplies its conditional
formatting and overwrites direct cell shading, so restyle first, then layer
cell-level overrides. Atomic-undo.
$ wordlive table set-style --table 1 --style "Grid Table 4 - Accent 1"
{"ok": true, "table": 1, "style": "Grid Table 4 - Accent 1"}
Failures: 2 table index out of range or unknown style, 3 Word busy.
table set-alignment¶
Align the whole table across the page width (distinct from the text alignment inside cells). Atomic-undo.
$ wordlive table set-alignment --table 1 --alignment center
{"ok": true, "table": 1, "alignment": "center"}
table set-borders¶
wordlive table set-borders --table INDEX [--sides all] [--style single] [--weight 0.5] [--color C] [--doc DOC_NAME]
Draw borders across the whole table grid in one call — the table-wide
counterpart of the per-cell borders verb. --sides is all/box, a single
outer edge, the interior gridlines horizontal/vertical, or a comma-separated
list (box,horizontal,vertical rules every line). --style is a line style
(single, double, dot, … or none); --weight snaps to Word's set
(0.25/0.5/0.75/1/1.5/2.25/3 pt); --color is a name/hex/r,g,b. Atomic-undo.
$ wordlive table set-borders --table 1 --sides box,horizontal,vertical --style single --weight 1
{"ok": true, "table": 1, "applied": {"sides": ["box", "horizontal", "vertical"], "style": "single", "weight": 1.0, "color": null}}
table set-banding¶
wordlive table set-banding --table INDEX [--first-row/--no-first-row] [--last-row/--no-last-row]
[--first-column/--no-first-column] [--last-column/--no-last-column]
[--banded-rows/--no-banded-rows] [--banded-columns/--no-banded-columns] [--doc DOC_NAME]
Toggle the "Table Style Options" (header row, total row, first/last column, banded
rows/columns) of the applied table style. Each flag is tri-state — pass it to
set, omit it to leave untouched. Only shows once a real table style is applied
(pair with table set-style). Atomic-undo.
$ wordlive table set-banding --table 1 --first-row --no-banded-rows
{"ok": true, "table": 1, "applied": {"first_row": true, "banded_rows": false}}
cell-valign¶
Set a table cell's vertical alignment. Atomic-undo. (For whole-row / whole-column
styling, address table:N:row:R / table:N:col:C and use the shading /
borders / apply-style / format-run verbs.)
$ wordlive cell-valign --anchor-id table:1:2:2 --align bottom
{"ok": true, "anchor_id": "table:1:2:2", "align": "bottom"}
Failures: 1 not a cell anchor or bad --align, 2 table/cell out of range.
Bookmarks, pins & links¶
Named anchors, durable pins, hyperlinks, cross-references, and captions.
write bookmark NAME (--text "…" | --create --anchor-id ID)¶
wordlive write bookmark NAME --text "..." [--doc DOC_NAME] # set existing
wordlive write bookmark NAME --create --anchor-id ID [--doc DOC_NAME] # create new
Two modes, both in a single atomic-undo scope:
--text "…"replaces an existing bookmark's text. The bookmark is preserved — wordlive re-adds it covering the new content after the WordRange.Textassignment (which would otherwise delete it).--create --anchor-id IDcreates a new bookmarkNAMEover an anchor's range (e.g.heading:2,range:120-140) — the prerequisite for internal links (link --bookmark NAME) and cross-references (cross-ref --target bookmark:NAME).NAMEmust start with a letter and contain only letters, digits, and underscores.
$ wordlive write bookmark Address --text "456 Elm St"
{"ok": true, "anchor": {"kind": "bookmark", "name": "Address"}}
$ wordlive write bookmark Intro --create --anchor-id heading:1
{"ok": true, "bookmark": "Intro", "anchor_id": "heading:1", "created": true}
(Creating a bookmark was previously bookmark add NAME --anchor-id ID, now a
hidden deprecated alias.)
Failures: 2 anchor not found, 3 Word busy.
pin ANCHOR_ID [--name SLUG]¶
Plant a durable handle on any anchor and print its pin:CODE id. A pin is a
Word-hidden bookmark (_wl_<code>) over the anchor's range; Word keeps it pinned
to the same content across the inserts/deletes that renumber positional para:N
/ heading:N ids, so it is the escape hatch when you need a stable address.
Resolve it later with --anchor-id pin:CODE. Omit --name for a random code, or
pass a readable slug (--name budget-intro, lowercase words joined by hyphens).
If the pinned content is later deleted the handle vanishes (the next resolve is
exit 2). One atomic-undo scope.
$ wordlive pin heading:4 --name methods
{"anchor_id": "pin:methods", "pin": "pin:methods", "target": "heading:4"}
Failures: 2 anchor not found, 3 Word busy.
pin-outline [--levels LO HI]¶
Pin every heading at once and print the {heading:N → pin:CODE} map — a durable
navigation scaffold to set up before a batch of structural edits. Idempotent: a
heading already carrying a handle reuses it (keyed by range start), so re-running
returns the same map. --levels LO HI restricts to an inclusive heading-level
band (e.g. --levels 1 2). One atomic-undo scope.
Failures: 3 Word busy.
bookmark add NAME --anchor-id ID (deprecated)¶
Hidden, deprecated alias for write bookmark NAME --create --anchor-id ID
(see above). Kept for one release; prefer the write bookmark --create form.
link --anchor-id ID (--url U | --bookmark B) [--text T] [--screen-tip S]¶
wordlive link --anchor-id ID (--url URL | --bookmark NAME) [--text "…"]
[--screen-tip "…"] [--doc DOC_NAME]
Turn an anchor into a hyperlink. Pass exactly one destination: --url for an
external link (URL / mailto: / file path) or --bookmark for an internal jump
to a bookmark in this document. With no --text the anchor's existing range
becomes the link; --text instead inserts new linked text at the end of the
range (so linking a heading or phrase keeps its content).
$ wordlive link --anchor-id range:120-140 --url "https://example.com"
{"ok": true, "anchor_id": "range:120-140",
"applied": {"url": "https://example.com", "bookmark": null, "text": null}}
Failures: 1 bad input (not exactly one of --url/--bookmark); 2 anchor not
found; 3 Word busy.
cross-ref --anchor-id ID --target TARGET [--kind …] [--no-hyperlink]¶
wordlive cross-ref --anchor-id ID --target TARGET
[--kind text|page|number|above_below] [--hyperlink | --no-hyperlink]
[--before | --after] [--doc DOC_NAME]
Insert a cross-reference to another anchor. --target is a bookmark:NAME,
heading:N, footnote:N, or endnote:N id. --kind selects what shows: the
referenced text (default), its page number, its number, or its
above_below position.
$ wordlive cross-ref --anchor-id end --target bookmark:Intro --kind page
{"ok": true, "anchor_id": "end",
"applied": {"target": "bookmark:Intro", "kind": "page", "hyperlink": true, "where": "after"}}
References go stale when the document shifts — run update-fields (or
snapshot) to refresh them. Failures: 1 bad input; 2 anchor / target not
found; 3 Word busy.
caption --anchor-id ID [--label Figure] [--text "…"]¶
wordlive caption --anchor-id ID [--label Figure] [--text "…"]
[--position above|below] [--doc DOC_NAME]
Insert an auto-numbered caption (Figure 1, Table 2, …) as its own paragraph
at an anchor. --label is a built-in (Figure/Table/Equation) or a custom
string; --text is the title after the label and number. Pairs with cross-ref
for "see Figure 2". The caption always becomes its own Caption-styled
paragraph (it never fuses into the target paragraph); on a table cell anchor it
captions the whole table. --position controls above/below — default is
above for a Table, below otherwise (the figure/table convention).
$ wordlive caption --anchor-id range:120-140 --label Figure --text "System overview"
{"ok": true, "anchor_id": "range:120-140",
"applied": {"label": "Figure", "text": "System overview", "position": null}}
Failures: 1 bad input; 2 anchor not found; 3 Word busy.
Content controls¶
Create and edit structured fill-in fields.
write cc NAME --text "…"¶
Replace a content control's text inside a single atomic-undo scope.
$ wordlive write cc Signatory --text "Jane Doe"
{"ok": true, "anchor": {"kind": "content_control", "name": "Signatory"}}
Failures: 2 anchor not found, 3 Word busy.
create-content-control --anchor-id ID [--kind K] [--title T] [--tag T] [--item ...]¶
wordlive create-content-control --anchor-id ID [--kind rich_text]
[--title T] [--tag T] [--item 'Text' --item 'Label=Value' ...]
[--where wrap|before|after] [--lock-contents] [--lock-control] [--doc DOC_NAME]
Create a content control — the structured-document fill-in field (the read/write
side is read cc / write cc). --where wrap (the default) surrounds the
anchor's existing range — e.g. a range:START-END from find — while before /
after insert a fresh empty control. --kind is one of rich_text (default),
text, picture, combo_box, dropdown, date, checkbox, building_block,
group, repeating_section. A --title (falling back to --tag) names the
control so it's addressable later as cc:TITLE. --item (repeatable, combo_box /
dropdown only) adds a choice — 'Text' or 'Label=Value'. --lock-contents
stops edits to the value; --lock-control stops deletion.
$ wordlive create-content-control --anchor-id range:120-140 --kind dropdown \
--title Status --item "Open" --item "Done=closed"
{"ok": true, "content_control": "Status", "cc_anchor_id": "cc:Status"}
Failures: 1 bad input; 2 anchor not found; 3 Word busy.
set-cc-properties --anchor-id cc:NAME [--title T] [--tag T] [--lock-contents] [--lock-control]¶
wordlive set-cc-properties --anchor-id cc:NAME [--title T] [--tag T]
[--lock-contents | --no-lock-contents] [--lock-control | --no-lock-control] [--doc DOC_NAME]
Re-set an existing content control's metadata in place — no delete + reinsert.
Pass at least one option; "" clears --title / --tag while omitting one
leaves it untouched. --lock-contents / --no-lock-contents toggle whether the
value is editable; --lock-control / --no-lock-control whether the control can
be deleted. A title (or tag, when untitled) rename changes the control's
cc:NAME anchor id.
$ wordlive set-cc-properties --anchor-id cc:Status --title Stage --lock-contents
{"ok": true, "anchor_id": "cc:Status", "applied": {"title": "Stage", "lock_contents": true}}
Failures: 1 bad input / wrong-kind anchor; 2 anchor not found; 3 Word busy.
set-cc-items --anchor-id cc:NAME --item ... [--item ...]¶
Replace a combo_box / dropdown control's choice list in place — the new
--item set replaces the existing entries (it does not append). Each --item
is 'Text' or 'Label=Value'. Only valid on a combo_box / dropdown control.
$ wordlive set-cc-items --anchor-id cc:Status --item "Open" --item "Done=closed"
{"ok": true, "anchor_id": "cc:Status", "applied": {"items": ["Open", {"text": "Done", "value": "closed"}]}}
Failures: 1 bad input / not a list control; 2 anchor not found; 3 Word busy.
Fields & references¶
Self-updating fields, notes, and the tables built from them (TOC, figures, index).
insert-field --anchor-id ID --kind KIND [--text CODE] [--before | --after]¶
wordlive insert-field --anchor-id ID
--kind page|numpages|date|time|filename|author|title|field
[--text "FIELD CODE"] [--before | --after] [--doc DOC_NAME]
Insert a self-updating Word field — a value Word keeps current rather than literal
text. page / numpages are the page number and total page count (combine for
"Page X of Y"); date / time stamp the current date/time; filename /
author / title pull document metadata. --kind field is the escape hatch:
pass the raw field code via --text (e.g. --text "REF myBookmark \h").
Page numbers belong in a footer or header — the anchor id footer:1:primary
works here like any other:
$ wordlive insert-field --anchor-id footer:1:primary --kind page
{"ok": true, "anchor_id": "footer:1:primary",
"anchor": {"kind": "footer", "name": "footer:1:primary"},
"applied": {"kind": "page", "text": null, "where": "after"}}
Newly inserted fields render once; run update-fields (or snapshot, which
repaginates) to refresh them after later edits. Failures: 1 --kind field
without --text, or bad input; 2 anchor not found; 3 Word busy.
update-fields¶
Recompute the document's fields (page numbers, cross-references, dates, a TOC) — the "make the numbers right again" verb after edits. Atomic-undo; scope is the main text story.
Failures: 3 Word busy, 4 Word not running.
insert-footnote --anchor-id ID --text "…" / insert-endnote …¶
wordlive insert-footnote --anchor-id ID --text "NOTE BODY" [--before | --after] [--doc DOC_NAME]
wordlive insert-endnote --anchor-id ID --text "NOTE BODY" [--before | --after] [--doc DOC_NAME]
Attach a footnote (bottom of the page) or endnote (end of the document) to the
anchor's range; Word auto-numbers the reference mark. Reports the new note's id
so you can edit or remove it later (footnote:N / endnote:N resolve through
--anchor-id everywhere).
$ wordlive insert-footnote --anchor-id range:120-140 --text "See appendix B."
{"ok": true, "anchor_id": "range:120-140", "footnote": 1, "note_id": "footnote:1"}
Failures: 1 bad input; 2 anchor not found; 3 Word busy.
insert-toc [--anchor-id ID] [--levels A-B] [--no-heading-styles] [--no-hyperlinks]¶
wordlive insert-toc [--anchor-id ID] [--levels 1-3]
[--heading-styles | --no-heading-styles] [--hyperlinks | --no-hyperlinks]
[--before | --after] [--doc DOC_NAME]
Insert a table of contents built from the document's headings. --anchor-id
defaults to start (the top of the document); --levels is the inclusive
heading-level span (1-3 = Heading 1–3). Entries link to their headings unless
--no-hyperlinks.
$ wordlive insert-toc --levels 1-2
{"ok": true, "anchor_id": "start",
"applied": {"levels": [1, 2], "use_heading_styles": true, "hyperlinks": true, "where": "after"}}
Page numbers populate only after repagination — run update-fields (or
snapshot) before reading them. Failures: 1 bad input (e.g. malformed
--levels); 2 anchor not found; 3 Word busy.
table-of-figures [--anchor-id ID] [--label L] [--no-label] [--no-hyperlinks]¶
wordlive table-of-figures [--anchor-id ID] [--label Figure] [--label | --no-label]
[--hyperlinks | --no-hyperlinks] [--before | --after] [--doc DOC_NAME]
Insert a table of figures built from the document's captions — every caption of
one --label (Figure/Table/Equation/a custom string) with its page number.
--anchor-id defaults to start; --no-label drops the "Figure"/"Table" prefix
from each entry, and entries link to their captions unless --no-hyperlinks.
$ wordlive table-of-figures --label Figure
{"ok": true, "anchor_id": "start",
"applied": {"label": "Figure", "include_label": true, "hyperlinks": true, "where": "after"}}
Page numbers populate only after repagination — run update-fields (or
snapshot) before reading them. Failures: 1 bad input; 2 anchor not found;
3 Word busy.
mark-index-entry --anchor-id ID --entry 'topic'¶
wordlive mark-index-entry --anchor-id ID --entry "topic"
[--cross-reference X] [--bold] [--italic] [--doc DOC_NAME]
Mark the anchor's range as a back-of-book index entry (an XE field) — the first
of the index's two steps (build it with insert-index). --entry is the headword;
use "main:sub" to nest a subentry. --cross-reference X makes the entry a "see
X" pointer instead of a page number, and --bold/--italic style the page number.
$ wordlive mark-index-entry --anchor-id range:120-140 --entry "risk:market"
{"ok": true, "anchor_id": "range:120-140", "entry": "risk:market"}
Failures: 1 bad input; 2 anchor not found; 3 Word busy.
insert-index [--anchor-id ID] [--columns N] [--run-in] [--right-align-page-numbers]¶
wordlive insert-index [--anchor-id ID] [--columns 2] [--run-in]
[--right-align-page-numbers] [--before | --after] [--doc DOC_NAME]
Build a back-of-book index from the entries marked with mark-index-entry.
--anchor-id defaults to end (the back of the document); --columns is the
column count (default 2), --run-in lays subentries inline rather than each on
its own line, and --right-align-page-numbers flushes the page numbers right with
a tab leader.
$ wordlive insert-index --columns 2
{"ok": true, "anchor_id": "end",
"applied": {"columns": 2, "run_in": false, "right_align_page_numbers": false, "where": "after"}}
Page numbers populate only after repagination — run update-fields (or
snapshot) before reading them. Failures: 1 bad input; 2 anchor not found;
3 Word busy.
footnotes / endnotes¶
List the document's footnotes / endnotes — each note's footnote:N / endnote:N
id, number, body text, and the para:N its reference mark sits in.
$ wordlive footnotes
[{"index": 1, "anchor_id": "footnote:1", "marker": "1",
"text": "See appendix B.", "para": "para:6"}]
Failures: 3 Word busy, 4 Word not running.
Citations & bibliography¶
Sources, in-text citations, and the bibliography / table of authorities.
bibliography-style --style STYLE¶
Set the document's bibliography style — the citation scheme Word renders citations
and the bibliography in (APA, MLA, Chicago, IEEE, Turabian, …). Which
names are accepted is build-dependent (Word ships a fixed set of style XSLTs);
an unsupported value fails. Atomic-undo.
Failures: 1 unsupported style (an OpError), 3 Word busy.
add-source --type TYPE [...]¶
wordlive add-source --type book
[--tag T --author "Last, First" (repeatable) --title "..." --year YYYY
--publisher "..." --city "..." --journal-name "..." --volume V --issue I
--pages P --url URL --edition E --doi D | --xml RAW] [--doc DOC_NAME]
Register a citation source in the document's master/source list — the first of
the citation workflow's two steps (cite it with insert-citation, then build the
list with insert-bibliography). --type is one of book, book_section,
journal_article, article_in_periodical, conference_proceedings, report,
web_site, document_from_site, electronic_source, art, sound_recording,
performance, film, interview, patent, case, misc (plus aliases).
--author is "Last, First" (or "First Last", and repeatable for several
authors); --tag is the short handle later passed to insert-citation and
auto-derives from the first author's surname + year when omitted. --xml
is the escape hatch: a raw <b:Source> OOXML element (which must carry its own
<b:Tag>), bypassing the typed flags.
$ wordlive add-source --type book --author "Smith, Jane" --title "On Risk" --year 2020
{"ok": true, "source": "Smith2020"}
Failures: 1 bad input (e.g. --xml without a <b:Tag>); 3 Word busy.
insert-citation --anchor-id ID --tag TAG [...]¶
wordlive insert-citation --anchor-id ID --tag TAG
[--pages P] [--prefix "..."] [--suffix "..."] [--volume V]
[--suppress-author] [--suppress-year] [--suppress-title]
[--locale 1033] [--before | --after] [--doc DOC_NAME]
Insert an in-text citation to a registered source at the anchor — a Word CITATION
field that renders per the active bibliography-style (e.g. (Smith 2020, 15)).
--tag is the source's tag (from add-source); --pages adds a page locator,
and the --suppress-* flags hide the author / year / title in this one citation.
A citation to a tag with no matching source still inserts, but renders
"Invalid source specified." — so register the source first.
$ wordlive insert-citation --anchor-id range:120-140 --tag Smith2020 --pages 15
{"ok": true, "citation": "Smith2020"}
Failures: 1 bad input; 2 anchor not found; 3 Word busy.
insert-bibliography [--anchor-id end] [--before | --after]¶
Insert the bibliography / works-cited block — a Word BIBLIOGRAPHY field listing
every cited source, formatted per the active bibliography-style. --anchor-id
defaults to end (the back of the document). Its entries and page-dependent
formatting populate only after repagination — run update-fields (or snapshot)
before reading them.
Failures: 1 bad input; 2 anchor not found; 3 Word busy.
mark-citation --anchor-id ID --long "full citation" [...]¶
wordlive mark-citation --anchor-id ID --long "FULL CITATION"
[--short ABBREV] [--category cases] [--before | --after] [--doc DOC_NAME]
Mark the anchor's range as a table-of-authorities entry (a Word TA field) —
the first of the table-of-authorities' two steps (build it with
table-of-authorities). --long is the full citation as it appears in the table;
--short is the abbreviated form for later same-authority references (defaults to
--long). --category groups the entry — cases / statutes / other /
rules / treatises / regulations / constitutional, or an int 1–16.
$ wordlive mark-citation --anchor-id range:120-140 \
--long "Brown v. Board, 347 U.S. 483 (1954)" --short "Brown" --category cases
{"ok": true, "anchor_id": "range:120-140"}
Failures: 1 bad input; 2 anchor not found; 3 Word busy.
table-of-authorities [--anchor-id end] [--category C] [--no-passim] [--no-keep-formatting]¶
wordlive table-of-authorities [--anchor-id end] [--category all]
[--passim | --no-passim] [--keep-formatting | --no-keep-formatting]
[--entry-separator "..."] [--page-range-separator "..."]
[--before | --after] [--doc DOC_NAME]
Build a table of authorities from the entries marked with mark-citation.
--anchor-id defaults to end. --category is all (every category) or a single
named category / int (as in mark-citation). --no-passim lists every page even
when an authority is cited on five or more (the default passim collapses those to
"passim"); --no-keep-formatting drops the marked entries' own character
formatting. --entry-separator / --page-range-separator override the punctuation
between an entry and its pages, and within a page range.
Page numbers populate only after repagination — run update-fields (or
snapshot) before reading them. Failures: 1 bad input; 2 anchor not found;
3 Word busy.
Themes¶
Read and apply the document-wide colour/font theme.
theme · list-themes¶
Read the document's theme — the document-wide brand primitive. theme reports the
12 theme colours (as #RRGGBB) and the major/minor fonts; list-themes reports the
built-in themes, colour schemes, and font schemes Office ships (the names the
apply-theme / set-theme-* commands accept). Both are non-mutating.
$ wordlive --json theme
{"colors": {"text1": "#000000", "accent1": "#156082", ...}, "major_font": "Aptos Display", "minor_font": "Aptos"}
apply-theme --theme NAME¶
Apply a whole document theme — colours, fonts, and effects in one step. --theme
is a built-in name (e.g. Facet, Ion — see list-themes) or a .thmx file path
(a brand file). Wrap-free atomic undo. Override individual brand colours/fonts
afterwards with set-theme-colors / set-theme-fonts.
Failures: 1 bad input (incl. an unknown theme name); 3 Word busy.
set-theme-colors [--scheme S] [--accent1 C] ...¶
wordlive set-theme-colors [--scheme NAME] [--text1 C] [--background1 C]
[--text2 C] [--background2 C] [--accent1 C] ... [--accent6 C]
[--hyperlink C] [--followed-hyperlink C] [--doc DOC_NAME]
Set the theme's colour scheme and/or individual brand colours. --scheme loads a
named built-in colour scheme (e.g. Blue) or a Theme-Colors .xml path; the
per-colour flags override individual slots. A colour value is a name (navy) or a
hex string (#1A73E8).
$ wordlive set-theme-colors --accent1 "#1A73E8" --accent2 "#34A853"
{"ok": true, "colors": {"accent1": "#1A73E8", "accent2": "#34A853", ...}, "applied": {...}}
set-theme-fonts [--scheme S] [--major F] [--minor F]¶
Set the theme's fonts. --scheme loads a named built-in font scheme (e.g.
Garamond) or a Theme-Fonts .xml path; --major / --minor override the heading
/ body font names.
$ wordlive set-theme-fonts --major Arial --minor Calibri
{"ok": true, "major_font": "Arial", "minor_font": "Calibri", "applied": {...}}
Failures for set-theme-*: 1 bad input (incl. an unknown scheme/colour); 3 Word busy.
Images, shapes & objects¶
Pictures, watermarks, text boxes, floating shapes, equations, and charts.
images / read-image --anchor-id ID [--out FILE]¶
The read side of images — pull an embedded picture's bytes back out (the write
side is insert-image). images lists every embedded picture: its image:N id,
MIME type, size (points), alt text, and the para:N it sits in.
$ wordlive images
[{"index": 1, "anchor_id": "image:1", "mime": "image/png",
"width": 240.0, "height": 160.0, "alt_text": "Chart", "para": "para:6"}]
read-image extracts one picture, resolved by --anchor-id image:N (or any
anchor whose range contains exactly one image). With --out the raw bytes are
written to that file and the JSON reports {path, mime, bytes}; without it,
base64 data is returned inline ({mime, bytes, base64}).
$ wordlive read-image --anchor-id image:1 --out logo.png
{"ok": true, "anchor_id": "image:1", "mime": "image/png", "bytes": 8462, "path": "logo.png"}
$ wordlive read-image --anchor-id image:1
{"ok": true, "anchor_id": "image:1", "mime": "image/png", "bytes": 8462, "base64": "iVBORw0KG…"}
Reading is non-mutating. Failures: 1 bad input (a range with no image, or more
than one); 2 anchor not found; 3 Word busy, 4 Word not running.
insert-image --anchor-id ID (--path FILE | --base64 VALUE) --wrap WRAP¶
wordlive insert-image --anchor-id ID (--path FILE | --base64 VALUE) --wrap WRAP \
[--before | --after] [--block | --no-block] [--width N] [--height N] \
[--alt-text "…"] [--lock-aspect | --no-lock-aspect] [--doc DOC_NAME]
Embed an image at any anchor. Exactly one image source is required:
--path reads a file from disk (best for large images); --base64 takes
base64 data inline, or --base64 - reads base64 from stdin (handy when an
LLM holds image data in memory). The picture is embedded in the document, not
linked, so the source file can move or vanish afterwards. Word auto-detects the
image's natural size; --width/--height (points) override it and
--lock-aspect (the default) keeps the aspect ratio.
A --path source is screened before any filesystem access: a non-local
path — a UNC \\host\share\…, a file://, or any URL — is rejected (exit 1),
and if --image-dir / WORDLIVE_IMAGE_DIRS is configured the path must resolve
inside it. Prefer --base64/stdin for untrusted (e.g. LLM-supplied) images.
--wrap is required so layout intent is always explicit:
--wrap |
Effect |
|---|---|
inline |
Stays in the text flow, like a character. |
auto |
Floats: Square if ≤ half the page's usable width, else top-and-bottom. |
square tight through top-bottom front behind |
Floats with that wrap type. |
--after (default) places the image just below the anchor; --before above.
--block puts the image on its own new (Normal) line instead of in the
anchor's text run — use it with --before at a heading so the image lands on its
own line above the heading instead of joining the heading text.
A floating wrap (anything but inline) leaves the text flow, so the image is
no longer an image:N; the output then carries a shape:N handle (see
Floating shapes) to re-wrap / reposition / resize / replace it. An inline
image stays image:N and reports "shape": null.
$ wordlive insert-image --anchor-id heading:2 --path diagram.png --wrap auto
{"ok": true, "anchor_id": "heading:2", "anchor": {"kind": "heading", "name": "Risks"}, "shape": "shape:1", "wrap": "auto", "where": "after"}
$ base64 logo.png | wordlive insert-image --anchor-id bookmark:Header --base64 - --wrap square --width 96
{"ok": true, "anchor_id": "bookmark:Header", "anchor": {"kind": "bookmark", "name": "Header"}, "shape": "shape:1", "wrap": "square", "where": "after"}
Failures: 1 the image is missing, unreadable, or not a recognised raster
format (PNG/JPEG/GIF/BMP/TIFF) — an ImageSourceError; 2 anchor not found
or an invalid --wrap value; 3 Word busy.
watermark (--text "…" | --remove) [--layout …] [--color …] [--font …] [--solid]¶
wordlive watermark --text "DRAFT" [--font NAME] [--color COLOR] \
[--layout diagonal|horizontal] [--transparent | --solid] [--doc DOC_NAME]
wordlive watermark --remove [--doc DOC_NAME]
Stamp (or --remove) a text watermark behind every page — WordArt drawn into
each section's header story, named like Word's own Design → Watermark object
(so it replaces a ribbon-added one). --layout is diagonal (45°, default) or
horizontal; --color the fill (default #C0C0C0); --solid turns off the
default 50% wash. Setting twice replaces rather than stacks. One atomic-undo step.
$ wordlive watermark --text "CONFIDENTIAL" --color "#ff0000"
{"ok": true, "text": "CONFIDENTIAL", "sections": 1}
$ wordlive watermark --remove
{"ok": true, "removed": 1}
Failures: 1 bad --layout/--color; 3 Word busy, 4 Word not running.
insert-text-box --anchor-id ID --text "…" [--width …] [--height …] [--wrap …]¶
wordlive insert-text-box --anchor-id ID --text "…" [--width W] [--height H] \
[--wrap square|tight|through|top-bottom|front|behind] [--before | --after] \
[--font F] [--size S] [--bold | --no-bold] [--italic | --no-italic] \
[--align left|center|right|justify] [--fill COLOR] \
[--border-color COLOR | --no-border] [--doc DOC_NAME]
Insert a floating text box / pull quote anchored to the anchor's paragraph.
--width/--height accept points or unit strings (3in, 8cm); --wrap is
how body text flows around it (the insert-image vocabulary minus inline).
--fill sets a background colour and --no-border / --border-color control
the outline. One atomic-undo step. The output carries the new text box's
shape:N id (see Floating shapes below) for restyling it afterward.
$ wordlive insert-text-box --anchor-id heading:2 --text "Key takeaway" --width 2.5in --fill "#eeeeff"
{"ok": true, "anchor_id": "shape:1", "wrap": "square"}
Failures: 2 anchor not found or invalid --wrap; 3 Word busy.
Floating shapes — shapes / set-shape-* / format-shape / group-shapes / ungroup-shape / replace-shape-image / delete-shape / set-image-*¶
wordlive shapes [--doc DOC_NAME]
wordlive set-shape-wrap --anchor-id shape:N [--wrap square|tight|through|top-bottom|front|behind] [--side both|left|right|largest] [--distance-top D] [--distance-bottom D] [--distance-left D] [--distance-right D]
wordlive set-shape-crop --anchor-id shape:N [--left L] [--top T] [--right R] [--bottom B]
wordlive set-shape-position --anchor-id shape:N [--left L] [--top T] [--relative-to margin|page]
wordlive set-shape-size --anchor-id shape:N [--width W] [--height H] [--lock-aspect | --no-lock-aspect]
wordlive format-shape --anchor-id shape:N [--fill C] [--border-color C | --no-border | --default-border] [--border-weight W]
wordlive set-shape-alt-text --anchor-id shape:N --text "…"
wordlive set-shape-text --anchor-id shape:N --text "…"
wordlive set-shape-rotation --anchor-id shape:N --degrees 30
wordlive set-shape-z-order --anchor-id shape:N --order front|back|forward|backward
wordlive set-shape-text-frame --anchor-id shape:N [--margin-left L] [--margin-right R] [--margin-top T] [--margin-bottom B] [--word-wrap | --no-word-wrap]
wordlive replace-shape-image --anchor-id shape:N (--path FILE | --base64 VALUE)
wordlive group-shapes --anchor-id shape:N --anchor-id shape:M [...]
wordlive ungroup-shape --anchor-id shape:N
wordlive delete-shape --anchor-id shape:N
wordlive set-image-alt-text --anchor-id image:N --text "…"
wordlive set-image-size --anchor-id image:N [--width W] [--height H] [--lock-aspect | --no-lock-aspect]
wordlive set-image-crop --anchor-id image:N [--left L] [--top T] [--right R] [--bottom B]
shape:N addresses the document's floating shapes — a text box from
insert-text-box, a floating image from insert-image (any --wrap but
inline), or WordArt — in document order; header-story watermarks are excluded.
shapes lists them (id, kind, size, rotation, z-order, wrap, wrap-side, crop, the
para:N they're anchored in). shape:N is positional: inserting or deleting a shape renumbers
the rest, so re-list rather than caching an id (the image:N / chart:N rule).
textbox:N is an addressing alias onto a text box's canonical shape:N.
--left/--top are lengths (2in) or center. set-shape-text /
set-shape-text-frame need a text box; set-shape-z-order restacks within the
float layer (distinct from wrap's front/behind-text); set-shape-rotation is an
absolute angle. group-shapes collapses two or more floats into one group
shape:N (pass --anchor-id twice or more), and ungroup-shape dissolves a
group back into its members. replace-shape-image needs a picture shape and swaps
its image in place (delete + reinsert at the same anchor, preserving wrap /
position / size / alt text) — pass exactly one of --path / --base64, and
--path is screened like insert-image. set-shape-wrap takes any one of
--wrap / --side (which sides text flows past — square/tight/through
honour it) / --distance-* (standoff gaps). set-shape-crop / set-image-crop
trim a picture in from its edges (a non-picture shape is rejected).
set-image-* restyle an inline picture (image:N) without floating it
(re-wrapping is insert-image --wrap). There is no autosize knob — Word's
resize-to-fit-text doesn't expose over COM.
The exec ops are set_shape_wrap, set_shape_crop, set_shape_position,
set_shape_size, format_shape, set_shape_alt_text, set_shape_text,
set_shape_rotation, set_shape_z_order, set_shape_text_frame,
replace_shape_image, delete_shape, group_shapes, ungroup_shape,
set_image_alt_text, set_image_size, set_image_crop.
$ wordlive set-shape-size --anchor-id shape:1 --width 3in --no-lock-aspect
{"ok": true, "anchor_id": "shape:1", "applied": {"width": "3in", "lock_aspect": false}}
Failures: 2 anchor not found or not a shape:N; 3 Word busy.
insert-equation --anchor-id ID (--unicodemath … | --latex … | --mathml …) / equations¶
wordlive insert-equation --anchor-id ID (--unicodemath "…" | --latex "…" | --mathml "…") \
[--display | --inline] [--before | --after] [--doc DOC_NAME]
wordlive equations [--doc DOC_NAME]
Insert a mathematical equation at any anchor. Exactly one input dialect is required:
| Input | Needs | Example |
|---|---|---|
--unicodemath |
nothing (native) | "x=(-b±√(b^2-4ac))/(2a)", "a^2+b^2=c^2" |
--latex |
the latex extra |
"\frac{-b\pm\sqrt{b^2-4ac}}{2a}" |
--mathml |
nothing | "<math>…</math>" (or --mathml - from stdin) |
UnicodeMath is typed into a math zone and built up by Word itself; LaTeX and
MathML travel LaTeX→MathML→OMML→Word through Office's own shipped transform, so
only the LaTeX→MathML hop needs a third party (pip install "wordlive[latex]").
The equation lands on its own paragraph with a pinned style (so it never
inherits a neighbouring heading's style and pollutes the outline/TOC):
--display (default) gives it the centred Equation paragraph style
(auto-created, based on Normal); --inline makes it Normal and left-aligned
(still its own paragraph, not mid-sentence). --after (default) places it below
the anchor; --before above (including a clean prepend at the document start).
equations is the read side: every equation's equation:N id, type
(display/inline), a linear preview, and the para:N it sits in. equation:N is
positional (Word's OMaths order) — inserting another equation before an
existing one renumbers it, so re-list rather than caching an id across inserts.
$ wordlive insert-equation --anchor-id heading:2 --unicodemath "a^2+b^2=c^2"
{"ok": true, "anchor_id": "heading:2", "equation": 1, "equation_anchor_id": "equation:1", "display": true, "where": "after"}
$ wordlive equations
[{"index": 1, "anchor_id": "equation:1", "type": "display", "linear": "𝑎2+𝑏2=𝑐2", "para": "para:1"}]
Failures: 1 malformed input (no dialect, or more than one; unparseable
MathML/LaTeX; the latex extra not installed) — an EquationError; 2 anchor
not found; 3 Word busy.
insert-chart --anchor-id ID --kind bar|pie|line|scatter --data JSON / charts¶
wordlive insert-chart --anchor-id ID --kind bar|pie|line|scatter --data JSON \
[--title "…"] [--before | --after] [--doc DOC_NAME]
wordlive charts [--doc DOC_NAME]
Insert an Excel-backed chart at any anchor (atomic-undo). --data is JSON
(or --data - to read it from stdin): an object {"label": value} for
bar/pie/line, or an array of [x, y] pairs for scatter (both axes
numeric, duplicate x preserved); line accepts either. --title sets the chart
title and series name.
Charts embed a hidden Excel workbook (InlineShapes.AddChart2), so Excel must
be installed — if it isn't, the command exits 6 (ExcelNotAvailableError)
and the document is left untouched. wordlive then breaks the data link, so the
chart's data is static: no embedded workbook ships in the document, and the
series data isn't read back. charts is the read side — every chart's chart:N
id, kind, title, and para:N; chart:N is positional (document order), so
inserting another chart earlier renumbers it.
$ wordlive insert-chart --anchor-id end --kind bar --data '{"Q1": 10, "Q2": 25, "Q3": 18}' --title "Quarterly"
{"ok": true, "anchor_id": "end", "chart": 1, "chart_anchor_id": "chart:1", "kind": "bar", "where": "after"}
$ echo '[[1.2, 3.4], [1.2, 3.9], [2.5, 6.1]]' | wordlive insert-chart --anchor-id end --kind scatter --data -
{"ok": true, "anchor_id": "end", "chart": 2, "chart_anchor_id": "chart:2", "kind": "scatter", "where": "after"}
$ wordlive charts
[{"index": 1, "anchor_id": "chart:1", "kind": "bar", "title": "Quarterly", "chart_style": 201, "has_legend": false, "para": "para:1"}]
Failures: 1 malformed --data (bad JSON, empty, wrong shape, non-numeric
value — an OpError); 2 anchor not found; 3 Word busy; 6 Excel not
installed (ExcelNotAvailableError).
format-chart / format-axis / add-trendline / set-series-color / format-series / add-error-bars¶
wordlive format-chart --anchor-id chart:N [--title TXT] [--legend|--no-legend] \
[--legend-position right|left|top|bottom|corner] [--chart-style INT] \
[--background COLOR] [--plot-background COLOR] \
[--font NAME] [--font-size SIZE] [--font-color COLOR] \
[--data-labels|--no-data-labels] [--data-label-format FMT] \
[--chart-type bar|pie|line|scatter] \
[--gap-width INT] [--overlap INT] [--data-table|--no-data-table]
wordlive format-axis --anchor-id chart:N --which value|y|category|x \
[--title TXT] [--minimum N] [--maximum N] [--scale linear|log] \
[--number-format FMT] [--gridlines|--no-gridlines]
wordlive add-trendline --anchor-id chart:N [--series N] \
[--kind linear|exponential|logarithmic|moving_average|polynomial|power] \
[--display-equation] [--display-r-squared] [--forward N] [--backward N] \
[--order N] [--period N]
wordlive set-series-color --anchor-id chart:N --color COLOR [--series N] [--point N]
wordlive format-series --anchor-id chart:N [--series N] [--point N] \
[--marker circle|square|diamond|triangle|x|star|dot|dash|plus|none|auto] \
[--marker-size N] [--smooth|--no-smooth] [--explosion N] \
[--data-labels|--no-data-labels] [--data-label-size N] [--data-label-color COLOR]
wordlive add-error-bars --anchor-id chart:N [--series N] \
[--kind fixed|percent|stdev|sterror] [--amount N] \
[--include both|plus|minus] [--axis y|value|x|category]
Format and design an existing chart:N — Word's chart "Design"/"Format"
tabs. These operate on the post-insert, static chart, so Excel is not needed
(no exit 6). Every option is tri-state: only what you pass is written. Colours are
a name, hex (#2E86C1), or comma-separated r,g,b. format-chart --chart-type
re-types the chart in place; --chart-style is the built-in design-gallery id;
--gap-width/--overlap tune bar spacing (bar/column charts) and --data-table
toggles the data-table grid. format-axis --scale log suits order-of-magnitude
data; --which takes value/y or category/x. add-trendline --kind power
--display-equation draws the law of best fit; polynomial takes --order (2–6),
moving_average takes --period. set-series-color --point N recolours one bar /
pie slice / marker (1-based); omit it to colour the whole series. format-series
styles a series' markers (--marker, --marker-size), line --smooth, pie
--explosion, and per-point data-label font (--point narrows marker / explosion
/ label to one point). add-error-bars draws fixed/percent/stdev/sterror
bars (--amount required unless --kind sterror).
$ wordlive format-chart --anchor-id chart:1 --chart-style 240 --legend --title "Quarterly"
{"ok": true, "anchor_id": "chart:1", "applied": {"title": "Quarterly", "legend": true, "chart_style": 240}}
$ wordlive format-axis --anchor-id chart:1 --which value --scale log --title "USD (M)"
{"ok": true, "anchor_id": "chart:1", "which": "value", "applied": {"title": "USD (M)", "scale": "log"}}
$ wordlive add-trendline --anchor-id chart:2 --kind power --display-equation
{"ok": true, "anchor_id": "chart:2", "applied": {"series": 1, "kind": "power", "display_equation": true, "display_r_squared": false}}
$ wordlive set-series-color --anchor-id chart:1 --color "#2E86C1" --point 2
{"ok": true, "anchor_id": "chart:1", "series": 1, "point": 2, "color": "#2E86C1"}
$ wordlive format-series --anchor-id chart:2 --marker circle --marker-size 8 --smooth
{"ok": true, "anchor_id": "chart:2", "series": 1, "point": null}
$ wordlive add-error-bars --anchor-id chart:1 --kind percent --amount 5
{"ok": true, "anchor_id": "chart:1", "series": 1, "applied": {"series": 1, "kind": "percent", "include": "both", "axis": "y", "amount": 5.0}}
Failures: 1 bad input (unknown colour / scale / trendline kind / marker, an
error-bar kind missing its amount, or the anchor isn't a chart:N); 2 chart not
found; 3 Word busy.
Sections, headers & footers¶
Section layout, page setup, and header/footer content.
sections¶
List the document's sections with each one's page setup. (Previously
section list, now a hidden deprecated alias.)
$ wordlive sections
[{"index": 1,
"page_setup": {"orientation": "portrait",
"top_margin": 72.0, "bottom_margin": 72.0,
"left_margin": 72.0, "right_margin": 72.0,
"page_width": 612.0, "page_height": 792.0}}]
Margins and page dimensions are in points. Headers and footers live in the
header / footer commands. Failures: 3 Word busy, 4 Word not running.
page-setup [--section N] [...]¶
wordlive page-setup [--section N]
[--margins V] [--top-margin V] [--bottom-margin V] [--left-margin V] [--right-margin V]
[--gutter V] [--orientation portrait|landscape] [--paper-size letter|legal|tabloid|a3|a4|a5]
[--columns N] [--column-spacing V] [--doc DOC_NAME]
The write mirror of section list — set a section's page geometry. --margins
sets all four margins at once; the per-side --*-margin flags override it. Length
values (margins, gutter, column spacing) accept points or a unit string
(1in, 2.5cm). --orientation and --paper-size reshape the page;
--columns N lays the section out in N equal, newspaper-style columns (the
section counterpart to insert-break --kind column). --section defaults to 1,
which is the whole document for a single-section file. Atomic-undo. At least one
option is required.
$ wordlive page-setup --orientation landscape --margins 0.5in --columns 2
{"ok": true, "section": 1,
"applied": {"margins": "0.5in", "orientation": "landscape", "columns": 2}}
Failures: 1 no option / bad input, 2 section out of range, 3 Word busy.
header read | write · footer read | write¶
wordlive header read [--section N] [--which primary|first|even] [--doc DOC_NAME]
wordlive header write [--section N] [--which primary|first|even] --text "..." [--doc DOC_NAME]
wordlive footer read [--section N] [--which primary|first|even] [--doc DOC_NAME]
wordlive footer write [--section N] [--which primary|first|even] --text "..." [--doc DOC_NAME]
Read or set a section's header/footer. --section defaults to 1 and
--which to primary (the other options are first for the first-page
header/footer and even for even pages). write is atomic-undo. A header/footer
is just a range, so its id (header:S:WHICH / footer:S:WHICH) also works with
replace, style apply, and format-paragraph.
$ wordlive header read --section 1
{"anchor_id": "header:1:primary", "section": 1, "which": "primary", "text": "Confidential"}
$ wordlive header write --section 1 --text "ACME Corporation — Q2 Report"
{"ok": true, "anchor_id": "header:1:primary", "section": 1, "which": "primary"}
--text mode (wordlive --text header read) emits just the header text.
Failures: 2 section out of range, 3 Word busy.
Comments & track changes¶
Review comments plus tracked-change recording and resolution.
revisions¶
List the document's tracked changes — the structured counterpart to
snapshot --markup all. Each revision reports its type (insert / delete /
format / …), author, the affected text, and a range:START-END id.
Reading is non-mutating. (Toggle Track Changes with track on / off; check it
with track status.) This is an alias for revision list — see the revision
group below to resolve changes.
$ wordlive revisions
[{"index": 1, "type": "insert", "author": "A. Reviewer", "text": "swift",
"anchor_id": "range:5-10", "start": 5, "end": 10, "date": "2026-06-08T16:57:00"}]
Failures: 3 Word busy, 4 Word not running.
revision list | accept | reject | accept-all | reject-all¶
wordlive revision list
wordlive revision accept --index N
wordlive revision reject --index N
wordlive revision accept-all [--anchor-id ID]
wordlive revision reject-all [--anchor-id ID]
Resolve tracked changes (the write side of revisions). accept makes the
change at 1-based --index permanent; reject undoes it — both renumber the
remaining revisions, so re-list between resolves. accept-all / reject-all
resolve every change at once and report the count; pass --anchor-id to scope
to a single anchor's range ("accept all my edits in this section" — note an
anchor's range is literal, so a heading:N covers only the heading line, not the
body below it; target a paragraph/range that spans the changes). All wrap one
atomic-undo step.
$ wordlive revision accept --index 1
{"ok": true, "index": 1, "accepted": true}
$ wordlive revision accept-all --anchor-id heading:3
{"ok": true, "accepted": 4, "anchor_id": "heading:3"}
Failures: 2 anchor not found / index out of range, 3 Word busy, 4 Word not
running.
comment list¶
Enumerate every review comment, in document order.
$ wordlive comment list
[{"index": 1, "author": "ReviewBot", "text": "Please verify this figure.",
"scope": "$400", "done": false}]
index is the 1-based handle used by resolve and delete. scope is the
document text the comment is attached to; done is the resolved flag (always
false on Word builds older than 2013). Failures: 3 Word busy, 4 Word not
running.
comment add --anchor-id ID --text "…"¶
Attach a comment to the anchor's range. The document text is untouched —
this is the polite, side-channel alternative to rewriting a passage. Atomic-undo.
The anchor id is any of the recognised forms, including
a range:START-END from find.
$ wordlive comment add --anchor-id heading:3 --text "Please expand this." --author "ReviewBot"
{"ok": true, "anchor_id": "heading:3", "comment": {"index": 1, "author": "ReviewBot"}}
--author is optional; without it Word uses the running app's user name.
Failures: 2 anchor not found, 3 Word busy.
comment resolve --index N¶
Mark comment N (from comment list) as resolved/done. Requires Word 2013+.
Atomic-undo.
Failures: 2 index out of range, 3 Word busy.
comment delete --index N¶
Delete comment N. Remaining comments re-index, so re-list before deleting
another by index. Atomic-undo.
Failures: 2 index out of range, 3 Word busy.
track status | on | off¶
wordlive track status [--doc DOC_NAME]
wordlive track on [--doc DOC_NAME]
wordlive track off [--doc DOC_NAME]
Inspect or toggle the document's Track Changes setting. While on, every edit (yours or the user's) is recorded as a revision the user can accept or reject.
The toggle is persistent — track on leaves Word recording revisions until
track off. For a self-restoring scope, prefer the library's
doc.tracked_changes() context manager, or set "tracked": true on an
exec script to record a single batch as tracked
changes and restore the prior setting afterwards. Failures: 3 Word busy, 4
Word not running.
Linting & regularizing¶
Audit formatting/structure and write the fixes back. New here? Start with the Linting & regularizing guide for the mental model, a guided walkthrough, and the full rule catalog — this section is the flag-level reference.
lint¶
wordlive lint [--rule ID|TAG ...] [--exclude ID|TAG ...] [--within ID] [--profile PATH] [--doc DOC_NAME]
Audit the document for formatting inconsistency, structural slips, and policy
breaches — the read that answers "what's off about this document before I hand
it over". Returns a severity-ranked list of findings, each
{rule, kind, severity, anchor_id, message, fixable, fix, observed, expected}:
kind is consistency / structural / policy, severity is error /
warning / info, and when fixable is true the fix is an op-shaped dict
(or list of them) — literally an exec op regularize would run.
--rule / --exclude (repeatable, mutually exclusive) narrow the rule set by
id or tag; with neither, the default set runs (every on-by-default
consistency + structural rule; policy and opinionated rules are off). A rule
that's off by default still runs when named (--rule em-dash-usage) or pulled in
by its tag (--rule typography lights up the whole cluster). --within scopes
the audit to one anchor (heading:N / range:S-E / table:N:R:C). It's a pure
read: layout rules repaginate content-neutrally, leaving selection, scroll, and
the document's Saved state untouched.
Foundation rules — structural: heading-keep-with-next, table-repeat-header,
list-numbering-continuity; consistency: heading-font-consistent,
heading-spacing-consistent, body-font-consistent (name/size/bold, body prose
only — table cells belong to table-style-consistent), and mixed-run-format
(report-only, not fixable).
Typography rules (tag typography) — on by default: trailing-whitespace,
leading-whitespace, space-before-punctuation, double-space,
manual-heading-formatting (report-only — a short bold/enlarged Normal
paragraph that reads like a heading but isn't styled; table cells are skipped, so
a bold header row isn't flagged), table-style-consistent
(restyle the minority tables onto the dominant table style). Off by default
(name them or use --rule typography): hyphen-as-range (→ en-dash),
em-dash-usage, tabs-for-layout, manual-line-break (the last three are
report-only). The fixable whitespace/punctuation/range rules write via the
find_replace regex mode (below), scoped to the offending paragraph, so they're
idempotent.
Finalization rules (tag finalization, all off by default — an opt-in
"is-this-ready-to-send?" check, --rule finalization): comments-present,
unaccepted-revisions, track-changes-on, hidden-text-present, and
stale-fields (updatable TOC/SEQ/REF/PAGE fields present — a refresh nudge) are
report-only; leftover-highlight clears leftover highlighter colour (the one
fixable rule, idempotent).
Field-code rules (the P1 cross-reference/caption backbone) — on by default:
broken-cross-reference (a REF/PAGEREF field rendering Word's "reference
source not found" error) and caption-manual-numbering (a Caption paragraph
numbered with literal text, not a SEQ field). Off by default (tag
layout): page-numbers-present (no PAGE field in any header/footer). Off
by default (tag crossref / academia): xref-as-literal-text (a body
paragraph mentioning a figure/table by literal number with no REF field —
heuristic, so opt-in). All are report-only. The cross-reference/caption rules
also carry the academia tag, so --rule academia selects the cluster.
Hyperlink rules (a walk over wordlive hyperlinks) — on by default:
hyperlink-broken-internal (an internal HYPERLINK \l jump whose target bookmark
no longer exists — a dead link). Off by default (tags hyperlinks / print):
hyperlink-bare-for-print (an external link whose visible text doesn't contain its
URL — invisible in print) and hyperlink-display-is-raw-url (a link whose whole
label is a bare URL). All report-only. --rule hyperlinks selects the cluster;
--rule print selects just the two print/sharing rules.
Heading & document-structure rules (§B — a walk over wordlive read outline) —
on by default: heading-level-skip (the outline jumps a level — an H1 followed
by an H3 with no H2) and empty-heading (a heading paragraph with no text). Off
by default (tags headings / structure): adjacent-headings (two headings in a
row with no body between), heading-numbering-manual (a heading numbered by hand,
3.1 Methods, not by automatic numbering), heading-trailing-period (a heading
ending in a period — the one fixable rule, strips it in place), and
toc-present-and-current (top-level headings but no table-of-contents field —
presence-only). --rule structure (or --rule headings) selects the cluster.
Layout / document-level rules (§H — a walk over wordlive sections /
wordlive properties plus wordlive read watermark), all off by default and
report-only. Tag layout: header-footer-consistent (the primary header/footer
text disagrees across the document's own sections) and draft-watermark-present
(a leftover DRAFT / CONFIDENTIAL watermark, also tagged finalization). The
profile-driven policy rules document-properties-filled (a required built-in
property — Title / Author by default — left empty) and, tagged notices,
confidentiality-notice / copyright-notice (a required notice string, default
©, missing from every header/footer and the body). --rule layout selects the
whole cluster; --rule notices selects just the two notice rules.
Policy rules run only when a --profile PATH (a JSON house-style config) enables
them: body-justified, body-line-spacing (needs a target), and
table-numeric-right-align (a threshold, default 0.8). The profile can also
override a rule's severity or disable a default rule. All three fix idempotently.
$ wordlive lint --profile wordlive.lint.json # enables the policy rules
$ wordlive lint --within heading:3
[{"rule": "heading-keep-with-next", "kind": "structural", "severity": "warning",
"anchor_id": "heading:3", "message": "Heading does not keep with next paragraph.",
"fixable": true, "observed": false, "expected": true,
"fix": {"op": "format_paragraph", "anchor_id": "heading:3", "keep_with_next": true}}]
fixable findings feed straight into regularize; report-only ones
(mixed-run-format) are yours to resolve by hand.
Failures: 1 unknown rule id/tag, 2 --within anchor not found,
3 Word busy, 4 Word not running.
regularize¶
wordlive regularize [--rule ID|TAG ...] [--exclude ID|TAG ...] [--within ID] [--profile PATH] [--dry-run] [--allow-content] [--doc DOC_NAME]
Apply the fixable lint findings in one atomic-undo edit (labelled
"Regularize formatting", so one Ctrl-Z reverts them all; selection and scroll
are preserved). Returns {applied, skipped, deferred, findings} — each a
list of finding dicts — plus ops_run (and dry_run when set). On a
dry run applied stays [] (nothing is written) and the plan is in findings.
The default fixes are targeted and idempotent — each writes the style's own
value back as a direct property, so a second regularize applies nothing (a
tested invariant). --dry-run plans without writing. Same --rule / --exclude
/ --within selection as lint.
Fixes that change content rather than formatting (deleting a stray paragraph,
inserting a caption, stripping a watermark) are flagged adds_content and
withheld into deferred — pass --allow-content to apply them in the same
pass. It's Track-Changes-aware: with Track Changes on, the edits are recorded as
revisions.
$ wordlive regularize --within heading:7 --dry-run
{"applied": [], "skipped": [], "dry_run": true,
"findings": [{"rule": "leading-whitespace", "anchor_id": "para:6", "fixable": true,
"fix": {"op": "find_replace", "find": "^[ \\t]+", "text": "",
"in": "para:6", "all": true, "mode": "regex", "required": false}}]}
$ wordlive regularize --within heading:7
{"applied": [{"rule": "leading-whitespace", "anchor_id": "para:6", "fixable": true}],
"skipped": [], "ops_run": 1, "findings": [ ... ]}
$ wordlive --text regularize --within heading:7 # human-readable summary
fixed 1; skipped 0 (report-only / not fixable)
fixed: leading-whitespace (para:6)
Run lint first to preview; regularize --dry-run to see exactly which fixes
would fire. Atomic-undo.
Failures: 1 unknown rule id/tag, 2 --within anchor not found,
3 Word busy, 4 Word not running.
Checkpoints & diff¶
Fingerprint the document and diff what changed.
checkpoint¶
wordlive checkpoint [--include text|text+style|text+format] [--within ID] [--out FILE] [--doc DOC_NAME]
Fingerprint the document's structure now → a checkpoint token (a pure read).
Store the token, edit the document, then diff --since FILE for a structured
change list — the way an agent verifies its edits landed, or sees what the user
changed (Word emits no content-change event). --include sets the fingerprint
depth: text (a restyle is invisible) < text+style (default — restyle
surfaces) < text+format (a direct-format edit surfaces as a reformat).
--within ID fingerprints just one anchor's range. Without --out the token is
the JSON object on stdout; with --out FILE the token is written to the file and
a small summary is emitted.
$ wordlive checkpoint --out cp.json
{"out": "cp.json", "include": "text+style", "scope": null, "paragraphs": 42, "tables": 1}
diff¶
Diff a stored checkpoint against the document now (--since, needs live
Word), or diff two stored checkpoints (--from / --to, no live Word). Emits a
content-aligned change list: each change is replace / insert / delete /
restyle / reformat, carrying the current para:N so a follow-up op can act on
it immediately; table edits are reported coarsely as table_change /
table_insert / table_delete (with the table:N and before/after shape). An
unchanged document returns [] (a doc_hash fast-path). --since rejects a
checkpoint scoped to a range: (offsets shift under edits — use a stable anchor or
--from/--to).
$ wordlive diff --since cp.json
[{"op": "replace", "anchor_id": "para:14", "index_before": 12, "index_after": 13,
"text_before": "Costs fell 4%.", "text_after": "Costs fell 9%."},
{"op": "insert", "anchor_id": "para:7", "index_after": 6, "text_after": "A new note."}]
Failures: 1 bad/missing file or argument combination, 2 --within anchor not
found, 3 Word busy, 4 Word not running.
Hyperlinks & field discovery¶
Read mirrors of the links and fields already in the document.
hyperlinks¶
List the document's hyperlinks — the read mirror of link. Each reports its
visible text, external address or internal sub_address bookmark,
screen_tip, and a range:START-END / para:N. Non-mutating.
$ wordlive hyperlinks
[{"index": 1, "text": "Acme", "address": "https://acme.example", "sub_address": "",
"screen_tip": "", "anchor_id": "range:15-19", "para": "para:2"}]
Failures: 3 Word busy, 4 Word not running.
set-hyperlink --index N [--address URL] [--sub-address BM] [--text T] [--screen-tip T]¶
wordlive set-hyperlink --index N [--address URL] [--sub-address BOOKMARK]
[--text T] [--screen-tip T] [--doc DOC_NAME]
Retarget or relabel an existing hyperlink in place — no delete + reinsert.
Address the link by its 1-based --index (from hyperlinks). --address is the
external destination (URL / mailto / file path); --sub-address is the
in-document bookmark target; --text is the visible link text; --screen-tip
the hover tooltip. Pass at least one; omitting one leaves it untouched.
--address and --sub-address are left orthogonal (setting one does not clear
the other). These retarget a link, they don't unlink it: --sub-address /
--screen-tip clear with "", but --address / --text cannot be emptied
(Word keeps every link pointing somewhere with visible text).
$ wordlive set-hyperlink --index 1 --address https://new.example --text "New site"
{"ok": true, "index": 1, "applied": {"address": "https://new.example", "text": "New site"}}
Failures: 1 bad input; 2 index out of range; 3 Word busy.
fields¶
List the document's fields — the read mirror of insert-field. Each reports its
kind (the code's leading keyword — PAGE / REF / TOC / …), raw code,
last-rendered result, locked, and a range:START-END / para:N. Run
update-fields first to refresh stale results. Non-mutating.
$ wordlive fields
[{"index": 1, "kind": "PAGE", "type": 33, "code": "PAGE", "result": "1",
"locked": false, "anchor_id": "range:16-17", "para": "para:2"}]
Failures: 3 Word busy, 4 Word not running.
Document metadata¶
Built-in/custom properties and document variables.
properties list | set | delete¶
wordlive properties list [--doc DOC_NAME]
wordlive properties set --name NAME --value VALUE [--custom] [--doc DOC_NAME]
wordlive properties delete --name NAME [--doc DOC_NAME]
Read and edit the document's metadata. list returns {builtin, custom} —
the built-in bag (Title, Author, Subject, Keywords, …, plus read-only stats like
the word count) and any custom name/value pairs. set writes a built-in
property by name, or a custom one with --custom (created if absent); delete
removes a custom property (built-ins can't be removed). Writes are atomic-undo.
$ wordlive properties set --name Title --value "Q3 Report"
{"ok": true, "name": "Title", "value": "Q3 Report", "custom": false}
Failures: 1 read-only/unknown built-in (set) or missing custom (delete),
3 Word busy, 4 Word not running.
variables list | set | delete¶
wordlive variables list [--doc DOC_NAME]
wordlive variables set --name NAME --value VALUE [--doc DOC_NAME]
wordlive variables delete --name NAME [--doc DOC_NAME]
Read and edit the document's variables — invisible named string storage (the
backing store for { DOCVARIABLE name } fields). list returns a {name: value}
map; set creates or updates a variable; delete removes one. Writes are
atomic-undo.
Failures: 2 missing variable (delete), 3 Word busy, 4 Word not running.
Snapshots & saving¶
Render pages to PNG and save / export.
snapshot [--anchor-id ID | --page N | --pages A-B]¶
wordlive snapshot [--anchor-id ID | --page N | --pages A-B] \
[--out FILE] [--dpi 150] [--max-dim N] [--markup none|all] [--doc DOC_NAME]
Render document page(s) to PNG so a vision model can see the layout — real fonts, spacing, and page geometry, not just the text. Word exports a pixel-faithful PDF of the document it has open and wordlive rasterises the requested pages. Read-only: the document and the user's cursor are untouched.
Pick at most one target; with none, the whole document is rendered:
| Target | Renders |
|---|---|
--anchor-id ID |
the page(s) the anchor occupies — a heading: expands to its whole section (heading + body) |
--page N |
a single 1-based page |
--pages A-B |
an inclusive page span, e.g. 2-4 |
Output: with --out FILE the image is written to disk — a single page to FILE,
multiple pages alongside it as <stem>-p<N><suffix>. Without --out, base64
PNG data is returned inline in the JSON (images[].base64), which suits an LLM
that wants to look at the page directly. --dpi (default 150) sets resolution.
--max-dim N caps each page's long edge to N pixels (only ever lowering
resolution). A vision model is billed on an image's pixel area, so a long-edge
cap is a predictable per-page token budget regardless of paper size — the lever
for a cheap whole-document layout check (pair it with no page target; ~1000
stays legible for "did my styling land"). It composes with --dpi (the cap wins
when it implies a lower resolution); --dpi 72 is a coarser alternative.
$ wordlive snapshot --max-dim 1000 # whole doc, every page capped to 1000px long edge
{"ok": true, "selector": "all", "dpi": 150, "max_dim": 1000, "count": 12, "images": [...]}
--markup all renders tracked changes and comments as visible revision marks
and balloons (default none renders the final document); the structured list is
the revisions command.
This needs the optional snapshot extra (PyMuPDF):
pip install "wordlive[snapshot]" (or uv add "wordlive[snapshot]").
$ wordlive snapshot --anchor-id heading:3 --out section.png
{"ok": true, "selector": "heading:3", "dpi": 150, "count": 1, "images": [{"page": 4, "bytes": 81234, "path": "section.png"}]}
$ wordlive snapshot --page 1
{"ok": true, "selector": 1, "dpi": 150, "count": 1, "images": [{"page": 1, "bytes": 64210, "base64": "iVBORw0KGgo…"}]}
Failures: 1 PyMuPDF isn't installed, or rasterising the PDF failed — a
SnapshotError; 2 --anchor-id not found; 3 Word busy.
save / save-as PATH / export-pdf PATH (gated)¶
wordlive save [--doc DOC_NAME]
wordlive save-as PATH [--format docx] [--overwrite] [--doc DOC_NAME]
wordlive export-pdf PATH [--pages A-B] [--doc DOC_NAME]
Persist the document or hand back a deliverable. These three verbs are gated:
they only write inside a directory whitelisted with the global --save-dir
(repeatable) or WORDLIVE_SAVE_DIRS (an os.pathsep-separated list). With no
whitelist configured, saving is off (exit 1, PathNotAllowedError). The
target is resolved first (so ../symlinks can't escape) and must then sit
inside an allowed directory. The Python API (doc.save() etc.) is ungated.
savewrites to the document's existing file (fails if it was never saved — usesave-asfirst). Its existing path must itself be whitelisted.save-as PATHwrites a.docx(the only--format; PDF isexport-pdf). Refuses to clobber an existing file unless--overwriteis given.export-pdf PATHexports a PDF — a pixel-faithful render via Word's PDF engine (the same onesnapshotuses), the recommended way to hand back a deliverable.--pages A-B(or a single--pages N) limits the page range; the whole document by default. Overwrites an existing PDF.
$ wordlive --save-dir C:\out save-as C:\out\report.docx
{"ok": true, "path": "C:\\out\\report.docx", "format": "docx"}
$ wordlive --save-dir C:\out export-pdf C:\out\report.pdf
{"ok": true, "path": "C:\\out\\report.pdf"}
$ wordlive save-as C:\elsewhere\x.docx # no whitelist → denied
error: saving is disabled: no save directories are configured …
# exit 1
Failures: 1 policy denial (no/wrong whitelist, refused overwrite) or bad
input; 3 Word busy; 4 Word not running.
Batch operations¶
Run many ops in one atomic-undo round-trip.
exec — --script ops.json or --ops '{…}'¶
Apply a batch of operations under a single atomic-undo scope. This is the most useful command for LLM tool-use: one round-trip per intent, not one per operation.
Provide the batch one of three ways (exactly one of --script / --ops is
required):
--script ops.json— read it from a file.--ops '{"ops": [...]}'— pass the JSON inline on the command line.--ops -— read the JSON from stdin (e.g.… | wordlive exec --ops -), which sidesteps the shell's command-line length limit and is the right choice for large payloads such as inline base64 images.
In every form a bare [...] array is accepted as shorthand for {"ops": [...]},
and malformed JSON returns a clean error (exit 1) rather than a traceback.
Script shape:
{
"label": "Update report",
"ops": [
{"op": "write_bookmark", "name": "Address", "text": "123 Main St"},
{"op": "write_cc", "name": "Signatory", "text": "Jane Doe"},
{"op": "insert_paragraph", "anchor_id": "heading:8", "text": "New risk paragraph.",
"where": "after", "style": "Body Text"},
{"op": "replace", "anchor_id": "heading:3", "text": "Updated section text"}
]
}
Supported ops¶
op |
Required fields | Optional |
|---|---|---|
write_bookmark |
name, text |
— |
write_cc |
name, text |
— |
insert_paragraph |
anchor_id, text |
where (after/before) or before: true, style, bind |
insert_block |
anchor_id, items |
where or before: true, bind |
insert_section |
anchor_id, heading, body |
level, where or before: true, bind |
insert_markdown |
anchor_id, markdown |
where or before: true, bind |
replace_section |
anchor_id, one of body / markdown |
— |
delete_paragraph |
anchor_id |
— |
append |
text |
style |
append_inline |
text |
— |
prepend |
text |
style |
prepend_inline |
text |
— |
insert_image |
anchor_id, wrap, and one of path / base64 |
where or before: true, block, width, height, alt_text, lock_aspect |
insert_equation |
anchor_id, one of unicodemath / latex / mathml |
display, where or before: true |
insert_chart |
anchor_id, kind, data |
title, where or before: true |
format_chart |
anchor_id |
title, legend, legend_position, chart_style, background, plot_background, font, font_size, font_color, data_labels, data_label_format, chart_type, gap_width, overlap, data_table |
format_axis |
anchor_id, which |
title, minimum, maximum, scale, number_format, gridlines |
add_trendline |
anchor_id |
series, kind, display_equation, display_r_squared, forward, backward, order, period |
set_series_color |
anchor_id, color |
series, point |
format_series |
anchor_id |
series, point, marker, marker_size, smooth, explosion, data_labels, data_label_size, data_label_color |
add_error_bars |
anchor_id |
series, kind, amount, include, axis |
set_shape_wrap |
anchor_id |
wrap, side, distance_top, distance_bottom, distance_left, distance_right |
set_shape_position |
anchor_id |
left, top, relative_to |
set_shape_size |
anchor_id |
width, height, lock_aspect |
set_shape_crop |
anchor_id |
left, top, right, bottom (a.k.a. crop_left/crop_top/crop_right/crop_bottom) |
format_shape |
anchor_id |
fill, border, border_weight |
set_shape_alt_text |
anchor_id, text |
— |
set_shape_text |
anchor_id, text |
— |
set_shape_text_frame |
anchor_id |
margin_left, margin_right, margin_top, margin_bottom, word_wrap |
set_shape_rotation |
anchor_id, degrees |
— |
set_shape_z_order |
anchor_id, order |
— |
replace_shape_image |
anchor_id, one of path / base64 |
— |
delete_shape |
anchor_id |
— |
group_shapes |
shapes (a list of ≥2 shape:N ids) |
— |
ungroup_shape |
anchor_id |
— |
set_image_alt_text |
anchor_id, text |
— |
set_image_size |
anchor_id |
width, height, lock_aspect |
set_image_crop |
anchor_id |
left, top, right, bottom (a.k.a. crop_left/crop_top/crop_right/crop_bottom) |
replace |
anchor_id, text |
— |
find_replace |
find, text |
in, all, occurrence, mode, required |
apply_style |
anchor_id, name |
— |
add_style |
name |
type, based_on, next_style |
set_style |
name |
based_on, next_style, plus the format_paragraph / format_run formatting fields |
format_paragraph |
anchor_id |
alignment, left_indent, right_indent, first_line_indent, space_before, space_after, line_spacing (a multiple 1/1.5/2, single/1.5/double, or an exact length like 14pt), page_break_before, keep_together, keep_with_next, widow_control |
format_run |
anchor_id |
bold, italic, underline, strikethrough, font, size, color, highlight, subscript, superscript, small_caps, all_caps, spacing |
regularize |
— | rules, within, profile, dry_run, allow_content |
set_shading |
anchor_id |
fill, pattern |
set_borders |
anchor_id |
sides (all/box/top/bottom/left/right/horizontal/vertical), style (a.k.a. line_style: single/double/dot/dash/none/…), weight, color |
drop_cap |
anchor_id |
position (dropped/normal/margin/none), lines (default 3), distance, font |
add_tab_stop |
anchor_id, position |
align, leader |
set_cell |
table, row, col, text |
— |
add_row |
table |
values |
delete_row |
table, row |
— |
add_column |
table |
values (fill top-to-bottom) |
delete_column |
table, column |
— |
merge_cells |
table, from, to |
— (from/to are [row,col] or "R:C") |
split_cell |
table, cell |
rows (default 1), cols (default 2) (cell is [row,col] or "R:C") |
set_heading_row |
table |
row (default 1), heading (default true), allow_break |
set_table_style |
table, style |
— |
set_table_alignment |
table, alignment |
— |
set_table_borders |
table |
sides, style (a.k.a. line_style), weight, color |
set_table_banding |
table |
first_row, last_row, first_column, last_column, banded_rows, banded_columns |
set_cell_vertical_alignment |
anchor_id (table:N:R:C), align |
— |
autofit_table |
table |
mode (content/window/fixed) |
append_record |
table, record |
— |
update_row |
table, key, values |
column |
create_table |
anchor_id, rows, cols |
style, data (row-major 2-D), header, where or before: true, bind (new cells default to the Normal paragraph style) |
delete_table |
table |
— |
insert_break |
anchor_id |
kind (page/column/section_next/section_continuous), where or before: true |
insert_field |
anchor_id, kind |
text (raw code for kind: field), where or before: true |
update_fields |
— | — |
set_page_setup |
section |
margins, top_margin, bottom_margin, left_margin, right_margin, gutter, orientation, paper_size, columns, column_spacing |
insert_footnote |
anchor_id, text |
where or before: true |
insert_endnote |
anchor_id, text |
where or before: true |
insert_toc |
anchor_id |
levels ([upper, lower]), use_heading_styles, hyperlinks, where or before: true |
mark_index_entry |
anchor_id, entry |
cross_reference, bold, italic |
insert_index |
anchor_id |
columns, run_in, right_align_page_numbers, where or before: true |
insert_table_of_figures |
anchor_id |
label, include_label, hyperlinks, right_align_page_numbers, where or before: true |
set_bibliography_style |
style |
— |
add_source |
source_type |
tag, author, title, year, publisher, city, journal_name, volume, issue, pages, url, edition, doi, or raw xml |
insert_citation |
anchor_id, tag |
pages, prefix, suffix, volume, suppress_author, suppress_year, suppress_title, locale, where or before: true |
insert_bibliography |
anchor_id |
where or before: true |
mark_citation |
anchor_id, long_citation |
short_citation, category, where or before: true |
insert_table_of_authorities |
anchor_id |
category, passim, keep_entry_formatting, entry_separator, page_range_separator, where or before: true |
add_bookmark |
name, anchor_id |
— |
pin |
anchor_id |
name (a readable slug) |
pin_outline |
— | levels (an [lo, hi] band) |
add_hyperlink |
anchor_id, and one of url / bookmark |
text, screen_tip |
set_hyperlink |
index (1-based, from hyperlinks) |
address, sub_address, text, screen_tip (pass ≥1; sub_address/screen_tip clear with "", address/text can't be emptied) |
insert_cross_reference |
anchor_id, target |
kind (text/page/number/above_below), hyperlink, where or before: true |
insert_caption |
anchor_id |
label, text, position (above/below; default above for Table, else below) |
create_content_control |
anchor_id |
kind, title, tag, items, where (wrap/before/after), lock_contents, lock_control |
set_cc_properties |
anchor_id (cc:NAME) |
title, tag, lock_contents, lock_control (pass ≥1; "" clears title/tag) |
set_cc_items |
anchor_id (cc:NAME), items |
— (replaces the combo_box/dropdown choice list) |
apply_theme |
theme |
— |
set_theme_colors |
— | scheme, colors |
set_theme_fonts |
— | scheme, major, minor |
set_property |
name, value |
custom |
delete_property |
name |
— |
set_variable |
name, value |
— |
delete_variable |
name |
— |
add_comment |
anchor_id, text |
author |
resolve_comment |
index |
— |
delete_comment |
index |
— |
accept_revision |
index |
— |
reject_revision |
index |
— |
accept_all_revisions |
— | anchor_id (scope to its range) |
reject_all_revisions |
— | anchor_id |
set_watermark |
text |
font, color, layout, semitransparent |
remove_watermark |
— | — |
insert_text_box |
anchor_id, text |
width, height, wrap, font, size, bold, italic, alignment, fill, border, before/after |
apply_list |
anchor_id |
type (bulleted/numbered/outline), continue |
apply_list_format |
anchor_id, levels |
continue (levels = per-level specs; see list format) |
remove_list |
anchor_id |
— |
restart_numbering |
anchor_id |
— |
indent_list |
anchor_id |
— |
outdent_list |
anchor_id |
— |
write_header |
section, text |
which (primary/first/even) |
write_footer |
section, text |
which |
The find_replace op mirrors wordlive replace --find … — fuzzy whitespace
+ smart-quote match, optional in anchor to scope it, and either all or
occurrence to handle multi-match. mode selects fuzzy (default) / literal
(exact) / regex (Python; text may use \1 backreferences, expanded per
match — {"op":"find_replace","find":"(\\d+)-(\\d+)","text":"\\1–\\2","mode":"regex","all":true}).
required: false makes a zero-match op a no-op instead of failing the batch (the
linter's typography autofixes rely on this). Ambiguous-match failures surface in
the batch response's failure.matches so the LLM can rewrite the op and retry. To
edit text inside a table, scope the replace to the cell anchor
("in": "table:N:R:C"); a match resolved through a whole-document scope that
can't be verified raises a replace_verification failure rather than risk
overwriting the wrong cell.
insert_paragraph mirrors the insert command: a new paragraph relative to
any anchor, with placement defaulting to after and an optional style that's
validated before the batch mutates anything. Placement accepts either the
verbose "where": "before"|"after" or the boolean "before": true — the latter
mirrors the command's --before/--after flags, so the same intent encodes the
same way whether you type it or batch it. (insert_image accepts both forms too.)
append adds a new final paragraph at the very end of the document
(optional style, validated first) — no anchor to resolve, equivalent to an
insert_paragraph op targeting the end anchor. append_inline instead
continues the document's last paragraph and takes text only (no style).
prepend / prepend_inline are their start-of-document mirrors (the start
anchor). append_paragraph / prepend_paragraph remain as explicit synonyms
of append / prepend.
Any field an op doesn't recognise (a typo, or a style handed to an inline
append) is reported in a top-level warnings array on the batch result — the op
still runs, but the ignored field is surfaced rather than silently dropped, so a
successful-looking response can't mask a payload you got wrong.
insert_image mirrors insert-image. Supply the image with either a path
(read from disk) or base64 (inline data — the natural choice in a JSON op,
with no command-line length limit). wrap is required; the optional fields
match the command's flags, including block (place the image on its own new
Normal line instead of in the anchor's text run). A bad image source surfaces
as the batch's failure with type: "ImageSourceError".
apply_style and format_paragraph are the same as their dedicated CLI
verbs — the style must already exist in the document, indent and spacing
values are in points, alignment is one of left/center/right/justify.
format_paragraph's page_break_before (a bool) forces or clears a
reflow-safe page break before the paragraph — the clean way to page-break a
style without a stray break character. The keep_together, keep_with_next,
and widow_control bools are the matching pagination controls (keep a
paragraph's lines together, keep it with the next paragraph, suppress widows/
orphans).
regularize runs the regularize command inside the batch: it
applies the fixable lint findings (selected by the optional rules / within
fields) as one step of the batch's atomic undo, and returns its
{applied, skipped, deferred, findings} report in the batch outputs. With
dry_run: true it plans without writing — the natural lead-off op when you want
to audit-then-fix in a single round-trip. Content-changing fixes are held in
deferred unless you pass allow_content: true.
set_cell, add_row, delete_row, add_column, delete_column,
merge_cells, split_cell, and set_heading_row operate on tables by 1-based
table index. set_cell is shorthand for a replace on a table:N:R:C anchor;
add_row's optional values is a JSON array matched to columns, add_column's
fills top-to-bottom; merge_cells/split_cell take cell coordinates as
[row, col] or "R:C" and leave the table non-uniform (table:N:R:C then
indexes physical cells); delete_column fails cleanly on a merged / mixed-width
table (delete those cells via table:N:R:C); set_heading_row marks a row
(default 1) as a repeating header across pages. All the table ops join the same
atomic-undo scope as the rest of the batch.
create_table builds a new table at a position anchor_id (heading:,
para:, start, end, range: — not a bare table:N); delete_table
removes one by 1-based table index. create_table's data is a row-major 2-D
array validated against rows×cols before the batch mutates anything,
style defaults to Table Grid, and header bolds the first row. Because a
successful batch reports structure it created, the response carries an
outputs array — [{"index": <op index>, "op": "create_table", "table": N,
"rows": R, "columns": C}] — so a later op (or a follow-up call) can address the
new table by its reported index. Filling the whole grid through data in the
create op keeps it one atomic undo and avoids a set_cell storm.
insert_break mirrors the insert-break command — an explicit page, column,
or section break at any anchor_id. kind defaults to page; placement
accepts the same where / before forms as the other insert ops. For a
reflow-safe page break tied to a paragraph (rather than a one-off mark), use a
format_paragraph op with page_break_before instead.
insert_field inserts a self-updating field (kind = page / numpages /
date / time / filename / author / title, or field with a raw code in
text) at an anchor_id — put page numbers in a footer:S:WHICH. update_fields
(no args) recomputes the document's fields. set_page_setup mirrors page-setup,
taking a 1-based section plus any of margins, the per-side *_margin fields,
gutter, orientation, paper_size, columns, and column_spacing.
insert_footnote / insert_endnote attach a note to an anchor_id and report
the new footnote:N / endnote:N in the batch's outputs. insert_toc inserts
a table of contents (levels is a [upper, lower] pair, default [1, 3]);
follow it with an update_fields op so its page numbers populate.
add_bookmark names a range (the prerequisite for the rest). pin plants a
durable handle on an anchor_id and returns its pin:CODE (random, or a
readable name slug); pin_outline pins every heading at once, returning the
{heading:N: pin:CODE} map (idempotent; levels restricts the band). These are
the batch-side mirror of the pin / pin-outline verbs — reach for them when a
positional id would renumber mid-batch.
Durable handles in a batch. Add bind: "slug" (or bind: true for a random
code) to an insert / insert_block / insert_section / insert_markdown /
create_table op to mint a pin: on the new content — it comes back as pin in
that op's outputs entry. And any op field of the exact form $ops[N].field is
replaced with an earlier op's recorded output before the op runs, so a batch can
create then target without a round-trip — e.g. create_table at op 0, then
{"op": "set_cell", "table": "$ops[0].table", …}. A forward / unknown reference
fails that op (and the batch) cleanly.
add_hyperlink
links an anchor_id to exactly one of url (external) or bookmark (internal),
with optional text. insert_cross_reference references a target anchor id
(bookmark: / heading: / footnote: / endnote:) — an unresolvable target is
an anchor_not_found failure. insert_caption adds an auto-numbered caption in
its own Caption-styled paragraph (label defaults to Figure); position
overrides the default placement (above for a Table, below otherwise), and on a
table:N:R:C anchor the caption attaches to the whole table. Refresh
cross-reference page numbers with an update_fields op after the document
settles.
set_hyperlink retargets / relabels an existing link in place (addressed by its
1-based index from the hyperlinks reader) rather than delete-and-reinsert —
pass any of address / sub_address / text / screen_tip. It retargets, it
doesn't unlink: sub_address / screen_tip clear with "", but address /
text can't be emptied (Word keeps a link pointing somewhere).
Likewise set_cc_properties re-sets a content control's title / tag /
lock_contents / lock_control, and set_cc_items replaces a combo_box /
dropdown's choice list — both addressed by the control's cc:NAME anchor.
add_comment, resolve_comment, and delete_comment mirror the comment
verbs — add_comment attaches a side-channel annotation to an anchor_id
without touching the text, while resolve_comment / delete_comment take a
1-based index. Since deletes re-index, ordering matters within a batch.
accept_revision / reject_revision resolve the tracked change at a 1-based
index; accept_all_revisions / reject_all_revisions resolve them all
(optionally scoped to an anchor_id's range). Like comment deletes, accepting /
rejecting re-indexes the rest, so order within a batch matters — the bulk ops are
safer when resolving several. set_watermark / remove_watermark stamp or clear
a text watermark behind every page, and insert_text_box drops a floating pull
quote at an anchor_id (these mirror the watermark / insert-text-box verbs).
apply_list, apply_list_format, remove_list, restart_numbering,
indent_list, and outdent_list mirror the list verbs — all take an
anchor_id, and apply_list's optional type defaults to bulleted.
apply_list_format takes a levels array of per-level specs (the list format
verb) to author a custom numbered/bulleted list. write_header /
write_footer set a section's header/footer by 1-based section index, with
an optional which (primary / first / even, default primary) — handy
for stamping a client name or page footer across a generated document in the
same atomic-undo batch as the body edits.
Recording the batch as tracked changes¶
Set "tracked": true at the top level of the script to flip Word's Track
Changes on for the whole batch and restore the prior setting when it finishes —
so the user sees every op as an accept/reject-able revision under one Ctrl-Z:
{
"label": "Suggest rewordings",
"tracked": true,
"ops": [
{"op": "find_replace", "find": "utilise", "text": "use", "all": true}
]
}
Behaviour on partial failure¶
If any op fails, the entire scope's UndoRecord still closes cleanly — but
operations before the failure have already been applied. The user can
roll the whole batch back with one Ctrl-Z. The response reports the failure
point precisely so an LLM can retry with a corrected payload:
{
"ok": false,
"ops_run": 2,
"label": "Update report",
"failure": {
"index": 2,
"op": {"op": "insert_paragraph", "anchor_id": "heading:99", "text": "…"},
"error": "heading not found: 'heading:99'",
"type": "AnchorNotFoundError"
}
}
The exit code reflects the first failed op: 2 for anchor-not-found,
3 for Word-busy, etc. — so an LLM's retry policy can branch on it.
Example invocation¶
Tooling & help¶
The about screen, the bundled LLM skill, installers, and a worked example.
about¶
--about/-A prints a colourful banner with the package version, author
(Tom Villani, Ph.D.), license (MIT), and the repository URL, then exits.
On a terminal the "word" half is blue and the "live" half a lighter cyan; piped
or redirected, the ANSI is stripped to clean ASCII. --version/-v prints just
wordlive <version>.
Both are eager top-level flags — no subcommand needed and Word is never touched.
$ wordlive --about
_ _ _
| | (_)
__ _____ _ __ __| | |___ _____
\ \ /\ / / _ \| '__/ _` | | \ \ / / _ \
\ V V / (_) | | | (_| | | |\ V / __/
\_/\_/ \___/|_| \__,_|_|_| \_/ \___|
Drive a running Microsoft Word instance with LLM agents
version 0.16.0
author Tom Villani, Ph.D.
license MIT
repo https://github.com/thomas-villani/wordlive
llm-help¶
Print a full agent guide — a bundled SKILL.md — to stdout: the anchor
model, every verb, image insertion, the exec batch format, and the exit-code
taxonomy. wordlive --help points an agent straight here, so a model can get
everything it needs in one call without an install step. Defaults to the CLI
guide; --python prints the Python-API (import wordlive as wl) guide
instead.
Unlike every other command, the output is raw Markdown rather than JSON (and is
unaffected by --json/--text) — it's documentation, exactly like --help
itself, meant to read cleanly into a model's context. The YAML frontmatter that
fronts the installed skill is stripped. Offline: it never touches Word.
$ wordlive llm-help
# wordlive (CLI)
`wordlive` drives a **running** Microsoft Word instance over COM (Windows only).
...
This is the same content install-skill writes to disk; reach
for llm-help when you just want it in context now, and install-skill when you
want coding tools to discover it on their own.
install-skill¶
Install wordlive's bundled agent skills so LLM coding tools can pick up how
to drive it. wordlive ships two: wordlive-cli (the command-line workflow) and
wordlive-python (the import wordlive as wl API). By default only the CLI
skill is installed; pass --python for just the Python one, or --both for
both. They land under the current project at ./.agents/skills/<name>/SKILL.md
(or ~/.agents/skills/<name>/ with --system). Offline — it never touches
Word, and refuses to clobber an existing file unless you pass --force.
$ wordlive install-skill
{"ok": true, "scope": "local", "installed": [
{"kind": "cli", "name": "wordlive-cli", "path": ".../.agents/skills/wordlive-cli/SKILL.md", "bytes": 6172}]}
$ wordlive install-skill --both
{"ok": true, "scope": "local", "installed": [
{"kind": "cli", "name": "wordlive-cli", "path": ".../.agents/skills/wordlive-cli/SKILL.md", "bytes": 6172},
{"kind": "python", "name": "wordlive-python", "path": ".../.agents/skills/wordlive-python/SKILL.md", "bytes": 7460}]}
$ wordlive install-skill --python --system --force
{"ok": true, "scope": "system", "installed": [
{"kind": "python", "name": "wordlive-python", "path": "/home/you/.agents/skills/wordlive-python/SKILL.md", "bytes": 7460}]}
Failures: 1 if a target can't be written (every target is checked up front, so
a missing --force fails before anything is written).
install-mcp¶
wordlive install-mcp [--client claude-desktop|claude-code] [--name NAME]
[--directory DIR] [--config PATH] [--print] [--force]
Register wordlive's MCP server in an agent's config so a client can
drive your open document. It merges an mcpServers.<name> entry (default name
wordlive) that launches the stdio server with
uvx --from "wordlive[mcp,snapshot]" wordlive-mcp — no separate install step,
and the snapshot extra enables the vision tool. Offline: it only edits config,
never touches Word — restart the client to load the change.
--client claude-desktop(default) writes the OS-specificclaude_desktop_config.json;--client claude-codewrites a project-local./.mcp.json.--directory DIRregisters a local checkout viauv run --directory DIR wordlive-mcp(for development) instead of the PyPIuvxform.--config PATHtargets a specific config file;--printjust emits the JSON snippet (writing nothing) so you can paste it into any client.- It refuses to overwrite an existing server entry unless you pass
--force.
$ wordlive install-mcp --print
{
"mcpServers": {
"wordlive": {
"command": "uvx",
"args": ["--from", "wordlive[mcp,snapshot]", "wordlive-mcp"]
}
}
}
$ wordlive install-mcp
{"ok": true, "client": "claude-desktop", "path": ".../Claude/claude_desktop_config.json",
"server": "wordlive", "action": "created", "entry": {"command": "uvx", "args": ["--from", "wordlive[mcp,snapshot]", "wordlive-mcp"]}}
Failures: 1 if the config can't be read/written, isn't a JSON object, or the
server entry already exists without --force. For the bundle (.mcpb) and a
full tool reference, see the MCP server page.
LLM tool-use example¶
A typical agent loop looks like:
# 1. Discover what's addressable.
outline = json.loads(run(["wordlive", "outline"]))
# 2. Model picks an anchor and a new value, returns:
# {"anchor_id": "heading:3", "text": "Revised context section"}
# 3. Apply.
result = run(["wordlive", "replace",
"--anchor-id", anchor_id,
"--text", text])
# 4. Branch on exit code.
if result.returncode == 2: # anchor not found — re-fetch outline
...
elif result.returncode == 3: # Word busy — back off and retry
...
For multi-step intents, batch into one exec --script ops.json call instead.
See the Cookbook for full worked examples.