Volume 5: Domain Template Creation

Chapter 5: The Generator Script — 20 Templates in a few Minutes

"This is the chapter where 80 hours becomes a few minutes. Read it once to understand it. Then run it and watch your domain come to life."


The Moment Everything Changes

You have ten CSV files. You have a domain-config.json. You have a design that has been validated, checked, and double-checked. Everything is ready.

Now you need 20 Word templates.

The traditional path — opening Word, designing a layout, inserting placeholder text, formatting headers, building tables, saving, repeating 19 more times — takes somewhere between 40 and 80 hours. It is tedious, inconsistent, and error-prone. Template 17 will look subtly different from Template 3. A placeholder will be misspelled in Template 9. A table column will be the wrong width in Template 14. You will spend as much time fixing inconsistencies as you spent building templates in the first place.

There is another path.

You write one JavaScript file. You run it once. In a few minutes — the time it takes to make a cup of coffee and read the morning headlines — you have 20 professionally formatted, consistently structured, correctly placeholdered Word templates ready for testing.

That is what this chapter teaches.


How the Generator Works

The generator script uses the docx npm package — the same library that powers the Data Publisher backend's document generation engine. It defines each of your 20 templates as a JavaScript function that returns an array of document elements: paragraphs, headings, tables, loops, placeholders. It then runs all 20 functions and writes the resulting .docx files to your word-templates/ directory.

The script is not magic. It is a disciplined application of three ideas:

Idea 1: Templates are code, not files. A Word template is just a structured arrangement of text, formatting, and placeholders. Code can express that structure just as well as a GUI — and code is consistent, versionable, and regeneratable.

Idea 2: Helper functions eliminate repetition. Every template uses the same kinds of elements: section headers, bold labels, placeholder fields, loop markers. Define each element type once as a helper function. Use it everywhere.

Idea 3: Generate everything in one run. A single script that generates all 20 templates in sequence means every template is created with the same helper functions, the same formatting defaults, the same placeholder syntax. Consistency is built in.


Setting Up the Script

Create a new file in your scripts/ directory:

scripts/generate-legal-services-templates.js

Begin with the imports and the output directory:

const {
  Document,
  Packer,
  Paragraph,
  TextRun,
  Table,
  TableRow,
  TableCell,
  HeadingLevel,
  AlignmentType,
  BorderStyle,
  WidthType,
  ShadingType,
  LevelFormat,
  PageNumber,
  Header,
  Footer
} = require('docx');

const fs = require('fs');
const path = require('path');

const OUTPUT_DIR = path.join(
  __dirname,
  '..',
  'DataPublisher_DomainTemplates',
  'legal-services',
  'word-templates'
);

// Ensure output directory exists
if (!fs.existsSync(OUTPUT_DIR)) {
  fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}

The Helper Functions

Helper functions are the beating heart of the generator script. Define them once, use them throughout all 20 templates. Consistency is automatic.

Here are the complete helper functions for a professional legal services domain. Study these carefully — they are the building blocks everything else is made from.

// ─── TEXT HELPERS ─────────────────────────────────────────────────────────────

// A plain text run
function text(str) {
  return new TextRun({ text: str });
}

// A bold text run
function bold(str) {
  return new TextRun({ text: str, bold: true });
}

// A placeholder field — blue Courier New, visually distinct from surrounding text
function ph(field) {
  return new TextRun({
    text: `<<${field}>>`,
    color: '0055AA',
    font: 'Courier New',
    size: 20
  });
}

// A loop control marker — dark gray, slightly smaller
function loop(marker) {
  return new TextRun({
    text: `{{${marker}}}`,
    color: '666666',
    font: 'Courier New',
    size: 18,
    italics: true
  });
}

// A conditional control marker
function cond(marker) {
  return new TextRun({
    text: `{{${marker}}}`,
    color: '884400',
    font: 'Courier New',
    size: 18,
    italics: true
  });
}

// ─── PARAGRAPH HELPERS ────────────────────────────────────────────────────────

// Section heading (Heading 1)
function h1(str) {
  return new Paragraph({
    heading: HeadingLevel.HEADING_1,
    spacing: { before: 320, after: 160 },
    children: [new TextRun({ text: str, bold: true, size: 28 })]
  });
}

// Subsection heading (Heading 2)
function h2(str) {
  return new Paragraph({
    heading: HeadingLevel.HEADING_2,
    spacing: { before: 240, after: 120 },
    children: [new TextRun({ text: str, bold: true, size: 24 })]
  });
}

// Standard body paragraph with optional children array
function para(children, options = {}) {
  return new Paragraph({
    spacing: { before: 80, after: 80 },
    ...options,
    children: Array.isArray(children) ? children : [text(children)]
  });
}

// A label-value pair on one line: "Client: <<clients.FirstName>>"
function labelValue(label, ...valueRuns) {
  return new Paragraph({
    spacing: { before: 60, after: 60 },
    children: [bold(`${label}: `), ...valueRuns]
  });
}

// An empty spacer paragraph
function spacer() {
  return new Paragraph({ spacing: { before: 120, after: 120 }, children: [] });
}

