Jump to: The Problem Prerequisites Sheet Setup Email Templates The Code Deploy & Schedule Extend It
The Problem This Solves

Without Agent

  • Rep manually checks renewal spreadsheet weekly
  • Sends generic "your policy renews soon" email
  • Forgets to follow up on non-responders
  • Policies lapse silently — revenue lost
  • Rep spends 3 hrs/week on renewal admin
  • 15–20% of renewals lapse

With Agent

  • Runs automatically every morning
  • Personalised email with policy summary
  • 3-touch sequence: 30d / 14d / 7d before expiry
  • Non-responders escalated to a rep at 7 days
  • Zero manual effort after setup
  • Retention rates increase 8–12%
Prerequisites
Step 1 — Set Up the Renewal Tracking Sheet
  1. 1

    Create a new Google Sheet named "Policy Renewal Tracker"

    Add these headers in Row 1 (columns A through M):

    Policy Number | Holder Name | Email | Phone | Policy Type | Coverage Amount | Premium | Renewal Date | Days Until Renewal | Touch 1 Sent (30d) | Touch 2 Sent (14d) | Touch 3 Sent (7d) | Status | Rep Escalated

  2. 2

    Add a formula in the "Days Until Renewal" column

    In cell I2, enter this formula (then drag down for all rows):

    =IFERROR(DATEDIF(TODAY(), H2, "D"), "EXPIRED")

    This auto-calculates how many days until each renewal. The agent uses column H (Renewal Date) directly.

  3. 3

    Add test rows with renewal dates

    Add 3 test rows with renewal dates set to: 30 days from today, 14 days from today, and 7 days from today. Use your own email address in the Email column for testing.

Step 2 — Understand the 3-Touch Email Sequence
Each email is personalised by Claude using the policyholder's actual details. Here's what each touch focuses on.
1

Touch 1 — 30 Days Out: Informational

Subject: "Your [Policy Type] policy renews in 30 days — here's what's covered"

Content: Summarise their current coverage, highlight key benefits, remind them of their premium. Light CTA: "Reply if you'd like to review your coverage options." No pressure.

2

Touch 2 — 14 Days Out: Action-Oriented

Subject: "Action needed: 2 weeks to renew your [Policy Type] policy"

Content: Restate coverage summary, make it clear what happens if the policy lapses (coverage gap), provide a direct renewal link or phone number. CTA: "Confirm renewal by [date]."

3

Touch 3 — 7 Days Out: Urgency + Rep Escalation

Subject: "Final reminder: 7 days to renew — your rep [Name] is here to help"

Content: Urgent tone (but not panic-inducing), mention coverage lapse risk, introduce the rep by name and direct number. The rep is also notified via Slack/email to make a personal outreach call.

Step 3 — The Agent Code
bash — project setup
mkdir policy-renewal && cd policy-renewal
python3 -m venv venv
source venv/bin/activate
pip install anthropic gspread google-auth google-auth-oauthlib \
    python-dotenv requests
.env
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_SHEET_ID=your-renewal-tracker-sheet-id
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=renewals@yourcompany.com
SMTP_PASSWORD=your-app-password
SENDER_NAME=Renewal Team
RENEWAL_LINK=https://yourcompany.com/renew
REP_SLACK_WEBHOOK=https://hooks.slack.com/services/...  # Reps' Slack channel
renewal_agent.py — full policy renewal outreach agent
import os
import smtplib
import requests
from datetime import date, datetime, timedelta
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from dotenv import load_dotenv
import anthropic
import gspread
from google.oauth2.credentials import Credentials

load_dotenv()
claude = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

# ── Sheet helpers ─────────────────────────────────────────────────────────────

def get_sheet():
    creds  = Credentials.from_authorized_user_file('token.json')
    client = gspread.authorize(creds)
    return client.open_by_key(os.getenv("GOOGLE_SHEET_ID")).sheet1

