Install
$ agentstack add skill-anylegal-ai-anylegal-oss-draft Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Document Drafting (docx-js, from-scratch only)
When to Use
Use this skill when:
- The user asks to draft a new document, clause, or section from scratch
- The user says /draft
- The user asks to "create", "write", "generate", or "prepare" legal content
Do NOT use this skill for:
- Filling placeholders in an existing template (e.g.
[Disclosing Party]→Acme Corp) — that is an edit task. InvokeSkill(skill="docx-editing")and useinstantiate_template(oredit_documentfor per-placeholder review). - Editing or restructuring an existing DOCX — also
docx-editing.
Critical rules (read first)
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.
Process
- Understand the request:
- What type of document or clause?
- Which jurisdiction and governing law?
- Which party does the user represent?
- Any specific requirements or constraints?
- Check playbook — if available in context, apply the user's preferred positions and risk posture.
- Multi-document sets — pre-flight
todo_write:
- If the user asks for a "set" / "series" / "package" (e.g. "Singapore Pte Ltd document set", "incorporation pack", "full SPA suite"), your first substantive tool call is
todo_writelisting one item per document. - Example:
[{"content": "Draft Company Constitution", ...}, {"content": "Draft Director Appointment Resolution", ...}, ...]. - After each document is saved, call
todo_writeagain marking the completed item and the next one in_progress. - Do not start
run_codeto create any document until the todo list is written. - Complete every document in the same turn. Don't stop mid-series.
- One document per
run_codecall. Never split a document across multiple calls.
Sandbox preconditions (trust these, don't probe)
docx(docx-js v9.5.1) is globally installed in the Node sandbox. Do not probe withnpm list docx,subprocess.run, or similar — justrequire('docx').- Write outputs to
/sandbox/output/.docx. That directory is where the backend imports from. Packer.toBuffer(doc)returns a Promise. Eitherawaitit or use.then(buf => ...). Writing the un-awaited Promise withfs.writeFileSyncthrowsERR_INVALID_ARG_TYPE.
Canonical docx-js invocation
// 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");
});
docx-js creation reference
Setup — all the imports you'll likely need
const { 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
// 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:
size: {
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.
const 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.
// ❌ 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.
const 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).
// 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:
- Always use
WidthType.DXA(neverWidthType.PERCENTAGE) - Table width must equal the sum of
columnWidths - Cell
widthmust match its correspondingcolumnWidth - Cell
marginsare internal padding — they reduce content area, not add to cell width - For full-width tables, use the content width (page width minus left and right margins)
Images
new 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
// 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
// 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
const 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
// 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
// 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
// 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
sections: [{
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({
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [anylegal-ai](https://github.com/anylegal-ai)
- **Source:** [anylegal-ai/anylegal-oss](https://github.com/anylegal-ai/anylegal-oss)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.