Account-based marketing, end to end

A full ABM programme built on Signal and one LLM, from "we have a target list" to "a meeting happened". This is longer than a workflow because ABM is not one job — it is six, and most programmes fail at the seams between them.

Time to build: a few days. Time to run: it runs itself, with one human approval gate that you should not remove.


The six stages, and where programmes actually fail

  1. Define the list — fails by being too big
  2. Detect arrival — fails by being too slow
  3. Judge the signal — fails by treating every visit as intent
  4. Choose the play — fails by having one play
  5. Execute — fails by sending generated mail unreviewed
  6. Measure — fails by measuring activity instead of meetings

Stages 1 and 3 are where the value is. Everything else is plumbing.


Stage 1 — Define the list

Start from who already reads you, not from a purchased list. You are looking for the shape of accounts that convert, and you have that shape:

customers = get_all("/accounts", classification="customer")

Ask a model to describe the pattern, then use it as a filter — not as a prediction:

Here are our existing customers as Signal sees them:
{customers_json}

Describe the pattern in at most five bullet points: size signals, behaviour
signals, anything about how they arrived. Be explicit about what this data
CANNOT tell you — it has no firmographics beyond domain, no revenue, no
headcount. Do not infer industry from a domain name.

The constraint in the last two sentences is the difference between a useful description and a hallucinated ICP.

Keep the target list under 200 accounts. A list of 2,000 is a mailing list wearing a different name, and every stage below degrades with size.


Stage 2 — Detect arrival

Webhook, filtered against the list. This is the workflow in alert your team when a target account appears; build it exactly as written and come back.

The only ABM-specific addition: record the first arrival per account, not per person. A committee arriving over ten days is one event.

def on_visitor(v):
    if v["company_domain"] not in TARGETS:
        return
    account = state.get(v["company_domain"]) or {"people": [], "opened": now()}
    account["people"].append(v)
    state[v["company_domain"]] = account
    if len(account["people"]) == 1:
        schedule_evaluation(v["company_domain"], delay_hours=72)

The 72-hour delay is deliberate. Deciding after one pageview is how you burn a target account on a generic email.


Stage 3 — Judge the signal

After the window, ask whether anything real is happening:

Account: {domain}
People who visited in the last 72 hours:
{people_json}
What they read:
{journeys_json}

Answer:
1. Is this an evaluation, a single curious person, or noise? Cite evidence.
2. If an evaluation, what are they trying to work out? Quote the pages.
3. Confidence: high, medium or low — and what would raise it.

If the honest answer is "one person read two pages", say that. A wrong "yes"
here costs us a target account; a wrong "no" costs us three days.

Make the asymmetry explicit, as in that last line. Without it, a model asked "is this an evaluation?" says yes far too often.


Stage 4 — Choose the play

Different signals deserve different responses. At minimum:

SignalPlay
One person, deep read of one topicUseful content on that topic, no pitch
Several people, one weekCoordinated outreach, one thread, name the group
Pricing + docs, repeat visitsDirect meeting request
Existing customer browsing new areaExpansion conversation, route to CS
Competitor domainNothing. Classify and move on.

The last row is a play. Doing nothing, deliberately, is the correct response to a signal that looks strong and means nothing.


Stage 5 — Execute, with a human gate

Generate drafts; do not send them. See write a first-touch email from what they read for the prompt and the SKIP rule.

The gate is not bureaucracy. On your highest-value 200 accounts, the cost of one bad automated email is measured in lost pipeline, and the cost of a human reading a draft is thirty seconds. Keep the gate until you have watched a hundred drafts go through unedited, and probably keep it after that.


Stage 6 — Measure the thing you actually want

Not sends, not opens, not "accounts engaged". Meetings.

weekly = {
    "targets_that_visited": len({a for a in state if state[a]["people"]}),
    "evaluations_judged_real": len([a for a in state.values() if a.get("verdict") == "evaluation"]),
    "drafts_approved": approved_count(),
    "meetings_booked": crm.meetings_since(last_monday, source="abm"),
}

The ratio worth watching is meetings ÷ evaluations-judged-real. If it is low, stage 3 is too generous and you are working noise. If evaluations are near zero while targets are visiting, stage 3 is too strict or your list is wrong.


What will go wrong

The list grows. Someone will ask to add fifty accounts. Each one dilutes every alert. Cap it and make additions require a removal.

The model gets more confident over time. It will not — you will get more trusting. Re-read ten judgements a month against the raw data.

Attribution will not close the loop cleanly. A meeting booked six weeks after a visit rarely traces back through any system. Accept that the last-touch number understates this programme, and judge it on the ratio above instead.

People change jobs. Your champion at a target account leaves and the account goes quiet. That is notice an account going quiet, and it is part of this programme, not a separate one.

Markdown source: /developers/playbooks/account-based-marketing-end-to-end.md