Mail Merge from CSV
Paste a template with {{column}} placeholders and a CSV — get one filled-in document per row. Runs entirely in your browser, no upload, no sign-up.
About mail merge
This tool does a classic mail merge: it fills a text or markdown template
once for every row of a CSV, then joins the results. Each {{Column}}
placeholder in the template is replaced by that row's value for the matching CSV
header column, so one small template plus a spreadsheet of data becomes a batch
of personalized letters, emails, labels, reminders, or messages.
Everything runs locally in your browser. Your template and data are never uploaded to a server, and it works offline.
How it works
- Template — write your text and drop
{{name}}style placeholders where the per-row values should go. The name inside the braces must match a column header in your CSV. - CSV data — paste rows where the first line is the header (the column names) and each following line is one record.
- Pick the placeholder style, the CSV delimiter, and a separator to put between the generated documents, then run.
Worked example
Template:
Hi {{name}},
Your invoice for ${{amount}} is due on {{due}}.
Thanks!
CSV:
name,amount,due
Alice,120,2026-08-01
Bob,90,2026-08-05
Output (with the default --- divider between rows):
Hi Alice,
Your invoice for $120 is due on 2026-08-01.
Thanks!
---
Hi Bob,
Your invoice for $90 is due on 2026-08-05.
Thanks!
Options
- Placeholder style —
{{column}}(double curly, default),{column}(single curly), or<<column>>(double angle). Pick whichever doesn't clash with literal braces in your template. - CSV delimiter — comma (default), semicolon (common in European exports), or tab (TSV).
- If a placeholder column is missing — replace with nothing (the usual
mail-merge behavior), keep the
{{placeholder}}text (handy for catching a typo in a field name), or show an error. - Case-insensitive matching — on by default, so
{{First Name}}matches a header calledfirst name. - Separator between documents — a
---rule (default), a blank line, a single newline, a page break (form feed), or none.
Limits and edge cases
- Up to 1000 data rows per merge (an in-browser memory bound). Split larger lists into batches.
- A placeholder for a column that exists but is empty on a given row always renders as an empty string, regardless of the missing-column setting.
- A row with fewer fields than the header treats the absent trailing cells as empty.
- Quoted CSV fields (
"a, b") may contain the delimiter and newlines. - This is plain named substitution — there are no loops, conditionals, or expressions. For Handlebars/Mustache logic against a single JSON object, use the render-template tool instead.
FAQ
What placeholder syntax should I use?
By default, wrap a column name in double curly braces: {{name}}. If your text
already contains literal braces, switch the placeholder style to {column}
or <<column>> so your merge fields don't clash with them.
Does the first CSV row have to be a header?
Yes. The first line is read as the column names, and every line after it is a data row that produces one output. Placeholders are matched to those header names.
What happens if a placeholder's column isn't in the CSV?
It depends on the If a placeholder column is missing setting: replace with
nothing drops it, keep the placeholder text leaves {{that}} in place so you
can spot the mismatch, and show an error stops the merge and names the missing
column. Note a column that exists but is blank for a row always renders empty.
My data uses semicolons or tabs — is that supported?
Yes. Set the CSV delimiter to semicolon (common in European spreadsheet exports) or tab (for TSV). Quoted fields containing the delimiter or newlines are handled correctly.
Can I do loops or if/else in the template?
No — mail merge is plain {{column}} substitution repeated per row. For
Handlebars/Mustache logic ({{#each}}, {{#if}}, nested paths) against a single
JSON object, use the render-template tool.
Is my data uploaded anywhere?
No. The merge runs entirely in your browser using WebAssembly. Your template and CSV never leave your device, and the page works offline once loaded.
Developer & Automation Access
Run it from the terminal
Same engine as this page, headless — via the gizza CLI:
gizza tool mail-merge 'Hi {{name}},
Your invoice for ${{amount}} is due on {{due}}.
Thanks!' 'csv=name,amount,due
Alice,120,2026-08-01
Bob,90,2026-08-05'New to the CLI? Get gizza →
Open it by URL
Pre-fill and auto-run this tool with query parameters — the names match the API/CLI:
https://gizza.ai/tools/mail-merge/?template=Hi%20%7B%7Bname%7D%7D%2C%0A%0AYour%20invoice%20for%20%24%7B%7Bamount%7D%7D%20is%20due%20on%20%7B%7Bdue%7D%7D.%0A%0AThanks%21&csv=name%2Camount%2Cdue%0AAlice%2C120%2C2026-08-01%0ABob%2C90%2C2026-08-05&syntax=double_curly&delimiter=comma&on_missing=empty&case_insensitive=true&separator=dividerMachine-readable descriptor: tool.json — title + parameters JSON Schema for agents.
