Editorial Report (.docx)
You will generate one .docx file via python-docx by writing a Python
program and running it through the execute_python_code tool. The blocks
below are parts of that one program, not separate runs -- later ones use the
doc, palette and helpers the earlier ones define, so assemble them into a
single script before executing. Save to
the workspace, then report the path + a 1-line content summary.
📦 Required runtime packages
The sandboxed execute_python_code ships with pandas, numpy,
matplotlib, openpyxl, and python-docx>=1.1.0 preinstalled — no
extra installation step is needed. The import name is docx, not
python_docx:
from docx import Document # ✅ correct
import python_docx # ❌ ModuleNotFoundError
Note:
execute_python_codeaccepts onlycodeandcapture_outputarguments; there is nopackagesparameter. All required libraries are already available in the sandbox image.
💾 Saving and reporting the file
execute_python_code runs with the task's output directory as the working
directory, so save with a plain filename — no /workspace, no nested
output/, no BytesIO round-trip:
doc.save("market_expansion_report.docx")
Then verify content, not size: an empty Document() is already ~36 KB, so a
byte threshold proves nothing. Re-open and check what you wrote.
from docx import Document
check = Document("market_expansion_report.docx")
# Cell text is not in check.paragraphs, so a table-only document would
# otherwise look empty and you would report a failure that did not happen.
has_text = any(p.text.strip() for p in check.paragraphs) or any(
cell.text.strip()
for tbl in check.tables
for row in tbl.rows
for cell in row.cells
)
assert has_text, "document has no text"
Assert the shape the request asked for, never a fixed minimum — a multi-section report can also check its tables and headings, but a one-page memo has neither, and padding one to clear a threshold breaks rule 5.
🔗 Make it clickable — REQUIRED
The executor response carries a markdown_link for each file it wrote (in
file_refs[]). Use that string verbatim as the first line of your answer,
as bare markdown:
[market_expansion_report.docx](file:20fae785-3823-4906-b385-d0e8a7807dc8)
Never fabricate the UUID, never wrap the link in backticks (it stops rendering
as a chip), and never restate it as a file_id: field. get_file_info() does
not return one — the reference is already on the executor result.
⚠️ Hard rules — NO exceptions
- MATCH THE USER'S LANGUAGE. If the prompt is Chinese (中文), ALL
document text (cover kicker, headings, body, table headers, captions,
footer) must be in Chinese. Translate template phrases like
EXECUTIVE SUMMARY→摘要,FINDINGS→调查结果,RECOMMENDATIONS→建议,As of YYYY-MM-DD→截至 YYYY-MM-DD. Never leave English kickers in a Chinese report. - One palette only. Pick one of the 5 palettes below; use only its
4 hex values (
ink,paper,paper_tint,ink_tint). Define apalette = {...}dict ONCE at the top of the script and referencepalette["ink"]etc. everywhere — do not copy literal hex values into individual styling calls. - Two fonts only. Headings =
Georgia(serif, present on all OSes). Body =Calibri(sans, Word default). No custom fonts — recipients won't have them and Word falls back to Times. For Chinese documents: both render Chinese via system fallback (PingFang on macOS, Microsoft YaHei on Windows) — do not switch fonts. - Use real Word styles, not manual formatting. Headings must use the
built-in
Heading 1/Heading 2/Heading 3styles so Word's navigation pane and auto table-of-contents work. A document where every heading is just bold 18pt body text is broken — it has no outline. - Forbidden:
- WordArt, drop shadows, glow, 3-D effects, gradient fills
- clipart, emoji as decoration, stock-photo placeholders
- centered body paragraphs (left-align / justify only)
- all-caps body text (kickers and labels only)
- Comic Sans, Arial Black, Times New Roman as a deliberate choice
- Real content only. No lorem ipsum, no
[Title here]placeholders, no fabricated statistics, no fake citations. If a section has no user data, drop the section rather than padding it. - Failure honesty — NEVER fake the deliverable.
- If
execute_python_coderaises after multiple retries, STOP and report the actual error. Do not write a stub file likewrite_file("report.docx", "placeholder")to make the chip appear. - The final answer must reflect what was actually written. Do not
describe sections or tables that aren't in the saved
.docx.
- If
🎨 Palettes — pick ONE
Same palettes as pdf-report-editorial and pptx-editorial — keep the
editorial family visually consistent. Each: ink (body text + rules),
paper (text on an ink band), paper_tint (table band + callout bg),
ink_tint (kickers, captions, footer). The keys are underscored — the dict
below is what every snippet indexes into.
- Monocle (default / business / tech / policy)
ink
0A0A0B· paperF1EFEA· paper_tintE8E5DE· ink_tint18181A - Indigo Porcelain (research / data-heavy)
ink
0A1F3D· paperF1F3F5· paper_tintE4E8EC· ink_tint152A4A - Forest Ink (sustainability / impact)
ink
1A2E1F· paperF5F1E8· paper_tintECE7DA· ink_tint253D2C - Kraft Paper (humanities / qualitative)
ink
2A1E13· paperEEDFC7· paper_tintE0D0B6· ink_tint3A2A1D - Dune (art / design / fashion criticism)
ink
1F1A14· paperF0E6D2· paper_tintE3D7BF· ink_tint2D2620
Define it once at the top of the script, then index it everywhere:
palette = {"ink": "0A0A0B", "paper": "F1EFEA",
"paper_tint": "E8E5DE", "ink_tint": "18181A"} # Monocle
python-docx takes hex without the # prefix, via
RGBColor.from_string(palette["ink"]).
✒️ Typography (python-docx Pt sizes)
| Role | Style | Family | Size | Weight |
|---|---|---|---|---|
| Cover title | Title |
Georgia | 40pt | regular |
| Cover subtitle / dek | body run | Calibri | 14pt | italic |
| Kicker (small caps label) | body run | Calibri | 9pt, uppercase | bold, ink_tint |
| H1 section | Heading 1 |
Georgia | 22pt | regular |
| H2 subsection | Heading 2 |
Georgia | 16pt | regular |
| H3 sub-subsection | Heading 3 |
Calibri | 12pt | bold |
| Body paragraph | Normal |
Calibri | 11pt | regular, line 1.4 |
| Pull quote | Intense Quote |
Georgia | 14pt | italic |
| Table header | table run | Calibri | 10pt | bold, paper on ink |
| Table body | table run | Calibri | 10pt | regular |
| Caption / footnote | Caption |
Calibri | 9pt | italic, ink_tint |
📐 Page setup
Set margins, size and orientation on each section before adding content.
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.section import WD_ORIENT
doc = Document()
sec = doc.sections[0]
# A4 portrait with editorial margins
sec.page_width, sec.page_height = Cm(21.0), Cm(29.7)
sec.orientation = WD_ORIENT.PORTRAIT
sec.top_margin, sec.bottom_margin = Cm(2.5), Cm(2.5)
sec.left_margin, sec.right_margin = Cm(2.2), Cm(2.2)
⚠️ Orientation gotcha: setting sec.orientation = WD_ORIENT.LANDSCAPE
does NOT swap the page dimensions — Word reads the width/height, not the
flag. You must swap them yourself:
sec.orientation = WD_ORIENT.LANDSCAPE
sec.page_width, sec.page_height = Cm(29.7), Cm(21.0) # swap explicitly
Set the default body font once on the Normal style so every paragraph
inherits it:
from docx.shared import Pt, RGBColor
normal = doc.styles["Normal"]
normal.font.name = "Calibri"
normal.font.size = Pt(11)
normal.font.color.rgb = RGBColor.from_string(palette["ink"])
normal.paragraph_format.line_spacing = 1.4
normal.paragraph_format.space_after = Pt(8)
Do not pin w:eastAsia to a named CJK face. Rule 2 is that Chinese renders
through each platform's own fallback — writing Microsoft YaHei into the
style hard-codes a font that is absent on macOS and Linux, which is the
substitution the rule exists to prevent. Leaving the attribute unset is what
lets Word pick PingFang, YaHei or Noto per platform.
🏛️ Cover page pattern
The cover is its own section, so the body starts on a fresh page and can later be given its own header, footer or page numbering without touching the cover. Structure: kicker → title → dek → meta row.
from docx.oxml.ns import qn
from docx.shared import Pt, RGBColor
def strip_style_border(doc, style_name):
"""Drop a built-in style's decorative border.
The border lives on the *style*, not on paragraphs using it, so clearing
a paragraph's own pPr does nothing -- it never had one. Title and Intense
Quote both ship an accent-blue w:pBdr that is in none of the palettes.
"""
style_pPr = doc.styles[style_name].element.get_or_add_pPr()
for bdr in style_pPr.findall(qn("w:pBdr")):
style_pPr.remove(bdr)
def add_kicker(doc, text):
p = doc.add_paragraph()
run = p.add_run(text.upper())
run.font.name, run.font.size, run.font.bold = "Calibri", Pt(9), True
run.font.color.rgb = RGBColor.from_string(palette["ink_tint"])
return p
# --- cover ---
doc.add_paragraph().paragraph_format.space_after = Pt(160) # vertical push
add_kicker(doc, "Research Brief")
# Title and Intense Quote ship an accent-blue border, and Intense Quote and
# Caption default to bold. None of that is in the palette. Clearing the border
# once on the style covers every paragraph that uses it.
strip_style_border(doc, "Title")
strip_style_border(doc, "Intense Quote")
title = doc.add_paragraph(style="Title")
title_run = title.add_run("Agent Infrastructure in 2026")
title_run.font.name, title_run.font.size = "Georgia", Pt(40)
title_run.font.color.rgb = RGBColor.from_string(palette["ink"])
dek = doc.add_paragraph()
dek_run = dek.add_run("Market shape, adoption curves, and the shift to "
"production-grade systems.")
dek_run.font.name, dek_run.font.size, dek_run.font.italic = "Calibri", Pt(14), True
meta = doc.add_paragraph()
meta_run = meta.add_run("Xagent Team · 2026-05-14") # middle dot, not hyphen
meta_run.font.size = Pt(10)
meta_run.font.color.rgb = RGBColor.from_string(palette["ink_tint"])
# A new section, not just a page break: the body gets its own section
# properties, so a header, footer or page numbering added later applies to
# it without touching the cover.
from docx.enum.section import WD_SECTION
doc.add_section(WD_SECTION.NEW_PAGE)
🔠 Heading hierarchy
Always use the built-in styles, then restyle the style object once — not each heading individually:
from docx.oxml.ns import qn
from docx.shared import Pt, RGBColor
def use_font(style, family):
"""Set a style's font and drop the template's theme fonts.
`.font.name` only adds w:ascii/w:hAnsi. The default headings also carry
w:asciiTheme="majorHAnsi" etc., and Word prefers the theme attributes --
so without this the headings stay in the template font, not Georgia.
"""
style.font.name = family
rpr = style.element.get_or_add_rPr()
rfonts = rpr.get_or_add_rFonts()
for theme in ("asciiTheme", "hAnsiTheme", "eastAsiaTheme", "cstheme"):
rfonts.attrib.pop(qn(f"w:{theme}"), None)
for name, family, size in (("Heading 1", "Georgia", 22),
("Heading 2", "Georgia", 16),
("Heading 3", "Calibri", 12)):
st = doc.styles[name]
use_font(st, family)
st.font.size = Pt(size)
st.font.color.rgb = RGBColor.from_string(palette["ink"])
st.font.bold = (name == "Heading 3")
st.paragraph_format.space_before = Pt(18)
st.paragraph_format.space_after = Pt(6)
doc.add_heading("Executive Summary", level=1)
doc.add_paragraph("...")
doc.add_heading("Market Size", level=2)
⚠️ doc.add_heading(..., level=0) applies the Title style, not a
heading — use it only on the cover. Body sections start at level=1.
Pull quotes use the built-in style too, so they stay in the outline-free body flow:
quote = doc.add_paragraph(style="Intense Quote")
quote_run = quote.add_run("Adoption is no longer the constraint; "
"operating cost is.")
quote_run.font.name, quote_run.font.size = "Georgia", Pt(14)
quote_run.font.italic = True
quote_run.font.bold = False # Intense Quote defaults to bold
quote_run.font.color.rgb = RGBColor.from_string(palette["ink"])
📊 Table styling
python-docx has no API for cell shading or borders, so both need raw OOXML. These two helpers are the whole toolkit — copy them verbatim.
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
# Both w:tcPr and w:tcBorders are ordered sequences: Word rejects the part when
# children appear out of order, and each tag may appear at most once. Appending
# is wrong on both counts, so everything below inserts at the schema position.
# Relative order from the OOXML schema. vAlign is here even though no recipe
# below sets it: it sorts after shd, so if a caller centres a cell before
# shading it, _put has to see vAlign to insert ahead of rather than after it.
_TCPR_ORDER = ("tcBorders", "shd", "vAlign")
_BORDER_ORDER = ("top", "left", "bottom", "right")
def _hex6(value):
"""Reject what Word cannot read. python-docx writes these attributes
verbatim and save() never validates, so a bad value only surfaces as
Word's "unreadable content" repair prompt when the user opens the file.
RGBColor.from_string already refuses the same mistake."""
text = str(value)
if len(text) != 6 or any(c not in "0123456789abcdefABCDEF" for c in text):
raise ValueError(f"expected 6 hex digits without '#', got {value!r}")
return text
def _put(parent, tag, order):
"""Replace parent's <w:{tag}> child, keeping the schema's element order."""
for stale in parent.findall(qn(f"w:{tag}")):
parent.remove(stale)
if tag not in order:
raise ValueError(f"unknown element {tag!r}; expected one of {order}")
el = OxmlElement(f"w:{tag}")
rank = order.index(tag)
later = [child for child in parent
if isinstance(child.tag, str)
and child.tag.rsplit("}", 1)[-1] in order
and order.index(child.tag.rsplit("}", 1)[-1]) > rank]
if later:
later[0].addprevious(el)
else:
parent.append(el)
return el
def shade_cell(cell, hex_fill):
"""Solid background fill for one table cell."""
hex_fill = _hex6(hex_fill)
tcPr = cell._tc.get_or_add_tcPr()
shd = _put(tcPr, "shd", _TCPR_ORDER)
shd.set(qn("w:val"), "clear")
shd.set(qn("w:color"), "auto")
shd.set(qn("w:fill"), hex_fill)
def set_row_border(row, edge, hex_color, sz=8):
"""Hairline on one edge of every cell in a row, e.g. 'top' or 'bottom'.
sz is in 1/8 pt. Re-styling the same edge replaces it."""
hex_color = _hex6(hex_color)
if not isinstance(sz, int) or isinstance(sz, bool) or sz <= 0:
raise ValueError(f"sz must be a positive integer, got {sz!r}")
for cell in row.cells:
tcPr = cell._tc.get_or_add_tcPr()
borders = tcPr.find(qn("w:tcBorders"))
if borders is None:
borders = _put(tcPr, "tcBorders", _TCPR_ORDER)
el = _put(borders, edge, _BORDER_ORDER)
el.set(qn("w:val"), "single")
el.set(qn("w:sz"), str(sz)) # 8 = 1pt
el.set(qn("w:color"), hex_color)
Editorial table rules — horizontal rules only, no vertical borders:
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.shared import Pt, RGBColor
rows = [("Region", "Revenue", "YoY"),
("North America", "4.2M", "+18%"),
("EMEA", "2.8M", "+11%")]
# No table style: the stock "Table Grid" draws all six edges in black
# (w:color="auto"), which is not a palette value, and nilling the verticals
# per cell still leaves its black top/bottom/insideH rules behind. An
# unstyled table starts with no borders at all, so the only rules on it are
# the ones drawn below.
table = doc.add_table(rows=len(rows), cols=len(rows[0]))
table.alignment = WD_TABLE_ALIGNMENT.CENTER
table.autofit = True
for r, record in enumerate(rows):
for c, value in enumerate(record):
cell = table.cell(r, c)
cell.text = str(value)
para = cell.paragraphs[0]
# Numeric columns right-aligned, including their header, so the
# column reads as one edge rather than a ragged one.
para.alignment = (WD_ALIGN_PARAGRAPH.RIGHT if c > 0
else WD_ALIGN_PARAGRAPH.LEFT)
run = para.runs[0]
run.font.name, run.font.size = "Calibri", Pt(10)
if r == 0: # header row
run.font.bold = True
run.font.color.rgb = RGBColor.from_string(palette["paper"])
shade_cell(cell, palette["ink"]) # ink header band
elif r % 2 == 0: # zebra band
shade_cell(cell, palette["paper_tint"])
run.font.color.rgb = RGBColor.from_string(palette["ink"])
else:
run.font.color.rgb = RGBColor.from_string(palette["ink"])
# One rule under the table. Nothing under the header row -- it is already a
# solid ink band, so an ink rule there would be invisible against itself.
set_row_border(table.rows[-1], "bottom", palette["ink"])
caption = doc.add_paragraph(style="Caption")
caption_run = caption.add_run("Table 1 — Revenue by region, FY2026.")
caption_run.font.size, caption_run.font.italic = Pt(9), True
caption_run.font.bold = False # Caption defaults to bold
caption_run.font.color.rgb = RGBColor.from_string(palette["ink_tint"])
Repeat the header row across page breaks for tables longer than a page:
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
# w:trPr allows at most one w:tblHeader, so replace rather than append -- a
# second one is schema-invalid, and a loop over tables would add one per pass.
trPr = table.rows[0]._tr.get_or_add_trPr()
for stale in trPr.findall(qn("w:tblHeader")):
trPr.remove(stale)
trPr.append(OxmlElement("w:tblHeader"))
📝 Output checklist
-
from docx import Document(import name isdocx, notpython_docx) - LANGUAGE matches the user's prompt — no English kickers in a ZH report
- One palette, only its 4 hex values appear anywhere
- Only Georgia + Calibri;
Normalcarries the body font, and no named CJK face is pinned tow:eastAsia - Real
Heading 1/2/3styles used (Word navigation pane shows an outline) - Page size, margins, and orientation set explicitly on the section (landscape = swap width/height, not just the orientation flag)
- Cover page present, followed by a
WD_SECTION.NEW_PAGEsection break (not an extra page break as well — that leaves a blank page) - Tables: ink header band,
paper_tintzebra rows, horizontal rules only, numbers right-aligned, caption below - No fabricated data, no lorem ipsum, no placeholder headings
- File saved with a plain filename, then re-opened and asserted non-empty (byte size proves nothing — an empty document is already ~36 KB)
- Any structural assertion matches what was requested — no fixed minimum that a legitimate one-page memo or letter would fail
- Final answer FIRST LINE is
[filename](file:UUID)as bare markdown
Then write the .docx and report path + which palette + which sections.