---
title: "Mail merge DOCX files in C#"
url: https://recursiveai.org/onedocument/cookbook/mail-merge/
description: "Fill Word merge fields from data to produce personalized DOCX letters."
version: "1.0.0"
---

Fill Word merge fields from data to produce personalized DOCX letters.

```shell
dotnet add package RecursiveAI.OneDocument.Documents
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneDocument.Documents;
using System.Text.Json;

using var data = JsonDocument.Parse(
    """[{"Name":"Ada"},{"Name":"Lin"}]""");
using var doc = Document.Open("letter.docx");
using var result = doc.MailMerge(data);
for (int i = 0; i < result.Documents.Count; i++)
    result.Documents[i].SaveAs($"letter-{i+1}.docx");
```

Writes `letter-1.docx` and `letter-2.docx` for Ada and Lin; the snippet prints nothing. The input file is in the zip.

Unzip [the example](https://recursiveai.org/onedocument/cookbook/mail-merge/mail-merge.zip), then `dotnet run`.

## Full program

`letter.docx` is included in the example. It contains `Dear «Name»,` where «Name» is a real Word `MERGEFIELD Name` field, not typed angle brackets. In Word, insert the field through Insert › Quick Parts › Field › MergeField, then enter `Name` as the field name. The JSON records are shown in the snippet.

```csharp
using RecursiveAI.OneDocument.Documents;
using RecursiveAI.OneDocument.Documents.MailMerge;
using System.Text.Json;

using var data = JsonDocument.Parse(
    """[{"Name":"Ada"},{"Name":"Lin"}]""");
using var doc = Document.Open("letter.docx");
using var result = doc.MailMerge(data);
for (int i = 0; i < result.Documents.Count; i++)
{
    string path = $"letter-{i + 1}.docx";
    result.Documents[i].SaveAs(path);
    Console.WriteLine(path);
    Console.WriteLine(result.Documents[i].ExtractText().Trim());
}
Console.WriteLine($"Warnings: {result.Warnings.Count}");
```

`dotnet run` prints:

```text
letter-1.docx
Dear Ada,
letter-2.docx
Dear Lin,
Warnings: 0
```

A missing merge field becomes empty and adds `mailmerge.field-missing` to `result.Warnings`; unsupported merge fields stay in place with a warning.

## Variations

### Combine the letters

Keep the full program through `Document.Open`, then replace the rest with:

```csharp
using var result = doc.MailMerge(data,
    new MailMergeOptions {
        Mode = MailMergeMode.SingleDocument
    });
result.Documents[0].SaveAs("letters.docx");
Console.WriteLine("Wrote letters.docx");
Console.WriteLine($"Documents: {result.Documents.Count}");
```

Output:

```text
Wrote letters.docx
Documents: 1
```

## Related recipes

- [Find and replace text in a Word document in C#](https://recursiveai.org/onedocument/cookbook/find-and-replace-in-word/)
- [Convert PowerPoint to PDF in C#](https://recursiveai.org/onedocument/cookbook/convert-pptx-to-pdf/)
- [Fill a Word template in C#](https://recursiveai.org/onedocument/cookbook/fill-word-template/)
- [Extract text from DOCX, XLSX and PPTX in C#](https://recursiveai.org/onedocument/cookbook/extract-text-from-office-files/)

## The short answer

Open a DOCX main document containing real Word merge fields, call `doc.MailMerge(data)` and save each document in the result. The default is one document per record. The complete example passes two JSON records and saves personalized letters for Ada and Lin.

A merge field is document markup, not plain text that happens to look like a placeholder. The input in this example contains a `MERGEFIELD Name` instruction whose displayed result is `Name`. Typing the characters «Name» into an ordinary paragraph does not create that field. For plain tag-based placeholders, use [Word templates](https://recursiveai.org/onedocument/cookbook/fill-word-template/) instead.

## How do I mail merge a Word document in C#?

Prepare the main document with Word merge fields and provide a sequence of records. OneDocument accepts the same data forms as its template engine, including JSON, dictionaries and generated template models. The JSON array above makes the record boundary visible: each object supplies a `Name` value for one pass through the letter.

Use a file-backed main document, as in `Document.Open("letter.docx")`. Mail merge saves and reopens the main document once per pass. A document created only over an in-memory package is not a usable main document for this operation and raises `OfficeFormatException`; save it and reopen the saved file first.

The result owns its output documents, so keep it alive while saving them and dispose it afterward. The source template is not changed. An empty data sequence produces no output documents; check the count before indexing the result in code that may receive no records. The simple example has two records, so it can safely save two files.

Field names are case-insensitive. Simple and complex fields are both read, including instructions split across runs. A missing name merges as empty and adds a located `mailmerge.field-missing` warning. Don’t judge success only from whether files exist: inspect `result.Warnings` when preparing a batch for delivery. See the [mail-merge reference](https://recursiveai.org/onedocument/docs/mail-merge/) for field switches and defect codes.

## How do I split a mail merge into individual Word documents?

Keep the default `PerRecord` mode and save each entry in `result.Documents` to a distinct path, as in the complete program. This produces separate documents directly; there is no combined file to split afterward.

For the opposite task, set `MailMergeOptions.Mode` to `MailMergeMode.SingleDocument`, as in the variation. That produces one document containing every emitted pass, separated by next-page section breaks. The variation saves `letters.docx` and reports one output document even though the data still has two records. Output-document count and record count mean different things in this mode.

Headers and footers come from the first record’s pass. Drawing, bookmark, comment and revision identifiers are renumbered so they remain unique in the combined file. If headers need a different recipient name for every letter, this first-pass rule is a reason to prefer per-record outputs rather than assuming single-document mode duplicates every story independently.

The engine understands more than simple name fields: conditional fields, record navigation and numeric or date pictures have documented rules. `NEXT` can consume another record in the same pass, and `SKIPIF` can omit a pass. This is why one pass is not always exactly one record in a sophisticated mailing-label document.

Some Word merge fields are deliberately not expanded, including `ADDRESSBLOCK`, `GREETINGLINE` and `DATABASE`. They remain as written with an unsupported-field warning. Other fields such as `PAGE` and `DATE` are untouched. Use explicit supported merge fields for addresses and greetings when you need predictable output, rather than assuming the merge operation recalculates every field in the document.
