The Atlas AnyLegal OSS — documentation bound to its code
20 documents
backend/anylegal_oss/workspace/skills/draft/SKILL.md

The `draft` skill: create a NEW document from a blank page via run_code(language="node") calling docx-js — explicitly not python-docx, pandoc, or lxml, and not for filling existing templates (that is docx-editing). Six critical rules up front (docx-js only; one document = one node call; bullets via numbering config not glyphs; distinct logical filenames; todo_write first for multi-document sets; content completeness, not skeletons), sandbox preconditions to trust rather than probe, a canonical docx-js invocation, and a large creation reference covering styles, lists, tables, images, page breaks, hyperlinks, footnotes, tab stops, multi-column layouts, TOC, headers/footers, and the library's gotchas. When generating new legal content from scratch — agreements, memos, letters, resolutions, term sheets.


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. Invoke Skill(skill="docx-editing") and use instantiate_template (or edit_document for 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

  1. 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?
  2. Check playbook — if available in context, apply the user's preferred positions and risk posture.
  3. 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_write listing one item per document.
    • Example: [{"content": "Draft Company Constitution", ...}, {"content": "Draft Director Appointment Resolution", ...}, ...].
    • After each document is saved, call todo_write again marking the completed item and the next one in_progress.
    • Do not start run_code to create any document until the todo list is written.
    • Complete every document in the same turn. Don't stop mid-series.
  4. One document per run_code call. 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 with npm list docx, subprocess.run, or similar — just require('docx').
  • Write outputs to /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.

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 (never WidthType.PERCENTAGE)
  • Table width must equal the sum of columnWidths
  • Cell width must match its corresponding columnWidth
  • Cell margins are 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")] })
// 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({ 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:

  1. Page size — set explicitly (docx-js defaults to A4)
  2. Landscape — pass portrait dimensions; orientation flag swaps internally
  3. Bullets — numbering config, never inline glyphs
  4. PageBreak — must be inside a Paragraph
  5. ImageRuntype parameter is required
  6. Tableswidth (DXA) on the table AND on each cell
  7. Cell margins — set them; otherwise the table looks cramped
  8. Shading — use ShadingType.CLEAR (SOLID renders black on some viewers)
  9. Tables aren't dividers — use a Paragraph with a bottom border
  10. TOC — instantiate TableOfContents, do not write a plain heading
  11. TOC headings — use HeadingLevel.* only, no custom paragraph styles
  12. Override built-in styles — exact IDs ("Heading1", "Heading2", etc.)
  13. outlineLevel — required on heading paragraphs (0 for H1, 1 for H2, etc.) for TOC to populate
  14. No \n — use separate Paragraph elements for line breaks

Reminders

One 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).

Content completeness

The final saved document must be complete, not a skeleton:

  • Every Schedule/Annex referenced in the body exists (even with [DETAILS] placeholders)
  • Every defined term in quotes appears in the Definitions clause
  • Standard boilerplate: Notices, Severability, Waiver, Counterparts, Entire Agreement, Amendment, Governing Law
  • Agreements: both parties' signature blocks. Deeds: witness blocks under each signatory.

Output Format

  • Brief explanation of structure and key decisions
  • The drafted content (created as a document in the workspace)
  • Notes: assumptions, areas needing user input, jurisdiction considerations

Guidelines

  • Always ask for clarification on jurisdiction, governing law, and represented party if not provided
  • Use clear, modern legal drafting style (avoid "hereinafter", "witnesseth")
  • Include proper defined terms with initial capitalization
  • Structure documents with logical clause numbering
  • Draft complete clauses — no placeholders unless explicitly asked
  • If drafting from a template, preserve the template's structure
  • Consider playbook positions when choosing clause language