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.
.claude/skills/anylegal-ai-draft/SKILL.md| 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)| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 22,125 | 8,049 | -64% | 1 | 1 | 0% | 3,394 | 6,049 | +78% | 0 | 0 | — |
case-02 | fail→fail | 28,920 | 102,173 | +253% | 1 | 1 | 0% | 5,408 | 13,234 | +145% | 0 | 0 | — |
case-12 | fail→fail | 18,022 | 59,688 | +231% | 1 | 1 | 0% | 3,675 | 14,213 | +287% | 0 | 0 | — |
case-03 | fail→fail | 25,545 | 84,288 | +230% | 1 | 1 | 0% | 6,208 | 11,862 | +91% | 0 | 0 | — |
case-04 | fail→fail | 27,853 | 88,460 | +218% | 1 | 1 | 0% | 6,186 | 10,990 | +78% | 0 | 0 | — |
case-05 | fail→fail | 15,867 | 62,649 | +295% | 1 | 1 | 0% | 3,649 | 9,043 | +148% | 0 | 0 | — |
case-06 | fail→fail | 28,224 | 100,532 | +256% | 1 | 1 | 0% | 6,184 | 13,046 | +111% | 0 | 0 | — |
case-07 | fail→fail | 30,995 | 63,451 | +105% | 1 | 1 | 0% | 5,622 | 12,919 | +130% | 0 | 0 | — |
case-08 | fail→pass | 7,159 | 65,571 | +816% | 1 | 1 | 0% | 1,385 | 9,347 | +575% | 0 | 0 | — |
case-09 | fail→fail | 49,414 | 91,311 | +85% | 1 | 1 | 0% | 6,176 | 12,903 | +109% | 0 | 0 | — |
case-10 | pass→fail | 9,088 | 99,043 | +990% | 1 | 1 | 0% | 1,808 | 12,482 | +590% | 0 | 0 | — |
case-11 | fail→pass | 10,250 | 68,408 | +567% | 1 | 1 | 0% | 1,699 | 11,667 | +587% | 0 | 0 | — |
case-13 | fail→fail | 11,898 | 103,169 | +767% | 1 | 1 | 0% | 2,354 | 12,644 | +437% | 0 | 0 | — |
case-14 | fail→fail | 16,676 | 10,057 | -40% | 1 | 1 | 0% | 2,828 | 6,183 | +119% | 0 | 0 | — |
case-15 | pass→fail | 27,831 | 69,750 | +151% | 1 | 1 | 0% | 6,171 | 12,308 | +99% | 0 | 0 | — |
case-16 | pass→fail | 13,208 | 49,821 | +277% | 1 | 1 | 0% | 2,510 | 12,807 | +410% | 0 | 0 | — |
case-17 | fail→fail | 34,567 | 176,978 | +412% | 1 | 1 | 0% | 6,034 | 13,483 | +123% | 0 | 0 | — |
case-18 | fail→fail | 27,073 | 86,967 | +221% | 1 | 1 | 0% | 6,179 | 12,525 | +103% | 0 | 0 | — |
case-19 | fail→fail | 20,833 | 98,022 | +371% | 1 | 1 | 0% | 4,519 | 11,215 | +148% | 0 | 0 | — |
case-20 | fail→fail | 21,352 | 5,049 | -76% | 1 | 1 | 0% | 2,755 | 5,772 | +110% | 0 | 0 | — |
case-21 | fail→fail | 36,507 | 4,714 | -87% | 1 | 1 | 0% | 481 | 5,761 | +1098% | 0 | 0 | — |
case-22 | fail→fail | 6,348 | 12,656 | +99% | 1 | 1 | 0% | 1,047 | 6,319 | +504% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 3 counted toward the lift figure. The other 19 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of -5 percentage points is the difference between those two pass rates over the 3 comparable cases. 4 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.