SOP: Extract Meeting Contacts from Email/Calendar → Baserow CRM

Version: 1.1 Created: 2026-03-11 Author: Dmitri Sunshine / Claude Last Updated: 2026-03-12 Frequency: Run once per email account, then periodically for new contacts Change Log: v1.1 — Corrected .env path, added migration-state.json reference, improved dedup docs


Purpose

Extract contact information from meeting scheduling platforms (Calendly, Cal.com, Reclaim, TidyCal) and Google Calendar events, then import into the Baserow CRM People table with deduplication against existing data.


Prerequisites

  1. Cowork with Gmail connector — connected to the target Gmail account
  2. Cowork with Google Calendar connector — connected to the same Google account
  3. Baserow credentials in solanasis-scripts/.env (NOT in the baserow/ subdirectory):
    • BASEROW_DB_TOKEN — API token with Create/Read/Update/Delete permissions (row CRUD)
    • BASEROW_DATABASE_ID — Database ID (currently 54, self-hosted)
    • BASEROW_EMAIL — login email (for JWT auth to discover table IDs)
    • BASEROW_PASSWORD — login password (for JWT auth)
    • Note: The import script automatically loads from ../ (parent) .env when run from solanasis-scripts/baserow/
  4. Table ID map at solanasis-scripts/scripts/migration-data/table_id_map.json:
    • Contains all table IDs on self-hosted Baserow (People=267, Organization=266, Tag=264, Location=265, Meeting Notes=272, Foundation Prospects=271)
    • The import script reads this automatically to find the People table ID
    • If this file is missing, the script falls back to JWT auth or BASEROW_PEOPLE_TABLE_ID env var
  5. Node.js 22+ and npm installed
  6. tsx installed globally: npm install -g tsx

Step-by-Step Process

Step 1: Extract Contacts via Cowork (15 min)

In a Cowork session with Gmail + Calendar connectors:

  1. Ask Claude to search for all Calendly notification emails:

    Search Gmail for: from:notifications@calendly.com subject:"New Event"
    
  2. Ask Claude to search for Cal.com booking notifications:

    Search Gmail for: from:notifications@cal.com subject:"new booking"
    
  3. Ask Claude to pull Google Calendar events in 3-month chunks:

    List calendar events from [start] to [end] with full details
    
  4. For each email/event, Claude extracts:

    • Name — from email body (“Invitee: [Name]”) or calendar event title
    • Email — from Reply-To header (Calendly) or calendar attendee list
    • Phone — from email body if available (Cal.com forms may include this)
    • Notes — from “Please share anything…” field or event description
    • Connected From — Calendly, Cal.com, Google Calendar, Reclaim, TidyCal
    • Meeting Date — from email or event
  5. Claude saves all contacts to: solanasis-scripts/baserow/extracted-contacts-YYYY-MM-DD.json

Step 2: Review the Extracted JSON (5 min)

Open the JSON file and verify:

  • All contacts have valid emails
  • Names look correct (partial names are flagged with “Needs Full Name”)
  • No personal contacts that should be excluded
  • Connected From values are accurate
  • Notes contain useful context

Step 3: Run the Import Script — Dry Run First (2 min)

cd solanasis-scripts/baserow
npx tsx import-contacts.ts --dry-run

This will:

  • Connect to Baserow and pull all existing People rows
  • Compare extracted emails against existing data
  • Show which contacts are NEW vs already exist (DUPLICATES)
  • NOT make any changes

Review the output carefully before proceeding.

Step 4: Run the Import Script — Live (2 min)

cd solanasis-scripts/baserow
npx tsx import-contacts.ts

This will:

  • Batch-insert all new contacts into the People table
  • Skip any contacts whose email already exists in Baserow
  • Generate an import report JSON file

Step 5: Verify in Baserow (5 min)

  1. Open Baserow in the browser
  2. Navigate to the People table
  3. Sort by “Created On” (newest first)
  4. Spot-check 5 random new contacts:
    • Name matches
    • Email is correct and lowercase
    • Connected From value is set
    • Notes contain meeting context
  5. Check total row count matches expected

Running for a Different Email Account

This process is designed to be repeatable. For the second email account:

  1. Connect the other Gmail account in Cowork (may need to disconnect/reconnect)
  2. Follow Steps 1-2 with the new account (Cal.com notifications may be here)
  3. Save to a different JSON (e.g., extracted-contacts-account2-YYYY-MM-DD.json)
  4. Run the import script with --file flag:
    npx tsx import-contacts.ts --file extracted-contacts-account2-2026-03-11.json
  5. The dedup logic will automatically prevent duplicate entries since it checks email against existing Baserow data

Data Field Mapping

Source FieldBaserow FieldNotes
Invitee Name / Event TitleNameAs-is from source
Reply-To header / Attendee emailEmailLowercased, trimmed
Form “Phone” fieldPhone NumberOften empty from Calendly notifications
”Please share…” / Event descriptionNotesCombined with meeting date and source
Platform nameConnected FromEnum: Calendly, Cal.com, Google Calendar, Reclaim, TidyCal
Event dateNotes (embedded)“First meeting: YYYY-MM-DD” at top of Notes

