Small Business Setups ⏱ 20 min to set up ✓ Tested March 2026

Personal AI CRM: Track Key Relationships Without Expensive Software

You don't need Salesforce. You need a text file and a Python script. This setup uses a plain contacts.md file plus an AI agent that reads it every morning and tells you exactly who to reach out to today — based on when you last talked and how often you said you'd follow up. No subscription, no UI to learn, no data locked in someone else's cloud. Set up in 20 minutes. Running this for 3 months of Ask Patrick business development.

The Problem

Most solopreneurs and consultants don't forget their relationships intentionally. They just don't have a system. You had a great call with someone three weeks ago and said "let's talk again in two weeks" — and then you forgot. The potential client went cold. The referral partner moved on.

Paid CRM tools are designed for sales teams with pipelines, stages, and deal values. You don't have that. You have 40–80 important relationships and you need to remember to nurture them. That's a $0 problem, not a $50/month problem.

What You're Building

A two-part system:

  1. contacts.md — A plain text file where each contact has a name, relationship context, last contact date, and a follow-up interval in days.
  2. crm_digest.py — A Python script that reads the file, identifies who's overdue for contact, sends you a daily digest (email or Discord message), and optionally generates a draft opening line via Claude.

You run the script daily via cron. It takes 2–3 seconds and costs roughly $0.003 per run with Claude Haiku.

The contacts.md Format

Copy this template and start filling it in. Each contact is a YAML-like block separated by ---:

# contacts.md
# Personal CRM — Updated manually after each interaction

---
name: Sarah Chen
role: VP of Product at Acme Corp
relationship: Warm lead — interested in the $29 Workshop tier
last_contact: 2026-02-18
follow_up_in_days: 14
contact_via: email
notes: |
 Had a 30-min call. She's building internal agents for customer support.
 Mentioned she's evaluating 3 vendors. Decision in March.
 She prefers email over LinkedIn.

---
name: Marcus Webb
role: Founder, Relay.so
relationship: Peer — good for referrals and collaboration
last_contact: 2026-02-28
follow_up_in_days: 21
contact_via: twitter_dm
notes: |
 We co-authored a thread on agent memory in January.
 He mentioned wanting to do a joint webinar. Haven't followed up.
 Reach out about scheduling something for April.

---
name: Priya Nair
role: Independent consultant, AI workflows
relationship: Customer (Library tier since Jan 2026)
last_contact: 2026-03-01
follow_up_in_days: 30
contact_via: discord
notes: |
 Active in the Discord. Asked a great question about event-driven agents last week.
 Check in monthly — she's the type to upgrade if she sees value.

---
name: Tom Okafor
role: CTO at Buildstack
relationship: Cold intro via Marcus — hasn't responded yet
last_contact: 2026-02-10
follow_up_in_days: 21
contact_via: email
notes: |
 Marcus intro'd us. Sent first email Feb 10. No response.
 Standard: follow up once more after 3 weeks, then move to 90-day cycle.

The Python Script

Save this as crm_digest.py. Requires: anthropic, python-dateutil, and either resend (for email) or just stdout for Discord webhook delivery.

#!/usr/bin/env python3
# crm_digest.py — Personal AI CRM digest
# Run daily via cron: 0 8 * * * python3 ~/crm_digest.py

import os
import re
import json
from datetime import date, timedelta
from dateutil.parser import parse as parse_date
import anthropic

CONTACTS_FILE = os.path.expanduser("~/contacts.md")
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
OVERDUE_THRESHOLD = 0 # days past due before flagging (0 = flag on due date)
UPCOMING_WINDOW = 3 # also show contacts due in next N days

client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)

def parse_contacts(filepath: str) -> list[dict]:
 """Parse contacts.md into a list of contact dicts."""
 with open(filepath, "r") as f:
 content = f.read()

 contacts = []
 blocks = content.split("---")
 for block in blocks:
 block = block.strip()
 if not block or block.startswith("#"):
 continue

 contact = {}
 # Extract simple key: value fields
 for line in block.split("\n"):
 match = re.match(r'^(\w+):\s*(.+)$', line.strip())
 if match:
 key, val = match.groups()
 contact[key] = val.strip()

 # Extract multi-line notes
 notes_match = re.search(r'^notes:\s*\|\n((?: .+\n?)*)', block, re.MULTILINE)
 if notes_match:
 notes_raw = notes_match.group(1)
 contact["notes"] = re.sub(r'^ ', '', notes_raw, flags=re.MULTILINE).strip()

 if "name" in contact and "last_contact" in contact:
 contacts.append(contact)

 return contacts

