Skip to content

Inbound Follow-Up Cron

Script: mini:~/tasks/scripts/inbound_followup.py
Status: Live (March 26, 2026)
Schedule: Mon/Wed/Fri at 8:45 AM CT via launchd on Mac Mini
Model: claude-sonnet-4-5


What It Does

Sends personalized follow-up emails to brands that haven't replied to our initial auto-response. Four follow-ups maximum, spaced at Day 3, 7, 11, and 14 after the original inbound email.

The script only processes leads that are in the "Inbound Follow-Up" Close CRM status (set by the inbound Zap). This gate ensures we never accidentally follow up on old or unrelated records.

Stops automatically when:

  • Responded = true — brand replied or booked a call (set by companion Zap)
  • Follow Up Count >= 4 — all 4 follow-ups sent

After the 4th follow-up with no response:

  • Logs a note on the Close CRM lead
  • Moves the lead status to "No Response"

Why a Python Cron Instead of Zapier

The previous Zapier approach (Zap 356080312) fetched all Airtable records on every run. At 3,000+ records and 3 runs/week, that's 9,000+ Zapier tasks per week just to check eligibility — before any emails are sent. A Python cron on Mini has zero per-task cost and handles pagination properly.


Architecture

Mac Mini launchd (Mon/Wed/Fri 8:45 AM CT)

Fetch candidates from Airtable
  Filter: In Follow-Up=TRUE, Responded=FALSE, Follow Up Count<4, not contacted today

For each eligible record:
  1. Generate email via Claude Sonnet (different angle per follow-up number)
  2. Look up RFC Message-ID from agent's Gmail mailbox
  3. Send Gmail reply in-thread from agent's address
  4. Update Airtable (Follow Up Count, Last Contact Date, Last Follow Up Body)
  5. If FU#4: log Close note + set status to No Response

Post end-of-run summary to #crm-review-queue in Slack

Gmail Threading

Each outgoing email is threaded correctly in the original conversation:

  1. The script fetches the RFC Message-ID of the original inbound email from the agent's Gmail mailbox (agents receive all inbound emails via routing from testcreator@/upflip@ addresses)
  2. Sends the reply with:
    • threadId = agent's thread ID
    • In-Reply-To = RFC Message-ID
    • References = RFC Message-ID
    • Subject = Re: [original subject]
  3. Email sends from the agent's address (annie@, charlie@, henry@) using Google service account with domain-wide delegation

Result: the reply appears in the original email thread in the recipient's inbox, with the agent's verified @creatorsagency.co badge.


Email Generation

Emails are generated by Claude Sonnet using a prompt built directly on the initial response Zapier prompt — same voice, same banned words, same structure.

Each follow-up adds something new the previous one didn't:

#DayNew information added
13Reacts to their original message, Q2 urgency
27Adds proof: 300+ brands, 4,000+ campaigns
311Reframes the call as market intel WE give THEM
414Warm close, timing only, door left open

Anti-repetition: Each email is passed to the next as context so Claude can't reuse the same hooks, observations, or phrases.

Dollar validation: If Claude includes any dollar sign in the output, the script automatically retries with an explicit correction.

Creator context: The Creator Name field from Airtable is passed to Claude. If set, the email references that creator naturally. If unknown, Claude speaks to the roster broadly without acknowledging the gap.

Hard rules enforced in the prompt:

  • No dollar amounts (hard ban + post-generation check)
  • No industry assumptions ("finance and business space", "fintech") unless the brand stated it
  • No validating their proposed rate or deliverable before the call
  • No specifying call length ("15 minutes", "quick call")
  • Always first person as the agent — never third person
  • Sign-off rotates each email (Cheers / Best / Talk soon / — Annie)
  • Calendar link embedded in the last sentence of the body

Airtable Fields Used

FieldIDPurpose
In Follow-UpfldCsQ2aiMDGSPmTUGate: only process leads with this checked
RespondedfldjDZdTmKt46zMidStop if true (brand replied or booked)
Follow Up Countfldt03M5kKQSHy947Tracks which follow-up we're on
Last Contact DatefldwLxh4WUzSrPQvCDouble-send protection: skip if set to today
Last Follow Up BodyfldyC29zZ4XBDbBFYPassed to Claude to prevent repetition
Message IDfldvdiPA1pSM4z2jgGmail message ID for thread lookup
SubjectfldKwJtBJE8AoS751Original email subject for Re: threading
assigned_agent_emailfldO9YkmrZdL4u5QOWhich agent handles this lead
Inbound MessagefldgkX8OGdmdoK4QOOriginal brand email body
Creator NamefldU0Wbc9eO4TqOWECreator the brand mentioned (if known)
routed_atfldv83HliHa55i5tqAuto-set on record creation — used for day cadence

Close CRM Integration

The script finds the Close CRM lead by matching the sender's email against Close contacts. On the 4th follow-up:

  1. Logs a note: "4 follow-up emails sent to [name] with no response. Moving lead to No Response status."
  2. Sets lead status to No Response (stat_lbKCSyUCGcs6RKejc5bjgg1Fqm0hGjbLEbifaNv6FbZ)

Slack Reporting

One message posted to #crm-review-queue at the end of each run:

:email: Inbound Follow-Up Run — 2026-03-28
Sent: 12 | Skipped: 18 | Errors: 0
By agent: Annie: 4 | Charlie: 5 | Henry: 3

Errors include the record ID and truncated error message so issues can be investigated via the log.


Running Manually

bash
# Dry run — see what would fire without sending anything
ssh mini "python3 ~/tasks/scripts/inbound_followup.py --dry-run"

# Limit to N sends (for testing)
ssh mini "python3 ~/tasks/scripts/inbound_followup.py --limit 5"

# Full run
ssh mini "python3 ~/tasks/scripts/inbound_followup.py"

Log file: mini:~/tasks/logs/inbound_followup.log


What Still Needs to Be Built

  • Responded detection Zap: When a brand replies to the thread, the Responded field needs to flip to true automatically so follow-ups stop. Currently this requires a Zapier Gmail watch that matches the reply to the Airtable record.
  • Cal.com booking → Responded: When a brand books a call via Cal.com, the calcom_close_handler.py script should also set Responded = true in Airtable.

Until these are built, Responded must be set manually if a brand replies outside of the normal flow.


AgentCalendar URL
Anniehttps://cal.com/team/creators-agency/annie-strategy-call
Charliehttps://cal.com/team/creators-agency/charlie-strategy-call
Henryhttps://cal.com/team/creators-agency/henry-strategy-call
Fallbackhttps://cal.com/team/creators-agency/strategy-call