POST /v1/email/batches takes a list, the agent task that drafts each row, a connected Resend account and a field map. It validates the whole combination, writes the rows and the drafting job in one transaction, and returns a scheduled batch. Nothing is sent. An invalid batch is a 400 listing every problem at once, not the first one — because telling you about row 12 when rows 12, 47 and 88 are wrong means three round trips through a five-thousand-row upload:

The column space

This is the part people get wrong, and everything else follows from it. A row has one flat column space, filled from two directions and edited from a third: They are merged in exactly that order — input, then output, then overrides — so a human edit always wins, and a re-draft can never silently discard one. The merged result is the row’s columns, and it is what the review table renders. field_map points to, subject and body at names in that merged space, and each may name either a CSV header or a field the task produces. That is the whole design in one control, and it is what makes these two the same feature:
In the first, email is a column of the file and subject_line / email_body are output fields of the task. In the second, all three are output fields — the task was given a phone number and a company, looked the person up with its tools, and returned work_email beside the copy it wrote. Nothing downstream can tell the difference, and nothing downstream needs to.

What makes a row sendable

A row can be sent when all three mapped names resolve to a non-empty string in the merged space, and the resolved to parses as an email address. Nothing else is checked. Every other field the task produces is kept on the row verbatim as context — visible in the review table, useful when you are deciding whether to send — and read by nobody. A task that returns confidence, industry and last_order_date beside its three mapped fields is doing something useful; those three values are never sent anywhere.

What the task has to look like

The drafting engine is an ordinary agent task: a prompt, a model, tools and MCP servers, plus a typed output. A batch adds two requirements on top of what inputs and output already says:
  • Anything you map must be a string output field. A task output field may be string, boolean, integer or number. The other three are fine to produce and are kept on the row, but a to, subject or body mapped at one is refused — an email’s subject has to be text.
  • The task’s variables are the list’s columns. Each row’s uploaded columns are handed to the task whole as its {{vars.*}} values — not filtered to what it declares — so a column the task does not read costs nothing, and a variable the task later declares picks up a column that was already there.
Output field names are lower-case letters, digits and underscores, starting with a letter. Column headers are letters, digits and underscores, starting with a letter or an underscore; a header that could not be read back as {{vars.name}} is refused at create.

Everything refused at create

All of these come back together in one 400.
A header that exists but is blank on some rows still passes create. The check is on the header, not the cells.Empty cells are dropped rather than stored as "", so a required variable on those rows reads as absent and the row fails at drafting time with missing_varsone row, not the batch, and with no tokens spent, because the check runs before the model is reached. The other rows draft normally, and you can fill the cell in and retry the row.That is the good version of this failure, and it is still worth avoiding. Count the blanks in every column a required variable reads before you create the batch.
The field map is re-validated on every drafting pass, not only at create. A task has no publish step, so an edit that renames the output field your mapping points at lands with nothing to announce it — and the batch stops on the next pass rather than producing four thousand rows that turn out to be unsendable. See monitoring.

Policy

Everything that is not the list, the task, the account or the mapping. calling_window applies to drafting only. Sending is never scheduled — it happens when a person presses send, whatever the clock says.

What happens after create

The batch is scheduled, and a drafting job exists for it in the same transaction that wrote the rows. From there:
  1. At start_at, or on the first pass if there is none, the batch moves to drafting and email_batch.started fires.
  2. Each pass claims up to max_concurrency rows, runs the task on each, and writes the result onto the row. If the calling window is shut it waits for the next opening instead.
  3. When the last row lands the batch becomes draftedReady to review in the dashboard — email_batch.drafted fires, and nothing about the batch is scheduled any more.
drafted means the drafts are ready. It says nothing at all about what has been sent.

A complete create

The response is an EmailBatchResponse. Two of its fields describe the column space directly: input_columns is every header your list carried, in the order it first appeared, and output_columns is every field the task declares — read live, so a task that grew a field shows the new column.

In the dashboard

Email → Outbound → New batch asks the same three questions in the order they have to be answered, because the third cannot be asked before the first two.
1

The list

Type rows in, upload a CSV, or both. The file is parsed in the browser and posted as JSON — it never leaves the tab as a file. Bad column names are flagged red on the header, and a file over 5,000 rows is refused where you chose it rather than after the form is filled in.
2

How it runs

The task, the Resend account, the sender, the body format, the schedule, the drafting hours and the pacing. This is the same form you get later when you edit a live batch, because they are the same fields.
3

The email

Three dropdowns — To, Subject, Body — each listing your file’s columns under From your file and the task’s output fields under a group named after the task. Non-text output fields appear greyed with (not text) beside them. Common names (email, subject, body) are pre-selected if they exist.Below them, in place of a 400 you would otherwise get: collisions between your columns and the task’s fields, required variables your file has no column for, and a count of how many rows have a blank cell in a required column.
Review and start drafting opens a confirmation that states the number of task runs you are about to pay for, that nothing is emailed by it, and that every row runs the task as it stands right now.

Review and send

Reading the drafts, editing cells, and the 50-row send cap.

Monitoring a batch

Statuses, counts, cost, and why a batch stops itself.