How to generate one document per record (mail merge) with an AI agent

Run the GroupDocs.Assembly MCP server locally and one prompt gives you a document per record — a welcome letter per customer, a certificate per attendee, an invoice per order — each file named after a field of its record, with any bad record listed instead of stopping the run:

GROUPDOCS_MCP_STORAGE_PATH=$(pwd)/templates dnx GroupDocs.Assembly.Mcp --yes

Write a welcome letter for every customer in customers.json, as PDFs named after the customer, and tell me which ones failed.

Note
The commands and config snippets on this page are for the .NET build of the server — the only platform available today. Installation and client setup: MCP server for .NET. Other platforms will expose the same tools with their own launch command; everything else on this page applies unchanged.

Prerequisites

  • The server registered in your AI client — quick start.
  • A template that speaks about one record (<<[customer.Name]>>, <<[customer.Plan]>>) and a JSON array of records in the storage folder.

The calls

customers.json  [ {Name:"Contoso Pharma", ...}, ... 5 records ]
        |
assemble_per_record {"template":{"filePath":"welcome-letter.docx"},
                     "data":{"filePath":"customers.json"},
                     "fileNameField":"Name","format":"pdf"}
        |
        +--> Contoso Pharma.pdf
        +--> Fabrikam Logistics.pdf
        +--> Tailspin Toys.pdf
        +--> Litware Labs.pdf
        '--> "Assembled 4 of 5 ... 1 record(s) failed: record 4: ...
              The template reads 'FirstInvoice', which the data does not have"
             (the bad record is listed; the other four are still written)

What the agent sees

Captured from server 26.11.0 with five customers — the fourth has no FirstInvoice (the evaluation note that opens each response is trimmed):

-> get_document_info {"file":{"filePath":"customers.json"}}
<- { "dataKind": "json", "data": { "root": "array", "length": 5 } }

-> get_template_fields {"template":{"filePath":"welcome-letter.docx"}}
<- { "dataSourceNames": [ "customer" ],
     "fields": [ "customer.Contact", "customer.Plan", "customer.Name",
                 "customer.Manager", "customer.FirstInvoice" ] }

-> assemble_per_record {"template":{"filePath":"welcome-letter.docx"},
     "data":{"filePath":"customers.json"},"fileNameField":"Name",
     "format":"pdf"}
<- Assembled 4 of 5 document(s) from 'welcome-letter.docx' with
   'customers.json', one per record (each record as 'customer'), as PDF.
   Saved to:
   …/Contoso Pharma.pdf
   …/Fabrikam Logistics.pdf
   …/Tailspin Toys.pdf
   …/Litware Labs.pdf
   1 record(s) failed:
   record 4: InvalidOperationException: ... Can not get the value of member
   'FirstInvoice' on type 'System.Data.DataRow'. The template reads
   'FirstInvoice', which the data does not have. ...

Three things to notice:

  • One record is customer. The server named each record after the template’s data source; no dataSourceName was needed.
  • The run did not stop. Four letters were written; record 4 is listed with the field it lacks. Fix the record, or pass allowMissingMembers: true to print nothing there.
  • Files are named by fileNameField. Without it they are numbered.

A safety cap

maxDocuments (default 100, at most 1000) refuses a run larger than you expected:

-> assemble_per_record {..., "maxDocuments":3}
<- Per-record assembly failed for 'welcome-letter.docx': there are 5 records,
   more than maxDocuments=3. Raise 'maxDocuments' (up to 1000) or pass fewer
   records.

Expected result

One PDF per good record in the storage folder, named after the customer, and a list of the records that failed with the reason for each.

Troubleshooting

SymptomCauseFix
“record 4: … The template reads ‘FirstInvoice’, which the data does not have.”that record lacks a fieldadd it, or allowMissingMembers: true
“there are 5 records, more than maxDocuments=3”the capraise maxDocuments
The array is inside an object ({"customers":[…]})the server needs to know whererecordsPath: "customers"

FAQ

Can the records come from a CSV or a spreadsheet? Per-record assembly reads JSON. The agent converts the rows to a JSON array first — or, for one document listing every row, use assemble_document with a <<foreach>>.

Can each letter go out by e-mail? Use an e-mail template (.eml/.msg) and format: "eml" to get one message file per record; sending them is a job for your mail tooling. Unlicensed, the HTML body is replaced by the evaluation line.

How large can a run be? Up to 1000 records per call with maxDocuments; split larger sets.

Add to your CLAUDE.md or AGENTS.md

Agents follow project instructions about when to use a tool. Paste this into the instruction file your agent reads (CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, .cursorrules):

For one document per person/order, use groupdocs-assembly assemble_per_record
with fileNameField set to a unique field. Report failed records by number and
field; never fill missing values by guessing.

Honest limits

  • Evaluation mode: every generated document starts with an Evaluation Only line, workbooks gain a worksheet, and e-mail templates lose their HTML body. Licensing.
  • <<[name]>>, not {{name}}: other engines’ placeholders stay in the output as text.
  • Templates are trusted input: image and document tags load the paths and URLs their expressions return.
  • Dates: write them as "2026-11-12T00:00:00" and format them in the template; a date-only string can shift by a day on machines east of UTC.

Read more