Monitors a spreadsheet of renewal dates, sends personalised email outreach at 30, 14, and 7 days before expiry, tracks responses, and escalates non-responders to a rep — fully automated.
Follow the Gmail API setup for OAuth credentials. You need Google Sheets API + Gmail send access (or any SMTP).
Used to personalise each renewal email based on the policy details. Get one at console.anthropic.com.
We'll create this in Step 1. It's the central source of truth for all policy renewals.
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
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.
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.
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.
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]."
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.
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
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
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()
Open your crontab: crontab -e. Add this line (runs at 8am every day):
0 8 * * * /path/to/policy-renewal/venv/bin/python /path/to/policy-renewal/renewal_agent.py >> /tmp/renewal-agent.log 2>&1
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.
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
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.
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.
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.
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.