# 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:

```python
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](../workflows/alert-your-team-when-a-target-account-appears.md);
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.

```python
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:

| Signal | Play |
|---|---|
| One person, deep read of one topic | Useful content on that topic, no pitch |
| Several people, one week | Coordinated outreach, one thread, name the group |
| Pricing + docs, repeat visits | Direct meeting request |
| Existing customer browsing new area | Expansion conversation, route to CS |
| Competitor domain | Nothing. 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](../workflows/write-a-first-touch-email-from-what-they-read.md)
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.

```python
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](../workflows/notice-an-account-going-quiet.md),
and it is part of this programme, not a separate one.
