Automation
Account-based marketing with a prospecting agent: personalising at the contact level
How to combine account-based marketing with an automated prospecting agent, so outreach is grouped by account, sequenced by role, and personalised from one contact to the next without ever sounding templated.
Traditional outbound treats every contact as an independent unit: one row, one message, one send. Account-based marketing rejects that premise. It starts from the account, not the contact, because in any meaningfully sized deal the actual buying decision is made by several people playing different roles, and a message that makes sense to a technical evaluator will land badly with the executive sponsor reading a forwarded copy of it. The two disciplines, ABM and automated prospecting, are often treated as opposites: one careful and manual, the other fast and mechanical. They do not have to be. This guide shows how to build a prospecting agent that thinks in accounts first and personalises at the contact level second, so automation and account discipline reinforce each other instead of fighting.
Group contacts into accounts before anything else
The foundational change from a flat contact list to ABM is structural: every contact needs to carry a stable account identifier, and the agent needs to reason at the account level before it ever looks at an individual person. A company domain is usually the simplest reliable account key, since it is present on nearly every business email and rarely changes.
from collections import defaultdict
def group_by_account(contacts):
accounts = defaultdict(list)
for c in contacts:
domain = c["email"].split("@")[-1].lower()
accounts[domain].append(c)
return accounts
Once contacts are grouped, the agent’s first real decision is not “who do I message” but “which accounts are worth pursuing at all”. An account with one junior contact and no economic buyer identified is a very different priority from one where you already have three roles mapped, and the sequencing logic that follows should reflect that difference explicitly.
Assign a role to every contact, and sequence by role
ABM messaging works because different roles need different arguments, and a prospecting agent should encode that as data rather than leave it to whoever writes the next campaign. A simple role taxonomy, mapped from job title, is enough to drive meaningfully different messaging without needing a bespoke classifier for every account.
ROLE_KEYWORDS = {
"economic_buyer": ["cfo", "vp finance", "chief", "director of operations"],
"technical_evaluator": ["head of it", "cto", "engineering", "architect"],
"end_user": ["analyst", "manager", "coordinator", "specialist"],
}
def infer_role(title):
t = (title or "").lower()
for role, keywords in ROLE_KEYWORDS.items():
if any(k in t for k in keywords):
return role
return "unknown"
The sequencing that follows from this is where ABM earns its name. A technical evaluator might receive the first outreach, with a message focused on implementation and integration; the economic buyer is approached slightly later, once there is a technical champion to reference, with a message about outcomes and cost rather than architecture. The agent enforces this order deliberately, rather than emailing every contact at an account on the same day with the same content.
SEQUENCE_ORDER = ["technical_evaluator", "end_user", "economic_buyer"]
def sequence_account(account_contacts, days_between_roles=4):
by_role = defaultdict(list)
for c in account_contacts:
by_role[infer_role(c.get("title"))].append(c)
plan = []
for i, role in enumerate(SEQUENCE_ORDER):
for c in by_role.get(role, []):
plan.append({**c, "role": role, "send_offset_days": i * days_between_roles})
return plan
Personalise from what the account already told you
The best contact-level personalisation in ABM rarely comes from generic company research; it comes from what earlier contacts at the same account have already revealed, whether through a reply, a form field, or a piece of content they engaged with. If the technical evaluator replied mentioning a specific integration concern, the message to the economic buyer three days later should be able to reference the fact that their team is already evaluating that exact question, without repeating the same generic opening every contact at every account receives.
def build_account_context(account_contacts, engagement_log):
context = {"signals": []}
for c in account_contacts:
for event in engagement_log.get(c["email"], []):
if event["type"] == "reply":
context["signals"].append(f"a colleague raised: {event['summary']}")
elif event["type"] == "content_engagement":
context["signals"].append(f"the team viewed: {event['asset']}")
return context
That accumulated context becomes the input to the same constrained-personalisation pattern used for individual prospecting: a fixed template, with a language model filling one bounded slot using only the account-level signal available at send time. The difference from ordinary prospecting is the scope of what informs that one sentence: not just the individual’s title and company, but everything the account itself has already surfaced.
def personalise_for_account(model, contact, account_context):
signals = "; ".join(account_context["signals"][-3:]) or "no prior signal"
prompt = (
"Write one opening sentence, under 25 words, for this specific role, "
"referencing the account context if relevant. No hype, no emojis.\n"
f"Role: {contact['role']}\nAccount signal: {signals}"
)
return model.complete(prompt).strip()
Never let one contact’s outreach contradict another’s
The single most damaging failure mode in ABM automation is internal inconsistency: two contacts at the same account receiving messages that quote different numbers, make different claims, or contradict each other if compared. Because the whole point of ABM is that people at the same account eventually talk to each other, or forward emails internally, this is not a hypothetical risk. The defence is to keep any claim that could vary, such as a pricing figure or a capability statement, in one shared source that every message at the account draws from, rather than letting each contact’s message be generated independently with room to drift.
def account_facts(account_id, facts_store):
# a single source of truth per account: pricing tier shown, claims made, dates promised
return facts_store.get(account_id, {})
def render_message(template, contact, account_context, account_id, facts_store):
facts = account_facts(account_id, facts_store)
opener = personalise_for_account(model, contact, account_context)
return template.format(
first=contact["firstname"], opener=opener,
pricing_note=facts.get("pricing_shown", "standard pricing"),
)
Recording every fact shown to an account, and reusing it rather than regenerating it per contact, is what keeps a multi-touch, multi-person sequence coherent under scrutiny.
Track engagement at the account level, not just the contact level
A single contact ignoring three emails looks like a dead lead. Three different contacts at the same account each opening one email and viewing the same pricing page looks like an account quietly building internal consensus, and it deserves a very different response, likely a faster follow-up and a different offer such as a group demo rather than another one-to-one email. Rolling engagement signals up to the account level is what makes this visible.
def account_engagement_score(account_id, engagement_log, account_contacts):
emails = {c["email"] for c in account_contacts}
score = 0
distinct_engaged = set()
for email in emails:
for event in engagement_log.get(email, []):
score += {"open": 1, "click": 3, "reply": 8, "content_engagement": 5}.get(event["type"], 0)
distinct_engaged.add(email)
# reward breadth: multiple people engaging outweighs one person engaging a lot
return score * (1 + 0.5 * (len(distinct_engaged) - 1))
Ranking accounts by this score, rather than ranking contacts, is what turns the agent’s daily output into a genuinely useful list for sales: not “who opened an email” but “which accounts are showing the kind of multi-person interest that predicts a real deal”.
Keep the same governance the contact-level agent needs
Everything that makes an individual prospecting agent safe still applies here, and ABM does not exempt you from any of it. Suppression and consent checks run before every send, regardless of role or account. Volume caps still apply, now per account as well as per run, so a single enthusiastic account does not get five emails in a week because five different automations each decided independently that it was time to reach out. Sending still goes through one authenticated channel with the same retry and delivery-tracking discipline. ABM changes what the agent optimises for; it does not relax any of the guardrails that keep automated outreach trustworthy.
Decide which accounts deserve the ABM treatment at all
Not every account in a pipeline justifies the overhead of multi-role sequencing and shared-fact governance; for a low-value, high-volume segment, a simpler one-to-one prospecting flow is often the right tool, and applying full ABM discipline everywhere just slows the team down without a matching return. The useful split is by deal size and strategic fit: accounts above a value threshold, or matching an ideal-customer profile your team has already defined, go through the account-grouped sequence; everything else runs through the lighter individual-contact flow described in a standard prospecting agent. Encoding that split as a simple rule keeps the agent from treating every account as equally deserving of a slower, more careful sequence.
def qualifies_for_abm(account_contacts, company_size, deal_threshold=25000):
has_multiple_roles = len({infer_role(c.get("title")) for c in account_contacts}) > 1
is_target_size = company_size and company_size >= 200
return has_multiple_roles and is_target_size
Let sales see the account view, not just the contact view
The final piece that makes this worth building is surfacing the account-level plan somewhere a salesperson actually looks, not just leaving it inside the agent’s internal state. A simple account summary, generated alongside the sequencing plan, tells a rep exactly where an account stands: which roles have been reached, what each contact has engaged with, and what the next scheduled touch is. That visibility is what lets a human step in exactly when the agent’s automated sequence should hand off to a real conversation, which for any account showing genuine multi-person engagement is usually sooner rather than later.
def account_summary(account_id, account_contacts, plan, engagement_log):
reached = {c["role"] for c in plan if engagement_log.get(c["email"])}
return {
"account_id": account_id,
"contacts_mapped": len(account_contacts),
"roles_reached": sorted(reached),
"next_touch": min((c["send_offset_days"] for c in plan
if c["role"] not in reached), default=None),
}
Where the agent should stop and a human should start
The most useful design decision in an ABM-aware prospecting agent is not any single technical pattern above; it is deciding, explicitly and in advance, the moment automation hands off to a person. A single contact opening one email is not that moment. Three contacts at the same account engaging with different assets within the same week is. The economic buyer replying, even briefly, is unambiguously that moment, and an agent that continues its scripted sequence to that contact afterward, rather than immediately flagging the account for a human to take over, has misunderstood what it was built for. The value of everything described here, the account grouping, the role sequencing, the shared facts, the rolled-up engagement score, is that it produces exactly the signal a busy salesperson needs to know which of the fifty accounts they are nominally responsible for actually deserves their attention this week. An agent that keeps sending automated touches to an account that has already shown that signal is not being efficient; it is actively working against the relationship a human should now be building, and the handoff rule deserves at least as much design attention as the personalisation logic that precedes it.
Key takeaways
- Group contacts into accounts by domain before any messaging logic runs.
- Infer a role per contact and sequence outreach by role, not all at once.
- Build account-level context from prior signals, and let personalisation draw on the whole account’s history, not just one contact’s data.
- Keep claims and facts shown to an account in one shared source, so no two contacts ever receive contradictory messages.
- Roll engagement up to the account level; multiple people engaging predicts a real deal better than one person engaging a lot.
- Apply the same suppression, consent and volume governance as any prospecting agent, with an added per-account cap.
Account-based marketing and automation are not in tension. The discipline ABM demands, thinking in accounts, sequencing by role, staying consistent across contacts, is exactly what a well-built agent can enforce more reliably than a busy human ever could.
References
Apply this to your business
Automation and AI
Documented, monitored automation that gives the team hours back and makes processes reliable.
n8n workflows and AI-assisted systems that remove repetitive marketing work, connect your tools and keep humans in control of what ships.
- n8n
- Zapier
- Make
- OpenAI API
Growth engine
One connected growth system with a single owner, measured end to end from first visit to revenue.
The integrated engagement. Positioning, website, CRM, automation, analytics and demand generation built as one measurable system.
- Astro
- HubSpot
- n8n
- GA4
CRM implementation
A CRM the team actually uses, with clean data, clear lifecycle stages and reporting leadership believes.
HubSpot and CRM implementations designed around your sales process, adopted by your team, and connected to marketing and reporting from day one.
- HubSpot
- Salesforce
- Brevo
- Zoho