def get_due_contacts(contacts: list[dict]) -> tuple[list, list]:
 """Return (overdue, upcoming) contact lists."""
 today = date.today()
 overdue = []
 upcoming = []

 for c in contacts:
 try:
 last = parse_date(c["last_contact"]).date()
 interval = int(c.get("follow_up_in_days", 14))
 due_date = last + timedelta(days=interval)
 days_until = (due_date - today).days

 c["due_date"] = due_date
 c["days_until"] = days_until
 c["days_overdue"] = max(0, -days_until)

 if days_until <= -OVERDUE_THRESHOLD:
 overdue.append(c)
 elif 0 <= days_until <= UPCOMING_WINDOW:
 upcoming.append(c)
 except Exception as e:
 print(f"Skipping {c.get('name', '?')}: {e}")

 overdue.sort(key=lambda x: x["days_overdue"], reverse=True)
 upcoming.sort(key=lambda x: x["days_until"])
 return overdue, upcoming

def generate_opener(contact: dict) -> str:
 """Use Claude Haiku to draft a short opening line for outreach."""
 prompt = f"""You're helping someone reach out to a contact. Write ONE sentence — a natural, non-salesy opening line for a message to {contact['name']}.

Context about {contact['name']}:
- Role: {contact.get('role', 'unknown')}
- Relationship: {contact.get('relationship', 'unknown')}
- Notes: {contact.get('notes', 'none')}
- Preferred contact method: {contact.get('contact_via', 'email')}
- Days overdue for follow-up: {contact.get('days_overdue', 0)}

Output only the single opening sentence. No subject line. No greeting. Just the first sentence of the message body."""

 response = client.messages.create(
 model="claude-haiku-4-5",
 max_tokens=100,
 messages=[{"role": "user", "content": prompt}]
 )
 return response.content[0].text.strip()

def build_digest(overdue: list, upcoming: list) -> str:
 """Build the digest message text."""
 today = date.today().strftime("%A, %B %d")
 lines = [f"# CRM Digest — {today}\n"]

 if not overdue and not upcoming:
 lines.append("✅ No contacts due today or in the next 3 days. You're caught up.")
 return "\n".join(lines)

 if overdue:
 lines.append(f"## 🔴 Overdue ({len(overdue)})")
 for c in overdue:
 lines.append(f"\n### {c['name']}")
 lines.append(f"**Role:** {c.get('role', '—')}")
 lines.append(f"**Relationship:** {c.get('relationship', '—')}")
 lines.append(f"**Overdue by:** {c['days_overdue']} days")
 lines.append(f"**Contact via:** {c.get('contact_via', 'email')}")
 if c.get('notes'):
 lines.append(f"**Notes:** {c['notes'][:200]}{'...' if len(c.get('notes','')) > 200 else ''}")
 opener = generate_opener(c)
 lines.append(f"**Draft opener:** _{opener}_")

 if upcoming:
 lines.append(f"\n## 🟡 Coming Up ({len(upcoming)})")
 for c in upcoming:
 due_label = "Today" if c["days_until"] == 0 else f"In {c['days_until']} day(s)"
 lines.append(f"- **{c['name']}** — {due_label} ({c.get('role', '—')})")

 return "\n".join(lines)

def main():
 contacts = parse_contacts(CONTACTS_FILE)
 overdue, upcoming = get_due_contacts(contacts)
 digest = build_digest(overdue, upcoming)
 print(digest)

 # Optionally: send to Discord webhook
 # import requests
 # webhook_url = os.environ.get("DISCORD_WEBHOOK_URL")
 # if webhook_url:
 # requests.post(webhook_url, json={"content": digest[:1900]})

if __name__ == "__main__":
 main()

Schedule It

Run it every morning at 8 AM:

# Add to crontab: crontab -e
0 8 * * * source ~/.zshrc && python3 ~/crm_digest.py >> ~/crm-digest.log 2>&1

