You are going to build a two-part pipeline: an agent task that takes a company domain, searches the web, and returns a typed result — including a subject line and a body written about that specific company — and an email batch that runs the task once per row of a list, puts every draft in front of you as a spreadsheet, and sends only the rows you name. When it is finished, one CSV plus one press of a button produces drafts; a second, separate, deliberate press sends them.
Resend’s Acceptable Use Policy prohibits unsolicited email. You are “prohibited from sending unsolicited messages of any kind, including cold outreach, purchased lists, or scraped contact data”, and “all mail must be sent to recipients who have explicitly opted in to receive communications from you”. This pipeline runs on your Resend account under your agreement with them, and the consequences of breaking it — a suspended account, a burned sending domain — land on you. Read resend.com/legal/acceptable-use before you upload a list. A list of people who asked to hear from you is the only list this feature is for.And one of ours: there is no spend cap on drafting. Every row runs the task and is billed — your model tokens, plus whatever its tools and MCP servers charge per call — whether or not the draft is ever sent. A five-thousand-row batch costs five thousand task runs with zero emails sent. Nothing anywhere caps that.

What you need

The build

1

Connect the research integration and narrow it to what you need

Tavily and Exa take an API key and are connected over the API. HubSpot, RocketReach, Asana, Jira, Cal.com, Calendly and Google Calendar are OAuth-only and must be connected from the dashboard’s Integrations page.
Then list what the server actually offers. This is a live call to the provider, so it also proves the connection works, and it returns each tool under both its own name and the exposed_name the model will see.
Now narrow the approval to the two tools this task needs. allowed_tools is null on a new connection, which means every tool the server lists; a PATCH replaces the list wholesale, and it can never be empty.
Approval matches the tool’s own name; the namespace is applied afterwards. Narrowing is worth the trouble because every approved tool is described to the model on every round — a task picking from six tools is faster, cheaper and more accurate than the same task picking from ninety. See MCP servers.
2

Write the task

A task is inputs, a prompt, a model, tools and a typed output. Here is the whole thing.
Four decisions in that config are worth being deliberate about.vars are the inputs, and the batch fills them from the CSV. Each row’s uploaded columns are handed to the task as its {{vars.*}} values, so a column named domain becomes {{vars.domain}}. A required variable with no column and no default is refused when the batch is created, before a single row runs.Every description is the only instruction the model gets. It becomes the description of that argument in the generated submit_result tool, so it is literally the prompt for the value. "The subject line" produces subject lines; the description above produces subject lines under sixty characters that name the company. Write it as: what good looks like, where to look, and what to send when the answer is not findable.Every output field is required and nullable. The model has to say something about each one, including “I could not find it” — which is why each description ends with a null case. An optional field lets the model silently omit the address it failed to find, and then “omitted” and “not generated yet” look identical to whoever reads the row. One of those is a fact and the other is a bug. See inputs and output.max_steps and timeout_seconds are two different budgets. max_steps (default 25, max 50) counts model → tools → model rounds, not tool calls: four searches in one reply cost one step. Twelve is generous for search-then-read-then-write and low enough to catch a loop. timeout_seconds (default 120, max 600) is wall-clock, and it is what actually bounds a run — a run that ends without calling submit_result is re-prompted, and each re-prompt starts the step budget over.
A task has no draft and no publish step. Saving is what makes it live, and an edit takes effect on the next run with nothing in between. There are no versions, so there is nothing to roll back to and nothing records which wording produced an earlier run. create_task and update_task return a warnings array beside the saved task — a prompt reading a variable nothing declares, a task with no prompt — and there is no publish screen to show them on, so read them where they come back.
3

Run it once by hand and read the trace

Never point a batch at a task you have not run. One run costs one run; five thousand rows of a bad prompt costs five thousand.
The request blocks for as long as the run takes, so set your client’s read timeout above timeout_seconds — the Python SDK defaults to 30 seconds and will abandon a run that is perfectly healthy.Then read three numbers before you read the output:A run that fails carries error.type, and the vocabulary says whose problem it is: no_output means fix the prompt, step_limit means fix max_steps or narrow the work, configuration means a missing key or a deleted tool is yours to fix. See errors and limits.
4

Connect Resend and verify a sending domain

Every batch sends through one connected Resend account, on your own API key. Talqing holds no email credential of its own.
Verification is domain-level — once yourdomain.com is verified, any local part on it sends — and the list is read live off the account rather than cached, because a stale one would offer you a sender that starts failing twenty minutes later. A from_email on an unverified domain is refused at create, at patch and at every send press. See email setup.
5

Map the CSV and the task output onto to, subject and body

