A working sales agent is a small number of well-described tools, an explicit approval boundary, and stopping rules — not a clever prompt. The first thing to build is not the agent at all: it is the validation step on the input list, because an agent multiplies whatever you feed it, including the errors.
Build the validation first
This ordering is deliberate and it is the advice I would most want back if I were starting again.
An agent working a good list is leverage. An agent working a bad list is damage at machine speed, and the cost lands on your sending domain rather than on an afternoon. In an enrichment run I measured, 44% of contacts came back attached to the wrong employer — real people, real titles, wrong company, and visually identical to the good records.
The check is one rule: keep a contact only if its own domain matches the company domain, or its most recent employer matches the company name. Put it in code, before the agent ever sees the row.
The tool surface
Fewer, better-described tools beat more tools. Past a few dozen, model selection accuracy degrades noticeably and debugging gets much harder.
| Tool | Job | Note |
|---|---|---|
| Search contacts | Find who exists | Return few, well-shaped results |
| Read a company page | Produce one specific observation | The whole personalisation layer |
| Draft a message | Write, do not send | Separate drafting from sending, always |
| Log an interaction | Record what happened | Makes behaviour auditable |
| Schedule follow-up | Queue the next touch | Where most of the value is |
Separating draft from send is the single most useful boundary. It lets you run the agent at full speed while a human approves output, and it means a misbehaving agent produces a bad draft rather than a bad reputation.
Writing tool descriptions
Most bad agent behaviour is a description problem. The model picks a tool based on the description alone, so write for a reader who cannot see your code.
- Say what it is for and when not to use it. The negative case prevents more errors than the positive one.
- Return errors as data, not exceptions. “No results, try a broader query” is recoverable. A stack trace makes the agent improvise.
- Make empty results explicit. An empty response reads to a model as a definitive answer. If empty can mean “something went wrong,” say so in the payload.
- Never make a tool await a long job. It will exceed the platform timeout and often surface as a generic error, which reads as a bug in your logic. Start and poll instead.
The approval boundary
Where the human sits changes as trust builds, and it should never disappear entirely.
- Start: human approves every outbound message.
- Then: human approves the first touch; the agent runs approved follow-ups automatically.
- Steady state: agent runs sequences; human handles every reply and every high-value account.
Keep the approval on high-value accounts permanently. The accounts where being slightly wrong is expensive are exactly the ones worth a human minute.
Stopping rules
An agent has no sense of when it is becoming annoying. Nothing emerges naturally, so write them down:
- Maximum touches before a contact goes dormant.
- Stop on any reply. Hand to a human immediately, including for “not interested” — that is a person, and an agent responding to it is the worst possible impression.
- Stop on a bounce, and remove the address rather than retrying.
- Global rate limit independent of the agent’s own logic, so a bug cannot become a spike.
- A kill switch you can hit without a deploy.
That last one has earned its place more than once. If stopping the agent requires shipping code, you will not stop it fast enough.
What it should not do
Be explicit about this in the design rather than hoping:
- Decide which accounts matter. That judgement is most of the value in outbound and it stays human.
- Handle anything off-script. A model will produce a fluent, confident, contextually wrong reply to an unexpected objection.
- Negotiate. Ever.
- Write directly to a system of record. Model proposes, code validates, system records.
Observability
Log what the agent actually did, as itself. Not what it was configured to do — what it sent, which tool it called, what came back.
When something goes wrong, the gap between intent and behaviour is the entire investigation, and without a log of the real calls you are guessing. Start debugging at the tool return value, not at the model: the most common cause of odd agent behaviour is a tool returning something other than what you assumed.
FAQ
What tools does it need?
Search, read a page, draft, log, schedule follow-up. Five well-described tools beat twenty vague ones.
Should it send without approval?
Not until proven, and never on high-value accounts. Automate follow-ups inside a sequence a human approved.
How do you stop it doing something stupid?
Explicit stopping rules, a small tool surface, a global rate limit, and a kill switch that does not require a deploy.
What is the first thing to build?
List validation, before any agent code. An agent multiplies your errors.
Related: AI SDR vs hiring an SDR, MCP vs function calling, and AI agent development.