# Or via OpenClaw cron:
openclaw cron add \
 --name "crm-digest" \
 --cron "0 8 * * *" \
 --session isolated \
 --message "Run ~/crm_digest.py and post the output to #daily-ops"

The AI CRM Agent System Prompt

If you're running this inside an OpenClaw agent rather than a standalone script, use this system prompt in your agent's SOUL.md or task file:

## CRM Agent Instructions

You manage my personal CRM. When I say "CRM digest" or "who should I contact today":

1. Read ~/contacts.md
2. For each contact, calculate: last_contact + follow_up_in_days = due_date
3. Identify anyone where due_date <= today (overdue) or due_date <= today+3 (upcoming)
4. For overdue contacts: show name, how many days overdue, their notes, and draft a one-sentence opener
5. For upcoming: just list them with the due date

Rules:
- Never fabricate contact details. Only use what's in contacts.md.
- Draft openers must feel natural for the relationship type (warm lead vs. peer vs. customer)
- If contacts.md hasn't been updated in >3 days, flag this — it means I'm not maintaining it
- Output plain markdown, not JSON

When I say "update [name] — [details]", append an interaction note to their contacts.md entry:
 last_contact: [today's date]
 [Add a brief note about what was discussed]

Updating the File After an Interaction

The system only works if you keep it current. Here's the habit:

After a call or meeting: Open contacts.md, find the person, update last_contact to today's date, and add a note line describing what was discussed and what was agreed. Takes 90 seconds.

After sending an email: Same thing. Update last_contact and optionally set a shorter follow_up_in_days if you're waiting on a response.

After no response: If you followed up and heard nothing, note it. Consider extending the interval to 45 or 90 days rather than deleting the contact. Cold doesn't mean dead.

Example update

---
name: Sarah Chen
role: VP of Product at Acme Corp
relationship: Warm lead — in evaluation stage
last_contact: 2026-03-06 ← updated today
follow_up_in_days: 10 ← shortened because she's deciding soon
contact_via: email
notes: |
 Had a 30-min call. She's building internal agents for customer support.
 Evaluating 3 vendors. Decision in March.
 2026-03-06: Sent proposal. She asked about volume pricing. Following up in 10 days.

What Triggers a Follow-Up Suggestion

The script flags a contact when: today >= last_contact + follow_up_in_days. That's it. No magic. The intelligence is in the intervals you set:

Adjust intervals based on the relationship and what you agreed to. If you said "let's talk in two weeks," set follow_up_in_days: 14. Don't overthink it.

Cost

contacts.md with 50 contacts: ~2,000 tokens to read and process

10 overdue contacts with draft openers: ~1,000 tokens of Claude Haiku output

Total per run: ~3,000 tokens — approximately $0.003 at Haiku pricing ($1/MTok input, $5/MTok output)

Annual cost at daily runs: ~$1.10/year

If you have 200 contacts and 30 are overdue daily, you're looking at ~$4–5/year. Still essentially free.

Common Mistakes

Mistake 1: Letting contacts.md go stale

If you don't update last_contact after an interaction, you'll get a false alert. The file is the source of truth — if you don't maintain it, the agent can't help you. Build the 90-second update habit or the system degrades within a week.

Mistake 2: Too many contacts

Don't dump your entire LinkedIn into this file. This is for relationships that genuinely matter to your business. 40–80 contacts is the right size. More than 100 and you're building a CRM you won't maintain.

Mistake 3: Uniform intervals for everyone

Don't set every contact to 14 days. That's how you generate 20 follow-ups a day and start ignoring the digest. Calibrate: close leads get short intervals, long-term relationships get longer ones.

Mistake 4: Relying on the opener without reading the notes

The draft opener is a starting point, not a finished message. Always read the notes before sending. The AI doesn't know about the conversation you had last week — only what you wrote down.

Mistake 5: Not including a "cold" handling strategy

When someone goes unresponsive, don't delete them. Change their interval to 90 days and add a note: "No response after 2 follow-ups — light touch quarterly." This keeps your options open without burning time.

The discipline is the system. The Python script is 60 lines. The real value is the habit: update the file within an hour of every interaction. If you do that, you'll never forget to follow up with anyone important again.

What pairs with this

You have the CRM. These connect naturally:

Browse the Library →

You're already in. Everything is here.

← Back to Library