Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Draft a NEW document from scratch via docx-js (run_code language="node"). Use when the user wants to create new legal content from a blank page — agreements, memos, letters, resolutions, term sheets. For filling an existing template with placeholder values, use the `docx-editing` skill instead.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | 575% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 587% | 0% |
| case-10 | ✓→✗ | ▼ Worse | 590% | 0% |
| case-15 | ✓→✗ | ▼ Worse | 99% | 0% |
| case-16 | ✓→✗ | ▼ Worse | 410% | 0% |
Use this skill when:
Do NOT use this skill for:
[Disclosing Party] → Acme Corp) — that is an edit task. Invoke Skill(skill="docx-editing") and use instantiate_template (or edit_document for per-placeholder review).docx-editing.These rules constrain how you call the tool. Following them produces documents that open cleanly in Word, Google Docs, and LibreOffice; ignoring them produces invalid output.
| # | Rule | Why | |---|---|---| | 1 | DOCX creation goes through run_code(language="node") calling docx-js. Not python-docx. Not pandoc. Not lxml. | docx-js is the only library with reliable APIs for footnotes, internal hyperlinks, TableOfContents, page numbers in headers/footers, and positional tabs. | | 2 | One document = one run_code(language="node") call. Write the complete document in a single call — no skeleton-then-fill, no part-by-part assembly. | docx-js has no Document.load() API; a second call to the same filename silently overwrites the first. | | 3 | Bullets are produced via numbering config, not inline characters. Never write new TextRun("• Item") — use LevelFormat.BULLET with a numbering block on the Document. See "Lists" below. | Inline bullet glyphs render inconsistently across viewers and break list semantics. | | 4 | Filenames must be distinct logical names. Avoid _Part1 / _Final / _Draft / _v2 / _Copy suffixes. One logical document = one filename. | Versioning suffixes signal split-document anti-patterns and confuse downstream document management. | | 5 | Multi-document sets: todo_write first. If the user asks for a "set" / "series" / "package", your first tool call is todo_write with one item per document. After each document is saved, mark it completed and continue with the next item — in the same turn. | Without an explicit todo list, multi-doc requests get partially completed and abandoned. | | 6 | Content completeness. The saved document must be complete, not a skeleton: schedules referenced in the body must exist, defined terms must appear in the definitions clause, both parties' signature blocks for agreements, witness blocks for deeds. | A skeleton wastes the user's time — they have to ask again to fill the gaps. |
Data prep in Python is fine (pandas, openpyxl). Do it in a separate run_code(language="python") call that writes JSON to /sandbox/output/*.json, then consume that JSON from a run_code(language="node") call. Do NOT spawn Node as a subprocess from Python.
todo_write:todo_write listing one item per document.[{"content": "Draft Company Constitution", ...}, {"content": "Draft Director Appointment Resolution", ...}, ...].todo_write again marking the completed item and the next one in_progress.run_code to create any document until the todo list is written.run_code call. Never split a document across multiple calls.docx (docx-js v9.5.1) is globally installed in the Node sandbox. Do not probe with npm list docx, subprocess.run, or similar — just require('docx')./sandbox/output/<filename>.docx. That directory is where the backend imports from.Packer.toBuffer(doc) returns a Promise. Either await it or use .then(buf => ...). Writing the un-awaited Promise with fs.writeFileSync throws ERR_INVALID_ARG_TYPE.javascript// run_code(language="node", code=...) const { Document, Packer, Paragraph, TextRun, HeadingLevel, AlignmentType, FootnoteReferenceRun, Bookmark } = require('docx'); const fs = require('fs'); const doc = new Document({ creator: "Anylegal.ai", styles: { default: { document: { run: { font: "Times New Roman", size: 24 } } }, paragraphStyles: [ { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: 32, bold: true, font: "Arial" }, paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, ] }, footnotes: { 1: { children: [new Paragraph("Source: Companies Act 2006 (UK), s. 172")] }, }, sections: [{ properties: { page: { size: { width: 12240, height: 15840 }, // US Letter. A4 = 11906 × 16838. margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, }, }, children: [ new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new Bookmark({ id: "preamble", children: [new TextRun("1. PREAMBLE")] })], }), new Paragraph({ alignment: AlignmentType.JUSTIFIED, children: [ new TextRun("This Agreement is dated "), new TextRun({ text: "15 May 2026", bold: true }), new TextRun(" pursuant to Section 172"), new FootnoteReferenceRun(1), new TextRun(" of the Companies Act."), ], }), ], }], }); Packer.toBuffer(doc).then(buf => { fs.writeFileSync("/sandbox/output/Agreement.docx", buf); console.log("ok"); });
##### Setup — all the imports you'll likely need
javascriptconst { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink, InternalHyperlink, Bookmark, FootnoteReferenceRun, PositionalTab, PositionalTabAlignment, PositionalTabRelativeTo, PositionalTabLeader, TabStopType, TabStopPosition, Column, SectionType, TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType, VerticalAlign, PageNumber, PageBreak } = require('docx'); const doc = new Document({ sections: [{ children: [/* content */] }] }); Packer.toBuffer(doc).then(buffer => fs.writeFileSync("/sandbox/output/doc.docx", buffer));
##### Page size
javascript// docx-js defaults to A4. For US documents, set US Letter explicitly. sections: [{ properties: { page: { size: { width: 12240, // 8.5 inches in DXA height: 15840 // 11 inches in DXA }, margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } // 1 inch margins } }, children: [/* content */] }]
Common page sizes (DXA units, 1440 DXA = 1 inch):
| Paper | Width | Height | Content Width (1" margins) | |-------|-------|--------|---------------------------| | US Letter | 12,240 | 15,840 | 9,360 | | A4 (default) | 11,906 | 16,838 | 9,026 |
Landscape orientation: docx-js swaps width/height internally — pass portrait dimensions and let it handle the swap:
javascriptsize: { width: 12240, // Pass SHORT edge as width height: 15840, // Pass LONG edge as height orientation: PageOrientation.LANDSCAPE // docx-js swaps them in the XML } // Content width = 15840 - left margin - right margin (uses the long edge)
##### Styles (override built-in headings)
Default to Arial — it renders consistently across Word, Google Docs, and LibreOffice. Headings stay in standard black; coloured headings rarely improve readability and often look amateurish in legal documents.
javascriptconst doc = new Document({ styles: { default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt default paragraphStyles: [ // Use exact IDs to override built-in styles { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: 32, bold: true, font: "Arial" }, paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // outlineLevel required for TOC { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, run: { size: 28, bold: true, font: "Arial" }, paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } }, ] }, sections: [{ children: [ new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Recitals")] }), ] }] });
##### Lists — bullets via numbering config
Bullet glyphs in TextRun content render inconsistently and break list semantics. Use a numbering config block on the Document and reference it from each list paragraph.
javascript// ❌ Avoid new Paragraph({ children: [new TextRun("• Item")] }) // glyph in text — bad // ✅ Use numbering config const doc = new Document({ numbering: { config: [ { reference: "bullets", levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, { reference: "numbers", levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }, ] }, sections: [{ children: [ new Paragraph({ numbering: { reference: "bullets", level: 0 }, children: [new TextRun("Confidentiality obligation")] }), new Paragraph({ numbering: { reference: "numbers", level: 0 }, children: [new TextRun("Definitions")] }), ] }] }); // Each reference creates an independent numbering sequence: // Same reference reused = continues (1, 2, 3 then 4, 5, 6) // Different reference = restarts (1, 2, 3 then 1, 2, 3)
##### Tables
Tables need both columnWidths on the table and width on each cell. Without both, layout breaks on some viewers.
javascriptconst border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }; const borders = { top: border, bottom: border, left: border, right: border }; new Table({ width: { size: 9360, type: WidthType.DXA }, // Use DXA — percentages break in Google Docs columnWidths: [4680, 4680], // Must sum to table width (DXA: 1440 = 1 inch) rows: [ new TableRow({ children: [ new TableCell({ borders, width: { size: 4680, type: WidthType.DXA }, // Match the columnWidth shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, // CLEAR — SOLID renders as black margins: { top: 80, bottom: 80, left: 120, right: 120 }, // Internal padding (does not add to width) children: [new Paragraph({ children: [new TextRun("Cell")] })] }) ] }) ] })
Table width calculation:
Always use WidthType.DXA (WidthType.PERCENTAGE is incompatible with Google Docs).
javascript// Table width = sum of columnWidths = content width // US Letter with 1" margins: 12240 - 2880 = 9360 DXA width: { size: 9360, type: WidthType.DXA }, columnWidths: [7000, 2360] // Must sum to table width
Width rules:
WidthType.DXA (never WidthType.PERCENTAGE)columnWidthswidth must match its corresponding columnWidthmargins are internal padding — they reduce content area, not add to cell width##### Images
javascriptnew Paragraph({ children: [new ImageRun({ type: "png", // Required: png, jpg, jpeg, gif, bmp, svg data: fs.readFileSync("/sandbox/input/image.png"), transformation: { width: 200, height: 150 }, altText: { title: "Title", description: "Desc", name: "Name" } // All three required })] })
##### Page breaks
javascript// PageBreak must live inside a Paragraph — standalone produces invalid XML new Paragraph({ children: [new PageBreak()] }) // Or use pageBreakBefore new Paragraph({ pageBreakBefore: true, children: [new TextRun("New page")] })
##### Hyperlinks
javascript// External link new Paragraph({ children: [new ExternalHyperlink({ children: [new TextRun({ text: "Click here", style: "Hyperlink" })], link: "https://example.com", })] }) // Internal link (bookmark + reference) // 1. Create bookmark at destination new Paragraph({ heading: HeadingLevel.HEADING_1, children: [ new Bookmark({ id: "schedule_a", children: [new TextRun("Schedule A")] }), ]}) // 2. Link to it new Paragraph({ children: [new InternalHyperlink({ children: [new TextRun({ text: "See Schedule A", style: "Hyperlink" })], anchor: "schedule_a", })]})
##### Footnotes
javascriptconst doc = new Document({ footnotes: { 1: { children: [new Paragraph("Source: Annual Report 2025")] }, 2: { children: [new Paragraph("See Schedule B for methodology")] }, }, sections: [{ children: [new Paragraph({ children: [ new TextRun("Net revenue grew 15%"), new FootnoteReferenceRun(1), new TextRun(" using adjusted metrics"), new FootnoteReferenceRun(2), ], })] }] });
##### Tab stops
javascript// Right-align text on the same line (e.g., date opposite a title) new Paragraph({ children: [ new TextRun("Company Name"), new TextRun("\tJanuary 2026"), ], tabStops: [{ type: TabStopType.RIGHT, position: TabStopPosition.MAX }], }) // Dot leader (e.g., TOC-style) new Paragraph({ children: [ new TextRun("Introduction"), new TextRun({ children: [ new PositionalTab({ alignment: PositionalTabAlignment.RIGHT, relativeTo: PositionalTabRelativeTo.MARGIN, leader: PositionalTabLeader.DOT, }), "3", ]}), ], })
##### Multi-column layouts
javascript// Equal-width columns sections: [{ properties: { column: { count: 2, // number of columns space: 720, // gap between columns in DXA (720 = 0.5 inch) equalWidth: true, separate: true, // vertical line between columns }, }, children: [/* content flows naturally across columns */] }] // Custom-width columns (equalWidth must be false) sections: [{ properties: { column: { equalWidth: false, children: [ new Column({ width: 5400, space: 720 }), new Column({ width: 3240 }), ], }, }, children: [/* content */] }]
Force a column break with a new section using type: SectionType.NEXT_COLUMN.
##### Table of Contents
javascript// Headings must use HeadingLevel directly — no custom styles for TOC entries new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" })
The TOC must be a new TableOfContents(...) object — not a plain heading. A Paragraph({ text: "Table of Contents" }) (or that text wrapped in a HEADING_1) produces a static label without a Word field, so "Update Field" in Word does nothing. When the user asks for a TOC, instantiate the class and add it to the section's children array before the headings it should index.
##### Headers / footers
javascriptsections: [{ properties: { page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } // 1440 = 1 inch }, headers: { default: new Header({ children: [new Paragraph({ children: [new TextRun("Header")] })] }) }, footers: { default: new Footer({ children: [new Paragraph({ children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })] })] }) }, children: [/* content */] }]
##### Quick reference — docx-js gotchas
A summary of the rules above, ordered by frequency of impact:
type parameter is requiredwidth (DXA) on the table AND on each cellShadingType.CLEAR (SOLID renders black on some viewers)TableOfContents, do not write a plain headingHeadingLevel.* only, no custom paragraph stylesoutlineLevel — required on heading paragraphs (0 for H1, 1 for H2, etc.) for TOC to populate\n — use separate Paragraph elements for line breaksOne document, one call — context. docx-js has no reader API; a second call to the same filename overwrites the first. Modern models emit 15–32K output tokens per turn — a full SSA is ~8–12K tokens of final text. It fits. Write it all in one call. If a monolith truly exceeds one call, split into separate logical documents (e.g. "Shareholders Agreement" + "Side Letter"), not parts of one file.
Filenames for multi-doc deliverables. Use descriptive distinct names: 1_Term_Sheet.docx, 2_Share_Subscription_Agreement.docx, 3_Shareholders_Agreement.docx. Avoid _Part1 / _Final / _v2 / _Draft / _Copy.
Editing is a different skill. If the user asks to revise a clause, fill template placeholders, or add tracked changes — that's not drafting. Invoke Skill(skill="docx-editing") and use instantiate_template (template fills) or edit_document (redline edits).
The final saved document must be complete, not a skeleton:
[DETAILS] placeholders)Other measured skills in the registry, with their headline benchmark lift.