# R1 — lead capture: a row on the sheet, a reply waiting for you

Read-date 2026-11-10. Part of the Back-Office Autopilot pack.

## What it does

A lead arrives - a form submission, an emailed enquiry saved as a CSV, a record
exported from wherever. This recipe reads it, puts **one row** on your sheet
with the details in fixed columns, and writes **one reply, as a draft**,
addressed to that lead. If the same person submits twice, they get one row and
one draft, not two.

It also tells the truth about what it could not use: a row with no email, or an
email that is not an address, is counted and named in the run summary, with the
file and the record number it came from. It never guesses an address.

## What it needs

Pick one member - they are the same recipe for three places your leads live.

| Member | Needs | Reaches the network? |
|---|---|---|
| **A** `lead_capture_to_sheets.py` | Python 3.12. No installs, no accounts. | No. No network code exists in the file. |
| **B** `lead_capture.n8n.json` + `n8n_normalize_code.js` | n8n, a Google Sheets credential, a Gmail credential | Only through the two Google nodes you authorised |
| **C** `lead_capture.apps-script.gs` | The Google account that owns the sheet | Only Apps Script's own Sheet read/write and Gmail draft |

## What it outputs

* one row per new lead, with a header row: `received_at`, `lead_date`, `name`,
  `email`, `phone`, `source`, `message`;
* one draft per new lead. Member A writes it as a numbered `.txt` file
  (`0001-name-example.com.txt`); B makes a Gmail draft; C makes a Gmail draft.
* a run summary counting leads seen, appended, duplicates, skipped **and why**,
  and drafts written.

## Verify it in 2 minutes

**Member A**, from inside `r1/`:

```
python3 lead_capture_to_sheets.py
python3 lead_capture_to_sheets.py
```

First run: 3 leads appended, 3 drafts in `outbox/leads_drafts/`, 1 duplicate and
3 skipped - all four numbers printed, each skip with its reason. Second run:
**0 appended, 0 drafts**, everything reported as already there. Open a draft:
its first line says `DRAFT`. Open the sheet: one row per person.

**Member B or C:** post one synthetic lead (a `curl` in `SETUP.md`, or press Run
on `verifyR1` in Apps Script). One row appears, one draft appears, nothing is
sent. The second attempt with the same address is refused.

## The one decision you should read before you change anything

**This recipe cannot send.** Not "is not configured to send" - the code for it
is not there, in any of the three members, and the tests in
`test_lead_capture.py` fail the build if someone adds it.

Why build something less convenient than a self-writing reply? Because a reply
to a new lead is written **in your name**, to someone who has never met you, and
it commits you to answering. An unreviewed automatic reply also carries a
commercial risk you would be the one to carry: a lead who was promised something
by a machine, in your signature. So the machine prepares, and you press send.
The part that was worth automating - noticing the lead, and writing the first
draft before it slips - is automated. The part that should stay human stays
human.

**Do not "fix" this by switching the Gmail node's resource from Draft to
message/send.** If you genuinely want a later message to go out on a schedule,
build it as its own file with its own approval step, so this recipe's promise
stays true and readable.

## What this does NOT do

* **It does not send.** No email, no SMS, no webhook fired at anybody. Drafts.
* **It does not upload or phone home.** Member A has no network code at all;
  there is nothing to inspect and trust - `test_lead_capture.py` reads every
  import and every call in the file and fails if a network or mail one appears.
* **It is not a CRM.** One sheet, one row per address. No pipeline, no tags.
* **No attachments, no calendar, no two-way conversation.** Replies come back to
  you, where you read them.
* **The numbers are only as good as the input file.** It dedupes by email
  address, case-insensitively. It does not fold dots or plus-tags
  (`a.b@x.com` and `ab@x.com` are two people as far as it is concerned): folding
  them would merge two real humans to save you one row you can delete.

## The two files that make it, and one honest limit

`n8n_normalize_code.js` is the readable copy of the script inside the workflow's
Code node. `build_workflow_json.py` is what puts it there - run it after any
edit, and the test `test_n8n_code_node_holds_the_shipped_script_word_for_word`
fails if the two ever disagree.

**The limit, said plainly:** n8n is not installed on the machine this pack was
built on. The workflow was checked offline - it parses, its four nodes chain in
one direction, the final node is a draft, no credential is baked in - but **it
has never been executed by n8n.** The node version numbers in the file are what
I wrote it against on 2026-11-10. `SETUP.md` step 3 lists the five things to
look at on import. Everything in Member A and Member C's logic is covered by
executed tests.

## Files in this folder

`lead_capture_to_sheets.py` · `lead_capture.n8n.json` · `n8n_normalize_code.js` ·
`build_workflow_json.py` · `lead_capture.apps-script.gs` · `SETUP.md` ·
`RECIPE.md` · `test_lead_capture.py` (32 tests, offline) ·
`samples/incoming/` (two synthetic lead files) · `data/` and
`outbox/` (the output of one run on those samples - synthetic, no real
person in them).
