Campaign sheet (07)
SkillWeb & browsingTurn a terminal records.jsonl into an owner-readable campaign sheet plus a HubSpot-import-shaped CSV - the chain's exit door. Use when a run is ready to hand to an owner: "build the campaign sheet", "export these for HubSpot", "who do I contact first", or as the final step of run-first-campaign. Reads whatever stage produced the records (qualified, signal-ranked, or fully resolved) and degrades gracefully on missing fields - no email column until 06 has run. Nothing sends and nothing downstream reads its output: the sheet and CSV are the human deliverable, approved on screen. To add verified emails first, run 06-resolution-email-person; to rank, 05-signal-builder.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Campaign sheet (07) skill
What this skill tells your AI
The instructions your AI receives, as published by zevenue/headless-gtm in skills/07-campaign-sheet/SKILL.md and read by ahel’s review.
The last step in the chain. Everything upstream produced records.jsonl; this
turns those records into what an owner actually uses - a ranked "who to contact
first and why" sheet, plus a CSV shaped for a one-click HubSpot import.
It adds no fields to the chain and nothing reads its output. The CSV is a
derived, human-facing export (headless-gtm-shared/CONVENTIONS.md) - the
canonical record stays records.jsonl, and re-running any upstream skill never
depends on this sheet.
What it reads
A terminal records.jsonl from any stage. The script tolerates every shape and
degrades instead of failing:
- Company name from
company(orname), site fromwebsite(orhttps://+domain), phone / city from wherever the discovery step put them. - Ranking from 05's
signals[]- the highest-score signal per record decides call order; itssignal_type,signal_sentence, andsource_urlbecome the "why now" line, and itsapproach(orfallback_approach) the recommended angle. Records with no signals stay in the sheet, ranked last. - Contacts from 06's
person_name/matched_title/email/verification_status. When 06 has not run, the contact columns are dropped entirely rather than shipped empty. - Rows with no website ride through on their listing fields - the sheet does not
drop them (a registry row with
domainempty is still a real target).
What it writes
runs/<run-id>/ (via common.runs_base(), step prefix 07):
campaign-sheet.md- owner-readable. Ranked cards for the top targets (company, city, why now with source, approach, contact or "not resolved"), then a compact table for the rest, then a "before you send" note.campaign-sheet.csv- HubSpot default contact-import headers:First Name, Last Name, Email, Company Name, Website URL, Phone Number, Lifecycle Stage, Notes. Every row is set to Lifecycle Stage = Lead;Notescarries the top signal and the recommended approach. First/Last/Email appear only when at least one record is resolved, and aListing URLcolumn appears only when at least one record carries one (directory-sourced runs) - so a phone-only registry row still exports a working link. The headers are HubSpot's defaults because it is the most common import target; every field is a plain column, so any other CRM (Salesforce, Pipedrive, Attio) takes the same file after a one-time header remap on import.
Run it
python3 scripts/campaign_sheet.py --records ./runs/<run-id>/records.jsonl --title "HVAC - Ontario"
Stdlib only - no API keys, no network. It runs on a synthetic fixture with zero accounts, so it is safe to demo before any real data exists.
The approval step (must hold)
Present both files on screen. Nothing is send-ready until the owner approves the sheet there. Nothing in this repo sends email - if asked to "just send it", say so plainly and point at the approved sheet and the drafted sequence. The import, and any send, is the owner's step after approval.
Optional: stage into HubSpot (only when a connector is present)
The default path is the CSV import - the owner uploads campaign-sheet.csv in
HubSpot and maps the columns (all default properties, so mapping is automatic).
If a HubSpot MCP or connector is available in the session, offer an alternative:
stage the approved rows directly as contacts with lifecyclestage = lead.
Before any write, confirm the batch size and get an explicit go-ahead, then
stage in batches and report what was created. Never create or modify HubSpot
records without that on-screen approval, and never stage rows the owner has not
approved on the sheet. If no connector is present, do not ask for one - the CSV
is the complete path.
Output
End with a one-paragraph recap: rows written, how many carry a resolved contact, the top 3 targets with their one-line reason, and the paths to the two files.
Signals
- GitHub stars
- 28
- Forks
- 6
- Last commit
- Jul 2026
Advanced
- Catalog kind
- skill
- Gateway key
x-07-campaign-sheet- Source
- github.com/zevenue/headless-gtm