1. Connect Tavily and narrow its tools
GET /v1/integrations/{integration_id}/mcp-tools lists the tools live from the provider, which also proves the key works. A new integration approves every tool (allowed_tools: null); narrow it to the two this task needs, so the model chooses from fewer:
2. Write the task
task.json
varsare the inputs. The batch fills them from CSV columns of the same name. Arequiredvariable with no column and no default is refused when the batch is created.- Each output
descriptionis the prompt for that value. It becomes the argument description of the generatedsubmit_resulttool. Say what good looks like and when to send null. required: falselets a field benull. Every field must still be sent, so “could not find it” is explicit rather than a missing key. See inputs and output.max_steps(default 25, max 50) counts model → tools → model rounds, not tool calls.timeout_seconds(default 300, max 600) is wall-clock and usually the real bound. See errors and limits.
3. Run it once and read the result
task_version: "draft" runs the unpublished config. The request blocks until the run finishes, so set your client’s timeout above timeout_seconds (the Python SDK defaults to 30 seconds).
Run it on a domain you know well, so you can tell a true sentence from a plausible one. When it is right, publish — a batch uses the published version:
4. Connect Resend
from_email; one on an unverified domain is refused.
5. Create the batch
Each row has one flat column space: your CSV’s columns plus the task’s output fields.field_map points to, subject and body at names in that space — a CSV header or a string output field. Here the address comes from the CSV and the words from the task.
- Drafting starts immediately and runs at any hour;
windowapplies to sending.draft_concurrency(1–10, default 5) is how many rows draft at once. - To have the task find the address too, add an output field such as
work_emailand map"to": "work_email". - Refused at create, in one 400: a mapped name that is neither a column nor an output field, a mapped output that is not a
string, a CSV header that collides with an output name, a required task variable with no column or default, an unverifiedfrom_email. See creating a batch. - A column that exists but is blank on some rows passes create; those rows fail at drafting with
invalid_input, without running the model.
6. Review and send
When drafting finishes, the batch status isdrafted (Ready to review). Nothing has been sent. List the drafts; not_ready_reason says why a row cannot go yet:
scope: "all" takes every draft; scope: "selected" with recipient_ids takes only those rows.
Test it
- Run the task by hand on a domain you know. If
specific_detailis generic, every row will be. - Create a two-row batch with your own addresses and watch
scheduled→drafting→drafted. - Blank one row’s
domainbefore upload; it should fail alone withinvalid_input. - Send both to yourself and check line breaks,
from_nameand reply-to.
What to know before scaling
- Republishing the task mid-batch changes later rows.
drafted_versionson the batch shows the split;POST /v1/email/batches/{batch_id}/redraftwithselection: "stale_version"reruns the older rows, at the cost of another task run each. - Dedup is per batch. One address is mailed at most once per batch; the same person on two batches gets two emails. Suppression of bounces and complaints is Resend’s.
Sentmeans Resend accepted it. Delivery, bounces and complaints are in the Resend dashboard.- A batch stops itself after ten consecutive drafting failures or one
configurationfailure. See monitoring.
Next
Reviewing and sending
Skipping rows, redrafting and the send in full.
Task tools and MCPs
What a task can reach while it works.