The stack
The problem
I would not upload a list simply because every row has an email address and a personalized opening. The expensive mistakes are often quieter: the same address appears under two names, an opt-out has slipped into a fresh export, or the message makes a claim that the research never established.
My approach keeps the draft, its evidence and its approval together. The software blocks repeatable data failures. A reviewer judges the factual assertions and relevance. Only then does the exporter create the sender file. That order gives me a specific reason for every included record.
How it works
- Build the ICP cohort and draft an initial email with source evidence.
- Run deterministic checks; every mechanically clean row still needs editorial review.
- Correct or reject weak records, record decisions for the exact batch, then export.
- Preview the mapped copy in a paused sender and test delivery separately.
The playbook
Define the batch before drafting
I would start with one ICP, one reason to contact it, and one useful offer. Build the cohort using the ICP list workflow. Keep an account out when the fit rests on a guess that the source cannot resolve. A smaller explicit cohort is easier to review than a large list whose selection logic nobody can explain.
Use the cold-email prompt or personalization playbook to draft the initial message. Carry the source URL, a short excerpt and the primary factual claim into the prospect file. Review the complete message later, including facts that were not copied into that claim field.
Download the pack and inspect the input contract
Download the complete campaign QA pack and extract it. I have included scripts/campaign.py, references, original and corrected CSVs, fictional decisions, and expected outputs. Python 3.9+ is the only runtime dependency. No paid tool is needed to reproduce the example.
The original CSV has eight fictional records. Its exact columns are id, email, first_name, company, subject, body, evidence_url, evidence_excerpt, claim and verification_status. The suppression file has one email column. Quoted body cells can contain paragraph breaks. The package contract explains every field; do not rename columns halfway through the workflow.
These are sample people and .example addresses. The evidence passage and verification declarations are fixture data. For real use, read the actual source and inspect a recent verification result; writing valid into a column is not verification.
python3 --version
python3 scripts/campaign.py --help
Run the mechanical checks
I run the original batch against the supplied suppression list. The expected result is seven blocked records and one needs_review record. Both duplicate rows are blocked; the script does not guess which identity is correct.
The JSON includes the exact subject, body and evidence next to each reason. That makes the review queue useful outside a terminal. I would keep this report with the input revision, rather than copy a green status back into an unrelated spreadsheet.
python3 scripts/campaign.py review --leads examples/prospects.csv --suppression examples/suppression.csv --out review.json
Make a decision for every failure
p01: Asha is suppressed. I leave the address excluded. p02 and p03: two records share Ben’s email. The fixture design identifies p02 as the intended record, so the corrected file retains it once and removes p03. With real data I would resolve identity before keeping either.
p04: the evidence excerpt is missing. I add the supplied fictional passage. p05: the subject still contains a company token. I replace it with the final subject. p07: verification is unknown. The corrected fixture simulates a valid result; a real record must wait for the actual check. p08: a formula-like string occupies the body. I replace it with the reviewed plain-text draft.
p06 is the important one. It has a source URL, an excerpt, a complete body and a declared valid address. The script returns needs_review. But the draft says pipeline dropped 40%, while the source only mentions two SDR openings. A model could easily wave this through unless I make evidence support a separate decision.
Correct the unsupported claim, then review again
I remove the fabricated pipeline number and the promise to fix it. The corrected question connects an observed hiring signal to a modest offer without asserting that I know the team’s internal situation. This is the kind of first-person judgment I want in the workflow: a reasoned choice, with its evidence visible.
The corrected CSV contains six records. Running the next command should return six needs_review rows. None are automatically approved. The bundled expected report lets you compare your output; it is an execution fixture, not a claim that these messages would perform well.
python3 scripts/campaign.py review --leads examples/corrected.csv --suppression examples/suppression.csv --out corrected-review.json
Record approval against the exact copy
For the offline rehearsal, examples/demo-decisions.json holds six explicitly fictional approvals. For real work, follow references/contract.md and record the actual reviewer, approve or reject, the reason, and whether the evidence and all claims were reviewed. Copy the current batch hash and row hashes into that file.
I would not let an agent infer approval from silence or a clean script result. The hashes also have a narrower job than security: they catch accidental changes between review and export. Someone with write access can forge a decision file; access control still belongs in the operating environment.
Try changing one word in a copy of corrected.csv and reusing the demo decisions. Export must refuse with Batch changed. A new suppression should invalidate the same approval. This small test catches the operational mistake of changing the thing somebody already reviewed.
Export only the approved records
This command exports six demo records. It makes no network request. The output has five columns: email, first_name, company_name, qa_subject and qa_body. Email is normalized to lowercase; subjects and multiline bodies are retained as quoted CSV cells.
Compare it with examples/expected-smartlead-import.csv. No suppressed contact, duplicate copy, company placeholder or fabricated pipeline claim should survive. The tool refuses to overwrite an existing output, so use a new filename for each run. Keep real inputs, decisions and exports in a private folder.
python3 scripts/campaign.py export --leads examples/corrected.csv --suppression examples/suppression.csv --decisions examples/demo-decisions.json --out smartlead-import.csv
Map a real export in a paused Smartlead campaign
The sample addresses stay offline. When you have your own reviewed records, map email and first_name to their standard fields. Map company_name to the company field or a custom field. Map qa_subject and qa_body as custom variables with exactly those names. Insert them with the variable picker into the subject and body, then preview every record.
I based this on Smartlead’s CSV mapping instructions and variable documentation, checked September 20, 2026. The export is tested locally; the account import and full-body rendering still need your sender preview. If paragraph breaks or variables render incorrectly, correct the paused draft before proceeding.
Keep blocked, bounced and unsubscribed contacts excluded. Refresh suppression immediately before import and inspect cross-campaign duplicates. Add the real sender signature and opt-out mechanism, send a test to your own inbox, and check reply-stop behavior and scheduling separately. The exported file alone cannot establish that a campaign is ready to activate.
What you get
The corrected p06 message
Hi Faye,
Your careers page lists two SDR openings. Would a checklist for reviewing new-rep account research be useful?
Vikash
Pitfalls to avoid
Treating populated evidence as proofI inspect what the passage actually supports. The sample hiring source cannot support a pipeline decline.
Reusing decisions after editing copyThe batch hash changes, and export refuses. Rerun review and record fresh decisions.
Confusing a CSV export with a sender testField mapping, paragraph rendering, suppression settings, test delivery and reply stops need checks in the sender.
Questions people ask
- Does the QA script verify facts or email addresses?
- No. It checks field presence, format and declared verification status. I review the actual source, full copy and dated verification result separately.
- Does the export send anything?
- No. It writes a local CSV for manual mapping. Sender settings and a test email are separate steps.
- Can I use the pack with Instantly or lemlist?
- The review logic is sender independent. The included mapping is for Smartlead; I would build and test a separate adapter before importing into another sender.
Related plays
- Installable campaign QA skill →SKILL.md, inputs, outputs and the downloadable runnable pack.
- Cold email software and cost calculator →Size the cohort and budget before picking a sender.
- Smartlead vs Instantly →Compare the capacity thresholds and the pilot I would run.
- Cold email writing prompt →Draft the message before reviewing it.
- Build an ICP list in Clay →Decide which accounts belong in the batch.