def get_upcoming_renewals(sheet) -> list[dict]:
    """Return policies renewing in the next 30 days."""
    rows    = sheet.get_all_records()
    today   = date.today()
    upcoming = []

    for i, row in enumerate(rows, start=2):
        renewal_str = row.get('Renewal Date', '')
        if not renewal_str:
            continue
        try:
            renewal_date = datetime.strptime(str(renewal_str), "%Y-%m-%d").date()
        except ValueError:
            try:
                renewal_date = datetime.strptime(str(renewal_str), "%m/%d/%Y").date()
            except ValueError:
                continue

        days_until = (renewal_date - today).days
        if 0 <= days_until <= 30:
            upcoming.append({'row_index': i, 'days_until': days_until, **row})

    return upcoming

def update_col(sheet, row_index: int, col_name: str, value: str):
    headers   = sheet.row_values(1)
    col_index = headers.index(col_name) + 1
    sheet.update_cell(row_index, col_index, value)

# ── Email generation with Claude ──────────────────────────────────────────────

def generate_renewal_email(policy: dict, touch_number: int) -> tuple[str, str]:
    """Generate a personalised renewal email for the given touch number."""
    days    = policy['days_until']
    urgency = {1: "informational", 2: "action-oriented", 3: "urgent"}[touch_number]
    sender  = os.getenv("SENDER_NAME")
    link    = os.getenv("RENEWAL_LINK")

    prompt = f"""Write a {urgency} insurance policy renewal email.

POLICY DETAILS:
- Holder: {policy['Holder Name']}
- Policy Type: {policy['Policy Type']}
- Policy Number: {policy['Policy Number']}
- Coverage Amount: ${policy.get('Coverage Amount', 'N/A')}
- Annual Premium: ${policy.get('Premium', 'N/A')}
- Renewal Date: {policy['Renewal Date']} ({days} days away)
- Renewal Link: {link}

TOUCH NUMBER: {touch_number} of 3
- Touch 1 (30 days): Informational — summarise coverage, light CTA
- Touch 2 (14 days): Action-oriented — clear next steps, renewal deadline  
- Touch 3 (7 days): Urgent — highlight coverage gap risk, introduce rep's 
  personal contact, rep will follow up by phone

TONE: Professional, warm, never fear-mongering. Address holder by first name.

Format:
SUBJECT: [subject line]
BODY:
[email body — under 200 words, plain text, no HTML]"""

    response = claude.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=500,
        messages=[{"role": "user", "content": prompt}]
    )
    text    = response.content[0].text
    lines   = text.strip().split('\n')
    subject = next((l.replace('SUBJECT:', '').strip() for l in lines if l.startswith('SUBJECT:')), 'Your policy renewal')
    body    = '\n'.join(lines[lines.index('BODY:') + 1:]).strip() if 'BODY:' in text else text
    return subject, body

# ── Email sending ─────────────────────────────────────────────────────────────

def send_renewal_email(policy: dict, subject: str, body: str):
    msg = MIMEMultipart()
    msg['From']    = f"{os.getenv('SENDER_NAME')} <{os.getenv('SMTP_USER')}>"
    msg['To']      = policy['Email']
    msg['Subject'] = subject
    msg.attach(MIMEText(body, 'plain'))

    with smtplib.SMTP(os.getenv("SMTP_HOST"), int(os.getenv("SMTP_PORT"))) as s:
        s.starttls()
        s.login(os.getenv("SMTP_USER"), os.getenv("SMTP_PASSWORD"))
        s.send_message(msg)

# ── Rep escalation ────────────────────────────────────────────────────────────

def escalate_to_rep(policy: dict):
    """Notify the rep via Slack when a policy is 7 days out with no response."""
    requests.post(os.getenv("REP_SLACK_WEBHOOK"), json={
        "blocks": [
            {"type": "header", "text": {"type": "plain_text", "text": "⚠️ Policy Renewal Escalation — 7 Days"}},
            {"type": "section", "fields": [
                {"type": "mrkdwn", "text": f"*Holder:*\n{policy['Holder Name']}"},
                {"type": "mrkdwn", "text": f"*Policy:*\n{policy['Policy Number']} ({policy['Policy Type']})"},
                {"type": "mrkdwn", "text": f"*Renewal Date:*\n{policy['Renewal Date']}"},
                {"type": "mrkdwn", "text": f"*Email:*\n{policy['Email']}"},
                {"type": "mrkdwn", "text": f"*Phone:*\n{policy.get('Phone', 'N/A')}"},
                {"type": "mrkdwn", "text": f"*Premium:*\n${policy.get('Premium', 'N/A')}"},
            ]},
            {"type": "section", "text": {"type": "mrkdwn", "text": "No response to 2 previous emails. Please make a personal call today."}},
        ]
    })

