extract_word
Extract text, metadata, or a structured representation from a Word (.docx) document.
Parameters
- pathstringone of
pathorsource_key - File path to the
.docxfile; supports${ctx.key}interpolation. - source_keystringone of
pathorsource_key - Context key whose value is the file path (must be a string).
- formatstringdefault "text"
- Output format:
"text"for plain text,"markdown"for Markdown with headings/lists/inline formatting,"json"for a structured block IR (see below). - output_keystringdefault "content"
- Context key where the extracted output is stored. For
text/markdownthe value is a string; forjsonit is an object. - metadata_keystring
- If set, document metadata (Dublin Core fields) is stored under this context key.
- comments_keystring
- If set, document comments (from
word/comments.xml) are stored under this context key as an array. See [Comments](#comments).
Providing both
pathandsource_keyis an error. Theformatparameter accepts"text","markdown", or"json"; any other value is rejected.
Context Output
<output_key>(defaultcontent) — the extracted document text, Markdown, or JSON IR.<metadata_key>(only whenmetadata_keyis set) — an object with available fields:title,author,subject,description,keywords,last_modified_by,created,modified,revision,category.
JSON format
When format = "json", the output is an object with a single blocks array. Each block is either a paragraph or a table, in source document order. This shape is designed for downstream LLM extraction with response_format = json_schema — it preserves run-level styling, colors (including resolved theme colors), and table structure, all of which are commonly load-bearing semantics in real-world documents (e.g. color-coded moderator instructions in market-research discussion guides).
Paragraph block
{
"type": "paragraph",
"index": 0,
"style": "Heading1",
"list": { "level": 0, "numbered": true },
"colors": ["0066FF"],
"runs": [
{ "text": "MODERATOR SAY: ", "bold": true, "color": "0066FF" },
{ "text": "Thank you for joining today.", "color": "0066FF" }
],
"text": "MODERATOR SAY: Thank you for joining today."
}
Run flags (bold, italic, underline, strike) are emitted only when true. color is the resolved hex (uppercase, no #) — see below. highlight is the OOXML highlight name when set (e.g. "yellow"). style and list are absent when not applicable. colors is a deduped union of run colors at the paragraph level, present only when at least one run has a color.
Table block
{
"type": "table",
"index": 1,
"rows": [
{ "cells": [
{ "paragraphs": [ /* paragraph blocks as above */ ] },
{ "paragraphs": [ /* ... */ ] }
] }
]
}
Tables nested inside cells are flattened — inner paragraphs are appended to the surrounding cell's paragraph list; no nested table block is emitted.
Comments
When comments_key is set, the node also parses word/comments.xml (if present) and walks word/document.xml for <w:commentRangeStart/End> markers to capture the source text each comment is anchored to.
Shape of ctx[comments_key]:
[
{
"id": "1",
"author": "Jane Reviewer",
"initials": "JR",
"date": "2026-03-15T10:30:00Z",
"text": "Reword this — too colloquial.",
"anchored_text": "quick brown fox"
}
]
anchored_text is the verbatim text the comment was attached to (joined run text between the comment's <w:commentRangeStart/> and <w:commentRangeEnd/> markers). It is omitted when the comment has no anchored range (whole-document comments).
If the document has no comments part, an empty array is written.
flow:step("extract", nodes.extract_word({
path = "${ctx.source}",
format = "json",
output_key = "doc",
comments_key = "comments"
}))
Color resolution
Run color is captured from w:color:
- Explicit hex (
w:val="0066FF") is captured as uppercase hex. w:val="auto"is dropped (no color field).w:themeColoris resolved againstword/theme/theme1.xml: theme namesdark1,light1,dark2,light2,accent1...accent6,hyperlink,followedHyperlinkare mapped to their concretesrgbClr/sysClrhex values from the document's color scheme.themeShade/themeTintadjustments are not currently applied.
Examples
Markdown extraction (default)
local flow = Flow.new("read_word_doc")
flow:step("extract", nodes.extract_word({
path = "/data/report.docx",
format = "markdown",
output_key = "doc_content",
metadata_key = "doc_meta"
}))
flow:step("done", nodes.log({
message = "Author: ${ctx.doc_meta.author}, Content: ${ctx.doc_content}"
})):depends_on("extract")
return flow
JSON IR for LLM-driven extraction
local flow = Flow.new("guide_to_json")
flow:step("extract", nodes.extract_word({
path = "${ctx.source_path}",
format = "json",
output_key = "doc"
}))
flow:step("analyze", nodes.llm({
provider = "openai",
model = "gpt-5-mini",
prompt = "Convert the following discussion guide IR into the project schema. Use color hints (0066FF = MODERATOR SAY, 037C72 = MODERATOR NOTE, etc.). IR: ${ctx.doc}",
output_key = "guide",
extra = {
response_format = {
type = "json_schema",
json_schema = {
name = "discussion_guide",
strict = true,
schema = {
-- your guide schema here
}
}
}
}
})):depends_on("extract")
return flow