A row has one flat column space, filled from two directions: your CSV supplies some names, the task’s output supplies the rest, and a person’s edits sit on top. field_map points to, subject and body at names in that merged space, and each may be either a CSV header or a task output field.That is why these two are the same feature:
In the first, email is a header of your file and the other two are output fields of the task. In the second, all three are output fields — which means the task has to declare work_email in its output, with a description saying where to look and to send null rather than guess. Nothing downstream can tell the two cases apart.A row is sendable when all three resolve to a non-empty string and to parses as an address. Every other field the task produces — company_name, what_they_do, specific_detail — is kept on the row as context you read while deciding, and is sent nowhere.Refused at create, all together in one 400: a mapped name that is neither a column nor an output field; a mapped output field that is not a string; a CSV header that collides with an output field name; a required task variable with no column and no default; an unverified from_email. The full list is in creating a batch.
A header that exists but is blank on some rows still passes create. The check is on the header, not the cells. Those rows fail at drafting time with missing_vars — one row, not the batch, and with no tokens spent, because the check runs before the model is reached. Count the blanks in every column a required variable reads before you upload.
6

Create the batch and let it draft

max_concurrency (1–10, default 5) is how many rows draft at once, and it is the only limit in the system — each row is a model run, and a task that fans out to a provider that rate-limits or charges per call will feel it here. calling_window applies to drafting only; sending is never scheduled.In the dashboard this is Email → Outbound → New batch, whose last step is three dropdowns — To, Subject, Body — listing your file’s columns under From your file and the task’s output fields under the task’s name, with non-text fields greyed out.
7

Review, fix a cell, and send fifty at a time

When the last row lands the batch reads drafted, shown as Ready to review. That means the drafts are ready. It says nothing about what has been sent.Read the rows, with not_ready_reason telling you why any of them cannot go yet — it is computed live on every read, never stored:
A row whose task returned null for the address is fixed by typing one in. An edit is stored apart from what the model wrote and merged last, so it always wins and a re-draft can never silently discard it. An empty value clears the override and falls back to whatever was underneath.
Then send, naming at most fifty rows, one id at a time.
The fifty-row cap is deliberate and must not be worked around. There is no selection shorthand on send, no bulk endpoint and no scheduler: a five-thousand-row batch cannot leave the building without a hundred separate calls, each of which should follow reading the fifty drafts it covers. That friction is the only structural safeguard this feature has. skip, restore and retry do take selection: "all_eligible", because they are reversible and mailing strangers is not.
A press may override from_email, from_name and reply_to for that call alone, which is how one list gets worked through from more than one address. The Resend account itself is not overridable — a different account is a different batch.

Test it

  1. One task run by hand, on a domain you know well, so you can tell a true sentence from a plausible one. Read specific_detail first: if it is generic, the prompt is not asking for enough, and every row will be generic.
  2. A two-row batch, with both addresses your own. Watch it move scheduleddraftingdrafted, then read both drafts as a stranger would.
  3. Break a row on purpose. Blank the domain cell on one row before uploading. It should fail at drafting with missing_vars, alone, while the other row drafts normally.
  4. Send those two to yourself. Check what actually arrives — the line breaks, the from_name, the reply-to — before any of it reaches somebody who did not ask for a test.

What to watch

A batch stops itself after ten consecutive drafting failures — missing_vars never counts, being a per-row data problem — or on the first configuration failure (a deleted task, a field map an edit broke) or the first account-level send failure (a revoked key, an unverified domain). Those are deterministic: they will fail identically for every remaining row, so stopping is the right answer. One address is mailed at most once per batch, enforced when the row is claimed for sending; the second row settles skipped with duplicate_recipient. And Talqing reports what it sent, not what landed — a row reading Sent means Resend accepted it, and whether it was delivered is a question only the Resend dashboard answers. Watching it there is how a sending domain survives.

Where it falls short

  • Cost scales with rows drafted, not rows sent. Drafting is the expensive half and it happens to every row. There is no cap, no budget and no estimate before the fact — decide the size of the list on the cost of the single run you measured.
  • A task edit mid-batch changes every later row, with no way back. Tasks have no versions. Edit the prompt while a batch is drafting and rows 1–800 came from the old wording, 801 onwards from the new one, and nothing on either records which. Pause the batch before you edit, or wait.
  • No cross-batch dedup and no suppression list of ours. The once-per-address rule is per batch. The same person on two lists gets two emails, and nothing here knows they unsubscribed from the first. Resend keeps a suppression list and auto-suppresses hard bounces and complaints; that is the one you are relying on, and it is yours.
  • Enrichment providers spend their own credits per row. A task attached to RocketReach or a similar provider spends that provider’s plan credits every time it runs, and nothing on our side caps it — a 5,000-row list is 5,000 lookups. Check what one run costs at the provider before you point a batch at it.
  • Task spend is not in Observability. The charts are built on calls and conversations; a task’s cost appears on its own run and on the batch page, and nowhere else.
  • A batch cannot be deleted. Cancel is the only terminal action, and identity fields — the sender, the body format, the field map — freeze once anything has been sent.

Next

Reviewing and sending

Skip, restore, retry, duplicates and the send press in full.

Monitoring a batch

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

Tools and MCP servers

What a task can reach while it works.