URL: /capabilities/reach-out/personal-first-touch · Id: `page:/capabilities/reach-out/personal-first-touch`

# Personal first touch

> Your verified list in, a first email to each person whose opening line quotes their own company, then two follow-ups that each add something new.

Your verified list in, a first email to each person whose opening line quotes their own company, then two follow-ups that each add something new.

```text title="Run it · Ask"
Send personal first touches to these 50 people, up to $3.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Prospect.reach({ goal: 'Send personal first touches to these 50 people, up to $3.', budget: { amount: 3, currency: 'USD' } })
```

```json title="Run it · Response"
{
  "mode": "try",
  "stored": false,
  "estimate": "About $1.33 per 100 people",
  "sample": {
    "person": "Dana W., Director of Operations",
    "first line": "Saw you are hiring a leasing coordinator. Is the rent spreadsheet part of the job?",
    "evidence": "a job post, 2026-10-21",
    "review": "passed",
    "state": "sent"
  }
}
```

- **Kind:** Agent task
- **Price:** $1.33 per 100 people
- **Your time:** 15 minutes
- **Approval:** Goes out on its own
- **Deepens:** [`workflow:outbound-sequence`](https://startupbuilder.dev/workflows/processes/outbound/outbound-sequence)

## What you get
- **100 people** · Your verified list in, a first email to each person whose opening line quotes their own company, then two follow-ups that each add something new. · [`noun:prospect`](https://startupbuilder.dev/business/nouns/prospect)
- `sent` · First touches sent.
- `held[]` · People not written to, with the reason, such as “the evidence quote is no longer on the page”.
- `sample[]` · Opening lines with the evidence each one quotes, so you can read what went out.
- `review` · The adversarial Review’s tally: passed, and cut with the reason.
- `replies` · Replies, each handed to Answer every inbound; unsubscribes, each suppressed.

**Connect once:** [Gmail](/code/integrations/gmail), [Outlook](/code/integrations/outlook). **How long:** Drafting takes about 5 minutes; sends pace over 3 to 5 days at the mailbox caps, and the sequence closes by day 10 to 14.

## Trigger
It runs when you call it on an Audience, and as each Buying-trigger feed hit lands.

```ts
on.Audience.built((audience, $) => $.Prospect.reach({ audience: audience.$id, offer: 'leaseline-pro', ask: 'a 15-minute call' }))
```

## Steps
Your agent does these steps, one row per step; each step’s check is part of its row and its price.

| Step | Verb | Returns | Price | Approval |
| --- | --- | --- | --- | --- |
| 1. Read each row’s evidence and buying trigger | `Collection.query` | The quote, its page and the trigger saved on each row | Free | Runs on its own |
| 2. Re-read each evidence page | [`Page.audit`](/code/data/pages-and-sites/audit-page) | The quote still on the page, or the row held as stale | About 2¢ | Runs on its own |
| 3. Write each first touch | `Content.draft` | An opening line that quotes the evidence or the trigger, then three sentences toward the ask | About 40¢ | Runs on its own |
| 4. Enroll each person who passes | [`Contact.enroll`](/code/apps/sequences/enroll) | Each person enrolled in the sequence from the warmed Mailbox, after the Consent and Suppression checks | About 14¢ | Goes out on its own |
| 5. Write each follow-up | `Content.draft` | A follow-up that adds a new fact or a new angle, never a bump, to each person who has not replied | About 76¢ | Runs on its own |
| 6. Hand each reply on | `Prospect.engage` | Each reply becomes a lead for Answer every inbound; each unsubscribe is suppressed | Free | Runs on its own |

Calls: `Collection.query`, then `Page.audit`, then `Content.draft`, then `Contact.enroll`, then `Content.draft`, then `Prospect.engage`.

About $1.33 per 100 people, each time it runs, capped at $3.00.

## Example: give it a goal
> Send personal first touches to these 50 people, up to $3.

The plan it shows runs 6 steps, priced before it spends anything; Run it above shows the result it returns.

## When something fails

| When | Then |
| --- | --- |
| A Mailbox nears its bounce threshold | It rests itself (`Mailbox.rest`) and the call waits on it; the other Mailboxes carry the day’s sends. |
| An evidence quote is gone from its page | The person is held as `stale-evidence`, or re-read when the agent can find a fresh quote for under 1¢. |
| A message fails Review twice | The person is held with the findings and that message is not billed. |
| You call it twice on the same Audience | No person gets the same touch twice: it is idempotent per Audience, offer, touch and person. |

Each step retries three times, then falls through to the next route; one item's failure lands in `skipped` with its reason, never failing the call. Before a step would cross `maxCost` it stops and returns what it has, with a `resume` link. Each step's idempotency key is the call's key plus the step and the item: `leaseline-prospect-reach-1-<step>-<item>`.

## How it is graded
A second model checks every fact in each message against the row’s evidence and your offer, before it sends. It passes when every person on the list gets a first touch whose opening line quotes their own evidence, and no message states a fact the row lacks; anyone it cannot write to is held with the reason. A miss is redone at no charge.

## Change it
| Ask | Compiles to |
| --- | --- |
| Ask for a trial instead of a call. | [`Prospect.reach`](/capabilities/reach-out/personal-first-touch) |
| Lead with the renewals angle from my message test. | [`Prospect.reach`](/capabilities/reach-out/personal-first-touch) |
| Send a LinkedIn connection note instead of a second email. | [`Contact.enroll`](/code/apps/sequences/enroll) |

```ts
import { $, workflow } from 'startup-builder'

export default workflow({
  name: 'prospect-reach',
  verb: 'Prospect.reach',
  input: { audience: 'string', offer: 'string', ask: 'string', touches: 'integer', mailbox: 'string?' },
  maxCost: 3,
  review: { pattern: 'adversarial', rubric: 'every fact matches the row’s evidence; no claim beyond the Offer' },
  grade: { evaluator: 'every fact against the row’s evidence; no claim beyond the Offer', passWhen: 'every person touched or held with a reason', remedy: 'redo' },
  execute: async (ctx, { audience, offer, ask = 'a 15-minute call', touches = 3, mailbox = 'warmed' }) => {
    const rows = await ctx.step('rows', () => $.Collection.query({ from: 'audience', id: audience, members: true }))
    const fresh = await ctx.step('evidence', () => $.Page.audit({ urls: rows.map((r) => r.evidence.url), fields: ['content'], maxCost: 0.05 }))
    for (const r of rows.filter((x) => fresh.has(x.evidence.quote))) {
      const first = await ctx.step(`write:${r.$id}`, () => $.Content.draft({ kind: 'first-touch', prospect: r, offer, ask, maxCost: 0.005 }))
      await ctx.step(`enroll:${r.$id}`, () => $.Contact.enroll({ contact: r.$id, sequence: 'three-touch', first: first.$id, mailbox }))
    }
    on.Message.replied((reply, $) => $.Prospect.engage({ from: reply.$id }))     // a reply ends the sequence and becomes a Lead
  },
})
```

Extend it with `extends: 'Prospect.reach'` and `base()`, or [compose your own](/code/compose) from these steps.

## Works with
**Plays that order it:** [Account visits](/plays/account-visits), [Benchmark rows](/plays/benchmark-rows), [Buying committee](/plays/buying-committee), [Champion follow](/plays/champion-follow), [Cold email](/plays/cold-email), [Gifting](/plays/gifting), [lead reactivation](/plays/lead-reactivation), [LinkedIn outbound](/plays/linkedin-outbound), [Moment outreach](/plays/moment-outreach), [Objection watch](/plays/objection-watch), [Product signals](/plays/product-signals), [Proof-of-work outreach](/plays/proof-of-work-outreach), [Warm intros](/plays/warm-intros).

| Built from | Kind | Its own price |
| --- | --- | --- |
| [Build a verified list](/capabilities/find-customers/build-a-verified-list) | Workflow | About $1.38 per 100 checked emails |

It serves [Reach out](/capabilities/reach-out), and runs in [Hands-on](/strategy/hands-on), [Borrowed](/strategy/borrowed). It deepens [`workflow:outbound-sequence`](https://startupbuilder.dev/workflows/processes/outbound/outbound-sequence).

## For agents

| Fact | Value |
| --- | --- |
| Guide | `Prospect.reach` |
| Guide approval | `limits` |
| Goal | Send personal first touches to these 50 people, up to $3. It may change counts, the order of steps, soft filters and the route a read takes, and it stops early when the goal is met. It never relaxes your words, raises its own budget, answers its own approval, calls a Play or acts outside your standing approval. |
| Trigger | `on.Audience.built((audience, $) => $.Prospect.reach({ audience: audience.$id, offer: 'leaseline-pro', ask: 'a 15-minute call' }))` |
| Retry | Three times with backoff, then the next route; the same idempotency key never charges, sends or posts twice |
| Idempotency key | `leaseline-prospect-reach-1` |
| Lookups | A `find:` input asks unless exactly one record fits |

| Step | Action class | Seat rule that would hold it |
| --- | --- | --- |
| 1. Read each row’s evidence and buying trigger | None | None. |
| 2. Re-read each evidence page | None | None. |
| 3. Write each first touch | None | None. |
| 4. Enroll each person who passes | `send` | None. |
| 5. Write each follow-up | None | None. |
| 6. Hand each reply on | None | None. |

The amounts live once, on the hub ([approvals](https://startupbuilder.dev/agents/approvals)).

| Step | Function | Doer | Checker | Assurance | Inputs |
| --- | --- | --- | --- | --- | --- |
| 1. Read each row’s evidence and buying trigger | `code` | Code | Code checks each result against its declared type | `measured` | `$0.audience` |
| 2. Re-read each evidence page | `code` | Code | Code checks each result against its declared type | `measured` | `$1.pages` |
| 3. Write each first touch | `agentic` | The [SDR](/capabilities/agents/sdr) agent | Adversarial review by a second model: Pass, or cut with the reason; a failure is redrafted once, then held | `graded` | `$2.rows, $0.offer, $0.ask` |
| 4. Enroll each person who passes | `code` | Code | Code reads the result back from where it landed | `measured` | `$3.passed, $0.mailbox` |
| 5. Write each follow-up | `agentic` | The [SDR](/capabilities/agents/sdr) agent | Adversarial review by a second model: Pass, or held | `graded` | `$4.silent, $2.rows` |
| 6. Hand each reply on | `code` | Code | Code reads the result back from where it landed | `measured` | `replies` |

The doer is never the checker, and the weakest assurance on the path is the whole call’s.

## Under the hood

| Kind | What it touches |
| --- | --- |
| Systems | [Prospecting](/code/apps/prospecting), [Sequences](/code/apps/sequences), [sites and content](/code/apps/sites-and-content) |
| Nouns | [`noun:collection`](https://startupbuilder.dev/business/nouns/collection), [`noun:page`](https://startupbuilder.dev/business/nouns/page), [`noun:content`](https://startupbuilder.dev/business/nouns/content), [`noun:action`](https://startupbuilder.dev/business/nouns/action), [`noun:contact`](https://startupbuilder.dev/business/nouns/contact), [`noun:prospect`](https://startupbuilder.dev/business/nouns/prospect) |
| Events | `Collection.queried`, `Content.drafted`, `Action.evaluated`, `Contact.enrolled`, `Prospect.engaged` |

## Reference

| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `audience` | `Audience` | Required | The list, from Build a verified list or your own. |
| `offer` | `Offer` | Required | What you are offering; no message claims what it cannot do. |
| `ask` | `string` | `'a 15-minute call'` | The one thing each message asks for. |
| `touches` | `integer` | `3` | Messages per person: day 0, day 3 and day 8. Any reply ends the sequence. |
| `mailbox` | `Mailbox[]` | `your two warmed inboxes` | Where cold first touches send from: 2 inboxes on a warmed second domain such as tryleaseline.app, 25 a day each after a 14-day ramp. Warm intros and replies stay on your main domain. |
| `maxCost` | `number` | `3.00` | The most this call spends each time it runs, in USD. It stops before a step would cross it and returns what it has, with a `resume` link. |

Emits `Prospect.reached`. Refuses with [`max-cost`](/code/refusals#max-cost) before it spends, and with the Data calls and Accounts groups ([refusals](/code/refusals)).