Edge Cases

ScenarioHandling
Canceled meetingInclude contact, set Connected From to “Calendly (Canceled)“
Multiple emails, same person (Brad Smith)Use primary email, note alternate in Notes
First name only (Sam, Arshan)Insert as-is, add “Needs Full Name” in Notes
Additional guests on CalendlyCreate separate contact per guest
Personal contacts (coaching, doula)Include, add “Personal contact, non-business context” in Notes
Duplicate email in batchScript skips batch duplicates automatically
Calendar connector timeoutUse Gmail invitation emails as supplementary source
No phone number availableLeave Phone Number empty; enrich later via Calendly CSV export

Phone Number Enrichment (Optional)

Calendly host notification emails do NOT include phone numbers. To get them:

  1. Log in to Calendly dashboard
  2. Go to Settings → Account → Export Data (or Scheduled Events)
  3. Download CSV with all event details including form responses
  4. The CSV will contain phone numbers if they were collected via form fields
  5. Match by email to update Phone Number field in Baserow

Same process applies for Cal.com: export from Cal.com dashboard.


Deduplication Strategy

The import script uses email-based deduplication:

  1. Pulls ALL existing People rows from Baserow
  2. Builds a Map: email (lowercase) → row ID
  3. For each new contact:
    • If email already exists → SKIP (logged in report)
    • If email is new → QUEUE for insertion
  4. Within the batch: also checks for in-batch duplicates

Baserow does not support unique constraints on fields at the API level. Uniqueness is enforced entirely in the import script. Always run a dry run first.


Troubleshooting

IssueSolution
ERROR: Missing required env varCheck .env file has all 4 variables
JWT auth failedVerify BASEROW_EMAIL and BASEROW_PASSWORD. If 2FA is on, set BASEROW_PEOPLE_TABLE_ID directly
No "People" table foundTable may be named differently. Check Baserow UI for exact name
POST batch failed: 400A field value may be invalid. Script falls back to individual inserts
Calendar connector timeoutUse shorter date ranges (1-3 months)
HTTP 429 Too Many RequestsWait and retry. Script has no built-in rate limiting for the import script (the main BaserowClient does)

Files Reference

FilePurpose
solanasis-scripts/.envBaserow credentials (DB token, email, password, database ID)
solanasis-scripts/scripts/migration-data/table_id_map.jsonTable IDs on self-hosted Baserow
solanasis-scripts/baserow/import-contacts.tsMain import script (reads .env from parent dir)
solanasis-scripts/baserow/extracted-contacts-*.jsonExtracted contact data (input)
solanasis-scripts/baserow/dedup-analysis-*.jsonDedup analysis showing new vs existing contacts
solanasis-scripts/baserow/import-report-*.jsonImport results (output)
solanasis-scripts/baserow/src/lib/baserow-client.tsReusable BaserowClient with rate limiting, retry, batch ops
solanasis-docs/misc/solanasis-email-to-baserow-crm-extraction-plan.mdDetailed extraction plan with decision log
solanasis-docs/misc/solanasis-email-to-crm-sop.mdThis SOP

Pro Tips

  1. Reply-To header is most reliable — Calendly sets it to the invitee’s actual email, cleaner than parsing HTML
  2. Run dry-run first, always — prevents accidental duplicate inserts if script is run twice
  3. Calendly CSV export is the fastest way to get phone numbers that forms collected
  4. For Cal.com: booking notification format differs from Calendly — may need parser adjustments
  5. Email normalization gotcha: Some people use plus-addressing (john+calendly@gmail.com vs john@gmail.com) — the script normalizes but doesn’t strip plus-suffixes (add this if it becomes an issue)
  6. Batch size limit: Baserow API supports max 200 rows per batch — script handles this automatically

Lessons Learned (2026-03-13 Import)

  1. Email-based dedup requires email fields to be populated. The Coda migration left many rows without Email values. The import script’s email-based dedup couldn’t match these, nearly creating 11 duplicates. Always enrich missing emails on existing rows before running the import.
  2. Name-based dedup is a preview, not gospel. The pre-import name analysis found 23 existing and 20 new, but live email dedup found 12 existing and 31 new. The difference was 11 rows missing emails + 1 contact (Arshan) that existed by email but not by name match. Always verify with a dry-run against live data.
  3. Email aliases cause false negatives. Brad Smith had brad.smith@philanthropy.network in Baserow but brad@philanthropy.network in the extraction. The email dedup treated him as new. Check for domain-matching aliases on contacts that show up in both the “existing” name analysis and the “new” email dedup.
  4. ESM compatibility matters. The project uses "type": "module" in package.json, so __dirname is not available. Scripts need fileURLToPath(import.meta.url) instead. Catch this early with a dry-run.
  5. Connected From enrichment is a bonus. Coda-migrated rows had empty Connected From fields. Patching these during the email enrichment step added useful provenance data at negligible cost.
  6. Dry-run, enrich, dry-run, import. The reliable sequence is: (1) dry-run to compare email dedup vs name analysis, (2) enrich missing emails on existing rows, (3) dry-run again to confirm counts, (4) live import.