# ── Main runner ───────────────────────────────────────────────────────────────

def run_agent():
    sheet    = get_sheet()
    policies = get_upcoming_renewals(sheet)
    today    = date.today().strftime("%Y-%m-%d")

    print(f"Policy Renewal Agent — {len(policies)} policies renewing in 30 days\n")

    for policy in policies:
        name  = policy['Holder Name']
        days  = policy['days_until']
        touch1_sent = policy.get('Touch 1 Sent (30d)', '')
        touch2_sent = policy.get('Touch 2 Sent (14d)', '')
        touch3_sent = policy.get('Touch 3 Sent (7d)', '')

        print(f"{name}: {days} days until renewal")

        # Determine which touch to send
        send_touch = None
        col_name   = None

        if days <= 30 and not touch1_sent:
            send_touch = 1
            col_name   = 'Touch 1 Sent (30d)'
        elif days <= 14 and not touch2_sent:
            send_touch = 2
            col_name   = 'Touch 2 Sent (14d)'
        elif days <= 7 and not touch3_sent:
            send_touch = 3
            col_name   = 'Touch 3 Sent (7d)'

        if not send_touch:
            print(f"  → Already sent touch for this window, skipping")
            continue

        print(f"  → Generating Touch {send_touch} email...")
        subject, body = generate_renewal_email(policy, send_touch)

        print(f"  → Sending to {policy['Email']}...")
        try:
            send_renewal_email(policy, subject, body)
            update_col(sheet, policy['row_index'], col_name, today)
            print(f"  ✓ Touch {send_touch} sent")

            # Escalate to rep at Touch 3
            if send_touch == 3:
                escalate_to_rep(policy)
                update_col(sheet, policy['row_index'], 'Rep Escalated', today)
                print(f"  ✓ Rep escalated via Slack")

        except Exception as e:
            print(f"  ✗ Failed: {e}")

    print("\nDone.")

if __name__ == "__main__":
    run_agent()
Step 4 — Deploy and Schedule
The agent should run every morning. Here's how to schedule it on different platforms.
  1. 1

    Schedule on Mac/Linux with cron

    Open your crontab: crontab -e. Add this line (runs at 8am every day):

crontab — run at 8am daily
0 8 * * * /path/to/policy-renewal/venv/bin/python /path/to/policy-renewal/renewal_agent.py >> /tmp/renewal-agent.log 2>&1
  1. 2

    Deploy to a cloud server (recommended for reliability)

    For production, deploy to a small cloud server so it runs even when your laptop is off. Options: Railway (easiest, deploy from GitHub with a Procfile containing worker: python renewal_agent.py) or a $5/month Hetzner or DigitalOcean VPS with a cron job. Add a requirements.txt with your pip packages for Railway deployment.

  2. 3

    Add a requirements.txt

requirements.txt
anthropic>=0.25.0
gspread>=6.0.0
google-auth>=2.0.0
google-auth-oauthlib>=1.0.0
requests>=2.31.0
python-dotenv>=1.0.0
Extend It

Reply detection & auto-update

Add a Gmail poller that watches for replies from policyholders. When they reply, update the Status column to "Responded" and remove them from future touches.

SMS reminders via Twilio

For Touch 3 (7 days), add an SMS message via Twilio in addition to the email. Response rates for SMS are 5–8× higher than email for time-sensitive reminders.

Upsell opportunity detection

At Touch 1 (30 days), have Claude analyse the current coverage and suggest a relevant add-on (e.g. umbrella policy for auto customers, rider for home insurance). Include a brief mention in the email.

Renewal rate analytics

Add a weekly summary email to the manager showing: renewals processed, response rates per touch, lapse rate, and premium revenue at risk — all calculated from the Google Sheet data.