// A horizontal rule using a bottom border on an empty paragraph
function rule() {
  return new Paragraph({
    border: {
      bottom: { style: BorderStyle.SINGLE, size: 6, color: '2C4770', space: 4 }
    },
    spacing: { before: 160, after: 160 },
    children: []
  });
}

// A loop start marker paragraph
function loopStart(tableName) {
  return new Paragraph({
    spacing: { before: 40, after: 40 },
    children: [loop(`ForEach:${tableName}`)]
  });
}

// A loop end marker paragraph
function loopEnd() {
  return new Paragraph({
    spacing: { before: 40, after: 40 },
    children: [loop('EndForEach')]
  });
}

// A conditional start marker paragraph
function ifStart(condition) {
  return new Paragraph({
    spacing: { before: 40, after: 40 },
    children: [cond(`IF ${condition}`)]
  });
}

// A conditional end marker paragraph
function ifEnd() {
  return new Paragraph({
    spacing: { before: 40, after: 40 },
    children: [cond('ENDIF')]
  });
}

// ─── TABLE HELPERS ────────────────────────────────────────────────────────────

const CONTENT_WIDTH = 9360; // US Letter, 1-inch margins

// A standard cell border definition
const cellBorder = {
  top: { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' },
  bottom: { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' },
  left: { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' },
  right: { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' }
};

// A header cell with shaded background
function headerCell(text, width) {
  return new TableCell({
    width: { size: width, type: WidthType.DXA },
    borders: cellBorder,
    shading: { fill: '2C4770', type: ShadingType.CLEAR },
    margins: { top: 80, bottom: 80, left: 120, right: 120 },
    children: [
      new Paragraph({
        children: [new TextRun({ text, bold: true, color: 'FFFFFF', size: 20 })]
      })
    ]
  });
}

// A standard data cell
function dataCell(children, width) {
  return new TableCell({
    width: { size: width, type: WidthType.DXA },
    borders: cellBorder,
    margins: { top: 80, bottom: 80, left: 120, right: 120 },
    children: [
      new Paragraph({
        children: Array.isArray(children) ? children : [text(children)]
      })
    ]
  });
}

// A shaded data cell for alternating rows (used in financial tables)
function shadedCell(children, width) {
  return new TableCell({
    width: { size: width, type: WidthType.DXA },
    borders: cellBorder,
    shading: { fill: 'F0F4F8', type: ShadingType.CLEAR },
    margins: { top: 80, bottom: 80, left: 120, right: 120 },
    children: [
      new Paragraph({
        children: Array.isArray(children) ? children : [text(children)]
      })
    ]
  });
}

// ─── DOCUMENT WRAPPER ─────────────────────────────────────────────────────────

// Creates a complete Document with standard page setup,
// a firm letterhead header, and a page number footer
function makeDocument(headerFirmName, children) {
  return new Document({
    styles: {
      default: {
        document: {
          run: { font: 'Georgia', size: 22 }
        }
      }
    },
    sections: [{
      properties: {
        page: {
          size: { width: 12240, height: 15840 },
          margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }
        }
      },
      headers: {
        default: new Header({
          children: [
            new Paragraph({
              border: {
                bottom: { style: BorderStyle.SINGLE, size: 6, color: '2C4770', space: 4 }
              },
              spacing: { after: 200 },
              children: [
                new TextRun({
                  text: headerFirmName,
                  bold: true,
                  size: 28,
                  color: '2C4770',
                  font: 'Georgia'
                })
              ]
            })
          ]
        })
      },
      footers: {
        default: new Footer({
          children: [
            new Paragraph({
              border: {
                top: { style: BorderStyle.SINGLE, size: 4, color: 'CCCCCC', space: 4 }
              },
              alignment: AlignmentType.CENTER,
              children: [
                new TextRun({ text: 'Page ', size: 18, color: '888888' }),
                new TextRun({
                  children: [PageNumber.CURRENT],
                  size: 18,
                  color: '888888'
                }),
                new TextRun({ text: ' of ', size: 18, color: '888888' }),
                new TextRun({
                  children: [PageNumber.TOTAL_PAGES],
                  size: 18,
                  color: '888888'
                })
              ]
            })
          ]
        })
      },
      children
    }]
  });
}

These 130 lines of helper functions are the entire visual language of your domain's templates. Everything that follows — all 20 templates — is built from these building blocks. The visual consistency of your domain is guaranteed before you write a single template function.


The Template Array

After your helper functions, define the template array. This is the manifest — it tells the generator what to create and where to save it.

const templates = [
  {
    id: 1,
    filename: 'Template-01-EngagementLetter.docx',
    name: 'Engagement Letter',
    description: 'Formal attorney-client engagement letter',
    csvFiles: ['matters.csv', 'clients.csv', 'attorneys.csv', 'firms.csv'],
    content: createEngagementLetter
  },
  {
    id: 2,
    filename: 'Template-02-ClientStatusUpdate.docx',
    name: 'Client Status Update',
    description: 'Professional matter status letter for clients',
    csvFiles: ['matters.csv', 'clients.csv', 'attorneys.csv', 'hearings.csv', 'deadlines.csv'],
    content: createClientStatusUpdate
  },
  {
    id: 3,
    filename: 'Template-03-Invoice.docx',
    name: 'Invoice',
    description: 'Detailed client invoice with time entries',
    csvFiles: ['invoices.csv', 'clients.csv', 'matters.csv', 'attorneys.csv', 'billing_entries.csv', 'firms.csv'],
    content: createInvoice
  },
  {
    id: 4,
    filename: 'Template-04-PaymentReceipt.docx',
    name: 'Payment Receipt',
    description: 'Payment confirmation receipt',
    csvFiles: ['invoices.csv', 'clients.csv', 'matters.csv', 'firms.csv'],
    content: createPaymentReceipt
  },
  {
    id: 5,
    filename: 'Template-05-CaseSummaryLetter.docx',
    name: 'Case Summary Letter',
    description: 'Comprehensive matter history and status summary',
    csvFiles: ['matters.csv', 'clients.csv', 'attorneys.csv', 'hearings.csv', 'deadlines.csv', 'documents_filed.csv'],
    content: createCaseSummaryLetter
  },
  {
    id: 6,
    filename: 'Template-06-RetainerAgreement.docx',
    name: 'Retainer Agreement',
    description: 'Formal retainer agreement establishing representation',
    csvFiles: ['matters.csv', 'clients.csv', 'attorneys.csv', 'firms.csv'],
    content: createRetainerAgreement
  },
  {
    id: 7,
    filename: 'Template-07-ConflictDisclosure.docx',
    name: 'Conflict of Interest Disclosure',
    description: 'Required conflict disclosure and waiver',
    csvFiles: ['matters.csv', 'clients.csv', 'attorneys.csv', 'firms.csv'],
    content: createConflictDisclosure
  },
  {
    id: 8,
    filename: 'Template-08-FileClosingLetter.docx',
    name: 'File Closing Letter',
    description: 'Matter closing confirmation letter',
    csvFiles: ['matters.csv', 'clients.csv', 'attorneys.csv', 'firms.csv'],
    content: createFileClosingLetter
  },
  {
    id: 9,
    filename: 'Template-09-PrivilegeNotice.docx',
    name: 'Attorney-Client Privilege Notice',
    description: 'Privilege and confidentiality notice',
    csvFiles: ['firms.csv', 'attorneys.csv'],
    content: createPrivilegeNotice
  },
  {
    id: 10,
    filename: 'Template-10-HearingPrepSheet.docx',
    name: 'Hearing Preparation Sheet',
    description: 'Internal preparation document for court appearances',
    csvFiles: ['hearings.csv', 'matters.csv', 'clients.csv', 'attorneys.csv', 'deadlines.csv'],
    content: createHearingPrepSheet
  },
  {
    id: 11,
    filename: 'Template-11-DepositionOutline.docx',
    name: 'Deposition Outline',
    description: 'Structured deposition question and topic outline',
    csvFiles: ['hearings.csv', 'matters.csv', 'attorneys.csv'],
    content: createDepositionOutline
  },
  {
    id: 12,
    filename: 'Template-12-CourtAppearanceChecklist.docx',
    name: 'Court Appearance Checklist',
    description: 'Pre-hearing logistics and materials checklist',
    csvFiles: ['hearings.csv', 'matters.csv', 'attorneys.csv', 'documents_filed.csv'],
    content: createCourtAppearanceChecklist
  },
  {
    id: 13,
    filename: 'Template-13-DeadlineCalendar.docx',
    name: 'Deadline Calendar',
    description: 'Matter deadlines organized by priority and due date',
    csvFiles: ['matters.csv', 'deadlines.csv', 'attorneys.csv'],
    content: createDeadlineCalendar
  },
  {
    id: 14,
    filename: 'Template-14-MatterStatusReport.docx',
    name: 'Matter Status Report',
    description: 'Firm-wide active matter status report',
    csvFiles: ['firms.csv', 'matters.csv', 'clients.csv', 'attorneys.csv'],
    content: createMatterStatusReport
  },
  {
    id: 15,
    filename: 'Template-15-AttorneyWorkload.docx',
    name: 'Attorney Workload Summary',
    description: 'Per-attorney active matters and hours summary',
    csvFiles: ['attorneys.csv', 'matters.csv', 'billing_entries.csv', 'deadlines.csv'],
    content: createAttorneyWorkload
  },
  {
    id: 16,
    filename: 'Template-16-ActiveCaseList.docx',
    name: 'Active Case List',
    description: 'Concise list of all active matters',
    csvFiles: ['matters.csv', 'clients.csv', 'attorneys.csv', 'practice_areas.csv'],
    content: createActiveCaseList
  },
  {
    id: 17,
    filename: 'Template-17-ClientContactSheet.docx',
    name: 'Client Contact Sheet',
    description: 'Quick-reference client contact information',
    csvFiles: ['clients.csv', 'matters.csv', 'attorneys.csv'],
    content: createClientContactSheet
  },
  {
    id: 18,
    filename: 'Template-18-MonthlyBillingSummary.docx',
    name: 'Monthly Billing Summary',
    description: 'Monthly financial summary by client',
    csvFiles: ['invoices.csv', 'clients.csv', 'matters.csv', 'billing_entries.csv', 'firms.csv'],
    content: createMonthlyBillingSummary
  },
  {
    id: 19,
    filename: 'Template-19-AttorneyBiography.docx',
    name: 'Attorney Biography Sheet',
    description: 'Professional attorney biography',
    csvFiles: ['attorneys.csv', 'firms.csv', 'practice_areas.csv'],
    content: createAttorneyBiography
  },
  {
    id: 20,
    filename: 'Template-20-NewClientWelcome.docx',
    name: 'New Client Welcome Package',
    description: 'Comprehensive welcome package for new clients',
    csvFiles: ['clients.csv', 'matters.csv', 'attorneys.csv', 'firms.csv'],
    content: createNewClientWelcome
  }
];

Template Functions: Three Complete Examples

We will now write three complete template functions, chosen to demonstrate the full range of complexity: a simple single-table template, a moderate multi-table template with a loop, and a complex template with nested loops, conditionals, and financial calculations. Every other template in your domain follows one of these three patterns.


Template 4: Payment Receipt (Simple)

The Payment Receipt is the simplest template in the domain — it draws from four tables but uses no loops and no conditionals. It is the right place to start.

function createPaymentReceipt() {
  return [
    // Header block
    h1('PAYMENT RECEIPT'),
    spacer(),

    // Firm information
    para([
      ph('firms.FirmName'), text(' | '),
      ph('firms.Address'), text(', '),
      ph('firms.City'), text(', '),
      ph('firms.State'), text(' '),
      ph('firms.ZipCode')
    ]),
    para([
      text('Tel: '), ph('firms.Phone'),
      text('  |  '),
      text('Email: '), ph('firms.Email')
    ]),

    rule(),

    // Receipt details
    labelValue('Receipt Date', ph('{{TODAY}}')),
    labelValue('Invoice Number', ph('invoices.InvoiceNumber')),
    labelValue('Matter', ph('matters.MatterNumber'), text(' — '), ph('matters.Description')),

    spacer(),

    // Client information
    h2('Billed To'),
    para([ph('clients.FirstName'), text(' '), ph('clients.LastName')]),
    para([ph('clients.Address')]),
    para([
      ph('clients.City'), text(', '),
      ph('clients.State'), text(' '),
      ph('clients.ZipCode')
    ]),
    para([ph('clients.Email')]),

    spacer(),
    rule(),
    spacer(),

    // Payment summary
    h2('Payment Summary'),
    new Table({
      width: { size: CONTENT_WIDTH, type: WidthType.DXA },
      columnWidths: [5760, 1800, 1800],
      rows: [
        new TableRow({
          children: [
            headerCell('Description', 5760),
            headerCell('Amount', 1800),
            headerCell('Paid', 1800)
          ]
        }),
        new TableRow({
          children: [
            dataCell([
              text('Legal services — '),
              ph('matters.Description')
            ], 5760),
            dataCell([ph('invoices.TotalAmount')], 1800),
            dataCell([ph('invoices.AmountPaid')], 1800)
          ]
        }),
        new TableRow({
          children: [
            shadedCell([bold('Balance Due')], 5760),
            shadedCell([], 1800),
            shadedCell([bold(ph('invoices.Balance'))], 1800)
          ]
        })
      ]
    }),

    spacer(),

    // Payment details
    labelValue('Payment Method', ph('invoices.Status')),
    labelValue('Payment Status', ph('invoices.Status')),

    spacer(),
    rule(),

    // Footer note
    para([
      text('Thank you for your payment. Please retain this receipt for your records. '),
      text('Questions? Contact '),
      ph('attorneys.FirstName'), text(' '),
      ph('attorneys.LastName'),
      text(' at '), ph('attorneys.Email'), text('.')
    ]),

    spacer(),

    para([
      bold(ph('firms.FirmName')),
      text(' — Confidential Client Document')
    ], { alignment: AlignmentType.CENTER })
  ];
}

What this demonstrates: The Payment Receipt shows the foundational template pattern. Notice how every piece of data is a ph() call referencing table.ColumnName syntax. The table uses headerCell() for the shaded header row and dataCell() / shadedCell() for data rows. The {{TODAY}} system variable is wrapped in ph() like any other field. The document is clean, professional, and completely driven by data.


Template 13: Deadline Calendar (Moderate — Loop)

The Deadline Calendar iterates over the deadlines table to produce a prioritized list. This is the ForEach pattern — the most common loop structure in domain templates.

function createDeadlineCalendar() {
  return [
    h1('DEADLINE CALENDAR'),

    // Matter context
    h2('Matter'),
    labelValue('Matter Number', ph('matters.MatterNumber')),
    labelValue('Description', ph('matters.Description')),
    labelValue('Client', ph('clients.FirstName'), text(' '), ph('clients.LastName')),
    labelValue('Lead Attorney', ph('attorneys.FirstName'), text(' '), ph('attorneys.LastName')),
    labelValue('Generated', ph('{{TODAY}}')),

    rule(),

    // Critical deadlines
    h2('Critical Deadlines'),
    ifStart('deadlines.Priority=Critical'),

    loopStart('deadlines'),
    ifStart('deadlines.Priority=Critical'),

    new Table({
      width: { size: CONTENT_WIDTH, type: WidthType.DXA },
      columnWidths: [1800, 4560, 1440, 1560],
      rows: [
        new TableRow({
          children: [
            dataCell([bold(ph('deadlines.DueDate'))], 1800),
            dataCell([ph('deadlines.Description')], 4560),
            dataCell([ph('deadlines.DeadlineType')], 1440),
            dataCell([ph('deadlines.Status')], 1560)
          ]
        })
      ]
    }),

    ifEnd(),
    loopEnd(),

    ifEnd(),

    spacer(),

    // High priority deadlines
    h2('High Priority Deadlines'),

    loopStart('deadlines'),
    ifStart('deadlines.Priority=High'),

    new Table({
      width: { size: CONTENT_WIDTH, type: WidthType.DXA },
      columnWidths: [1800, 4560, 1440, 1560],
      rows: [
        new TableRow({
          children: [
            dataCell([ph('deadlines.DueDate')], 1800),
            dataCell([ph('deadlines.Description')], 4560),
            dataCell([ph('deadlines.DeadlineType')], 1440),
            dataCell([ph('deadlines.Status')], 1560)
          ]
        })
      ]
    }),

    ifEnd(),
    loopEnd(),

    spacer(),

    // All deadlines header table
    h2('All Deadlines'),

    new Table({
      width: { size: CONTENT_WIDTH, type: WidthType.DXA },
      columnWidths: [1800, 3360, 1440, 1200, 1560],
      rows: [
        new TableRow({
          children: [
            headerCell('Due Date', 1800),
            headerCell('Description', 3360),
            headerCell('Type', 1440),
            headerCell('Priority', 1200),
            headerCell('Status', 1560)
          ]
        })
      ]
    }),

    loopStart('deadlines'),

    new Table({
      width: { size: CONTENT_WIDTH, type: WidthType.DXA },
      columnWidths: [1800, 3360, 1440, 1200, 1560],
      rows: [
        new TableRow({
          children: [
            dataCell([ph('deadlines.DueDate')], 1800),
            dataCell([ph('deadlines.Description')], 3360),
            dataCell([ph('deadlines.DeadlineType')], 1440),
            dataCell([ph('deadlines.Priority')], 1200),
            dataCell([ph('deadlines.Status')], 1560)
          ]
        })
      ]
    }),

    loopEnd(),

    spacer(),

    // Notes section
    h2('Attorney Notes'),
    loopStart('deadlines'),
    ifStart('deadlines.Notes'),
    para([
      bold(ph('deadlines.DueDate')), text(' — '),
      ph('deadlines.DeadlineType'), text(': '),
      ph('deadlines.Notes')
    ]),
    ifEnd(),
    loopEnd(),

    rule(),

    para([
      text('Prepared by '),
      ph('attorneys.FirstName'), text(' '), ph('attorneys.LastName'),
      text(' on '), ph('{{TODAY}}'), text('. Confidential — Attorney Work Product.')
    ], { alignment: AlignmentType.CENTER })
  ];
}

What this demonstrates: The Deadline Calendar shows three different uses of the ForEach loop in a single template — filtered loops (only Critical, only High), and a full unfiltered loop for the complete table. The ifStart() / ifEnd() markers filter loop iterations to show only records matching a condition. The notes section at the bottom shows a conditional within a loop — notes paragraphs only appear for deadlines that have notes populated. This pattern — filter a loop with a conditional — is one of the most useful patterns in domain template design.


Template 3: Invoice (Complex — Nested Data, Financial Calculation)

The Invoice is the most complex template in the legal services domain. It draws from six tables, iterates over billing entries, handles three different billing types with conditional sections, and presents financial totals. Study this one carefully — it demonstrates every advanced pattern the platform supports.

function createInvoice() {
  return [
    // Invoice title and number
    h1('INVOICE'),
    labelValue('Invoice Number', ph('invoices.InvoiceNumber')),
    labelValue('Invoice Date', ph('invoices.InvoiceDate')),
    labelValue('Due Date', ph('invoices.DueDate')),

    spacer(),

    // Firm and client in two-column layout
    h2('From'),
    para([bold(ph('firms.FirmName'))]),
    para([ph('firms.Address')]),
    para([
      ph('firms.City'), text(', '),
      ph('firms.State'), text(' '),
      ph('firms.ZipCode')
    ]),
    para([ph('firms.Phone')]),
    para([ph('firms.Email')]),

    spacer(),

    h2('Bill To'),
    // Conditional: Individual vs Entity client
    ifStart('clients.ClientType=Individual'),
    para([ph('clients.FirstName'), text(' '), ph('clients.LastName')]),
    ifEnd(),
    ifStart('clients.ClientType=Entity'),
    para([bold(ph('clients.CompanyName'))]),
    para([text('Attn: Legal Department')]),
    ifEnd(),
    para([ph('clients.Address')]),
    para([
      ph('clients.City'), text(', '),
      ph('clients.State'), text(' '),
      ph('clients.ZipCode')
    ]),
    para([ph('clients.Email')]),

    rule(),

    // Matter reference
    h2('Re: Legal Services'),
    labelValue('Matter', ph('matters.MatterNumber'), text(' — '), ph('matters.Description')),
    labelValue('Responsible Attorney', ph('attorneys.FirstName'), text(' '), ph('attorneys.LastName')),
    labelValue('Billing Type', ph('matters.BillingType')),

    spacer(),

    // ─── HOURLY BILLING SECTION ──────────────────────────────────────────────
    ifStart('matters.BillingType=Hourly'),

    h2('Time and Services'),

    new Table({
      width: { size: CONTENT_WIDTH, type: WidthType.DXA },
      columnWidths: [1440, 3600, 1560, 1560, 1200],
      rows: [
        new TableRow({
          children: [
            headerCell('Date', 1440),
            headerCell('Description', 3600),
            headerCell('Attorney', 1560),
            headerCell('Hours', 1560),
            headerCell('Amount', 1200)
          ]
        })
      ]
    }),

    loopStart('billing_entries'),

    new Table({
      width: { size: CONTENT_WIDTH, type: WidthType.DXA },
      columnWidths: [1440, 3600, 1560, 1560, 1200],
      rows: [
        new TableRow({
          children: [
            dataCell([ph('billing_entries.EntryDate')], 1440),
            dataCell([ph('billing_entries.Description')], 3600),
            dataCell([
              ph('attorneys.FirstName'), text(' '),
              ph('attorneys.LastName')
            ], 1560),
            dataCell([ph('billing_entries.Hours')], 1560),
            dataCell([ph('billing_entries.Amount')], 1200)
          ]
        })
      ]
    }),

    loopEnd(),

    spacer(),

    // Hourly totals
    new Table({
      width: { size: CONTENT_WIDTH, type: WidthType.DXA },
      columnWidths: [7560, 1800],
      rows: [
        new TableRow({
          children: [
            shadedCell([bold('Total Hours')], 7560),
            shadedCell([bold(ph('invoices.TotalHours'))], 1800)
          ]
        }),
        new TableRow({
          children: [
            shadedCell([bold('Total Fees')], 7560),
            shadedCell([bold(ph('invoices.TotalAmount'))], 1800)
          ]
        })
      ]
    }),

    ifEnd(),

    // ─── FLAT FEE BILLING SECTION ─────────────────────────────────────────────
    ifStart('matters.BillingType=Flat Fee'),

    h2('Professional Services'),

    new Table({
      width: { size: CONTENT_WIDTH, type: WidthType.DXA },
      columnWidths: [7560, 1800],
      rows: [
        new TableRow({
          children: [
            headerCell('Services', 7560),
            headerCell('Fee', 1800)
          ]
        }),
        new TableRow({
          children: [
            dataCell([ph('matters.Description')], 7560),
            dataCell([ph('matters.FlatFee')], 1800)
          ]
        }),
        new TableRow({
          children: [
            shadedCell([bold('Total Fixed Fee')], 7560),
            shadedCell([bold(ph('invoices.TotalAmount'))], 1800)
          ]
        })
      ]
    }),

    ifEnd(),

    // ─── CONTINGENCY BILLING SECTION ──────────────────────────────────────────
    ifStart('matters.BillingType=Contingency'),

    h2('Contingency Fee Statement'),
    para([
      text('This matter is being handled on a contingency fee basis. '),
      text('No legal fees are owed unless and until a recovery is obtained.')
    ]),
    labelValue('Agreed Contingency Percentage', ph('matters.ContingencyRate')),

    ifEnd(),

    spacer(),
    rule(),
    spacer(),

    // ─── PAYMENT SUMMARY ──────────────────────────────────────────────────────
    h2('Payment Summary'),

    new Table({
      width: { size: CONTENT_WIDTH, type: WidthType.DXA },
      columnWidths: [7560, 1800],
      rows: [
        new TableRow({
          children: [
            dataCell([text('Total Charges')], 7560),
            dataCell([ph('invoices.TotalAmount')], 1800)
          ]
        }),
        new TableRow({
          children: [
            dataCell([text('Payments Received')], 7560),
            dataCell([ph('invoices.AmountPaid')], 1800)
          ]
        }),
        new TableRow({
          children: [
            shadedCell([bold('Balance Due')], 7560),
            shadedCell([bold(ph('invoices.Balance'))], 1800)
          ]
        })
      ]
    }),

    spacer(),

    // Retainer application note
    ifStart('matters.RetainerAmount'),
    para([
      text('Note: A retainer of '),
      ph('matters.RetainerAmount'),
      text(' was collected at the outset of this matter and has been applied to fees as earned.')
    ]),
    ifEnd(),

    spacer(),

    // Payment instructions
    h2('Payment Instructions'),
    para([
      text('Please remit payment by '),
      ph('invoices.DueDate'),
      text('. Make checks payable to '),
      ph('firms.FirmName'),
      text(' or pay online at '),
      ph('firms.Website'), text('.')
    ]),
    para([
      text('Questions regarding this invoice? Contact '),
      ph('attorneys.FirstName'), text(' '), ph('attorneys.LastName'),
      text(' at '), ph('attorneys.Email'),
      text(' or '), ph('attorneys.Phone'), text('.')
    ]),

    spacer(),
    rule(),

    para([
      text('This invoice is confidential and intended solely for '),
      ph('clients.FirstName'), text(' '), ph('clients.LastName'),
      text('. '),
      ph('firms.FirmName'), text(' | '),
      ph('firms.Address'), text(', '),
      ph('firms.City'), text(', '), ph('firms.State'), text(' '), ph('firms.ZipCode')
    ], { alignment: AlignmentType.CENTER })
  ];
}

What this demonstrates: The Invoice is the crown jewel of complex templates. It shows three mutually exclusive conditional sections — Hourly, Flat Fee, and Contingency — that render completely different content based on a single field value. It shows a loop inside a conditional (billing_entries loop only renders for Hourly matters). It shows a conditional within the payment section (retainer note only appears when a retainer amount exists). And it shows the complete financial summary pattern — total charges, payments received, balance due — that appears in some form in every professional services domain.


The Metadata JSON Files

Each .docx template needs a companion .json metadata file. The generator creates these automatically alongside each template. Add this to your script:

function createMetadata(template) {
  return {
    id: template.id,
    filename: template.filename,
    name: template.name,
    description: template.description,
    csvFiles: template.csvFiles,
    generatedAt: new Date().toISOString(),
    version: '1.0.0'
  };
}

The Generation Engine

This is the loop that drives everything. It runs each template function, writes the .docx file, and writes the metadata JSON. Add this at the bottom of your script:

async function generateAllTemplates() {
  console.log('\n🏛️  Legal Services Domain Template Generator');
  console.log('════════════════════════════════════════════');
  console.log(`📁 Output directory: ${OUTPUT_DIR}\n`);

  const index = [];
  let successCount = 0;
  let errorCount = 0;
  const startTime = Date.now();

  for (const template of templates) {
    try {
      process.stdout.write(`  Generating Template ${String(template.id).padStart(2, '0')}: ${template.name}...`);

      // Build the document
      const children = template.content();
      const headerName = '<<firms.FirmName>>';
      const doc = makeDocument(headerName, children);

      // Write the .docx file
      const buffer = await Packer.toBuffer(doc);
      const docxPath = path.join(OUTPUT_DIR, template.filename);
      fs.writeFileSync(docxPath, buffer);

      // Write the .json metadata file
      const jsonFilename = template.filename.replace('.docx', '.json');
      const jsonPath = path.join(OUTPUT_DIR, jsonFilename);
      fs.writeFileSync(jsonPath, JSON.stringify(createMetadata(template), null, 2));

      // Add to index
      index.push({
        id: template.id,
        filename: template.filename,
        name: template.name,
        description: template.description,
        category: template.category || 'General',
        csvFiles: template.csvFiles
      });

      console.log(' ✓');
      successCount++;

    } catch (err) {
      console.log(` ✗ ERROR: ${err.message}`);
      errorCount++;
    }
  }

  // Write the TEMPLATE_INDEX.json
  const indexPath = path.join(OUTPUT_DIR, 'TEMPLATE_INDEX.json');
  fs.writeFileSync(indexPath, JSON.stringify({
    domain: 'legal-services',
    version: '1.0.0',
    generatedAt: new Date().toISOString(),
    templateCount: successCount,
    templates: index
  }, null, 2));

  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);

  console.log('\n════════════════════════════════════════════');
  console.log(`✅ ${successCount} templates generated successfully`);
  if (errorCount > 0) {
    console.log(`❌ ${errorCount} templates failed — check errors above`);
  }
  console.log(`📄 TEMPLATE_INDEX.json written`);
  console.log(`⏱️  Total time: ${elapsed} seconds`);
  console.log(`📁 Output: ${OUTPUT_DIR}`);
  console.log('════════════════════════════════════════════\n');
}

// Run it
generateAllTemplates().catch(err => {
  console.error('Fatal error:', err);
  process.exit(1);
});

Running the Generator

With all 20 template functions written and the generation engine in place, you have one command to run:

node scripts/generate-legal-services-templates.js

The output looks like this:

🏛️  Legal Services Domain Template Generator
════════════════════════════════════════════
📁 Output directory: .../DataPublisher_DomainTemplates/legal-services/word-templates

  Generating Template 01: Engagement Letter... ✓
  Generating Template 02: Client Status Update... ✓
  Generating Template 03: Invoice... ✓
  Generating Template 04: Payment Receipt... ✓
  Generating Template 05: Case Summary Letter... ✓
  Generating Template 06: Retainer Agreement... ✓
  Generating Template 07: Conflict of Interest Disclosure... ✓
  Generating Template 08: File Closing Letter... ✓
  Generating Template 09: Attorney-Client Privilege Notice... ✓
  Generating Template 10: Hearing Preparation Sheet... ✓
  Generating Template 11: Deposition Outline... ✓
  Generating Template 12: Court Appearance Checklist... ✓
  Generating Template 13: Deadline Calendar... ✓
  Generating Template 14: Matter Status Report... ✓
  Generating Template 15: Attorney Workload Summary... ✓
  Generating Template 16: Active Case List... ✓
  Generating Template 17: Client Contact Sheet... ✓
  Generating Template 18: Monthly Billing Summary... ✓
  Generating Template 19: Attorney Biography Sheet... ✓
  Generating Template 20: New Client Welcome Package... ✓

════════════════════════════════════════════
✅ 20 templates generated successfully
📄 TEMPLATE_INDEX.json written
⏱️  Total time: 18.4 seconds
📁 Output: .../DataPublisher_DomainTemplates/legal-services/word-templates
════════════════════════════════════════════

18 seconds. Not 30 minutes. Not 80 hours. 18 seconds.

Your word-templates/ directory now contains 41 files: 20 .docx templates, 20 companion .json metadata files, and one TEMPLATE_INDEX.json. Every template is consistently formatted with the same fonts, the same heading hierarchy, the same placeholder syntax, the same professional letterhead header, and the same page number footer. That consistency was not designed — it was guaranteed by the helper functions.


The Regeneration Principle

Here is the rule that makes programmatic generation so powerful, and that you must internalize before you move on:

You never edit a generated .docx file directly.

Ever.

If a template needs to change — a layout adjustment, a new field added, a heading renamed — you change the template function in the generator script and run it again. The new .docx file overwrites the old one. All 20 templates stay consistent. Your generator script remains the single source of truth.

The moment you edit a .docx file directly, you have created a divergence between your script and your templates. The next time you run the generator, your hand-edit will be overwritten. And if you forget that the edit exists and regenerate without reapplying it, the divergence disappears silently — the worst kind of bug.

The discipline: generator script is master, .docx files are output. Always.


When Templates Have Errors

Sometimes a template function throws an error when the generator runs. The generation engine catches these errors, logs them, and continues to the next template — so one broken template does not abort the entire run.

Common errors and their fixes:

"Cannot read properties of undefined" — A helper function received undefined instead of a string. Check that every ph() call references a real column name. ph('clients.FistName') (typo) will fail; ph('clients.FirstName') will not.

"children is not iterable" — A Paragraph or TableCell was given a non-array children value. Check that every para() call with multiple runs passes them as an array: para([ph('x'), text(' '), ph('y')]), not para(ph('x'), text(' '), ph('y')).

"Table columnWidths must sum to table width" — Your column widths do not add up to CONTENT_WIDTH (9360). Add them up. They must sum exactly. Off-by-one errors produce tables that render with a gap or overflow.

Template generates but placeholder shows literal text — The placeholder syntax is incorrect. <<clients.FirstName>> is correct. <clients.FirstName> (single angle bracket), <<FirstName>> (missing table prefix), and <<clients.firstname>> (wrong case) will all render as literal text rather than being replaced with data.


The Script Is Version-Controlled

Your generator script is code. It belongs in version control alongside your CSV files and domain-config.json. When you release version 1.1 of your domain with new templates or updated layouts, you commit the updated script alongside the new generated templates. Anyone who forks your domain can trace the exact history of every template decision.

This is one of the quiet advantages of programmatic generation that manual template creation can never offer: your template design decisions are explicit, readable, and reviewable in the script. A future collaborator does not have to reverse-engineer a .docx file to understand why a table has five columns — they read the generator function and see exactly why.


Chapter Summary

The generator script uses the docx npm package to create all 20 Word templates programmatically from JavaScript function definitions. It runs in under 30 seconds and produces 41 files: 20 templates, 20 metadata files, and one index.

Helper functions — text(), bold(), ph(), loop(), cond(), h1(), h2(), para(), labelValue(), rule(), loopStart(), loopEnd(), ifStart(), ifEnd(), headerCell(), dataCell(), shadedCell(), and makeDocument() — are the building blocks of every template. Define them once, use them everywhere. Consistency is automatic.

Three complexity levels cover all template patterns: Simple (single-table, no loops), Moderate (multi-table with ForEach loops), and Complex (nested loops, conditionals, financial calculations).

The generator script is master. Generated .docx files are output. Never edit output directly — always change the script and regenerate.


What's Next

Chapter 6 covers the README — the document that is simultaneously your user guide, your marketplace listing, and your sales pitch. Your domain now has everything under the hood. Chapter 6 puts the face on it.


Volume 5 — Building Intelligent Systems: Building Organizational Knowledge Systems on Data Publisher for Word Part of the Building Intelligent Systems Series