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
- Cowork with Gmail connector — connected to the target Gmail account
- Cowork with Google Calendar connector — connected to the same Google account
- 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 (currently54, 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).envwhen run fromsolanasis-scripts/baserow/
- 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_IDenv var
- Node.js 22+ and npm installed
- 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:
-
Ask Claude to search for all Calendly notification emails:
Search Gmail for: from:notifications@calendly.com subject:"New Event" -
Ask Claude to search for Cal.com booking notifications:
Search Gmail for: from:notifications@cal.com subject:"new booking" -
Ask Claude to pull Google Calendar events in 3-month chunks:
List calendar events from [start] to [end] with full details -
For each email/event, Claude extracts:
- Name — from email body (“Invitee: [Name]”) or calendar event title
- Email — from
Reply-Toheader (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
-
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-runThis 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.tsThis 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)
- Open Baserow in the browser
- Navigate to the People table
- Sort by “Created On” (newest first)
- Spot-check 5 random new contacts:
- Name matches
- Email is correct and lowercase
- Connected From value is set
- Notes contain meeting context
- Check total row count matches expected
Running for a Different Email Account
This process is designed to be repeatable. For the second email account:
- Connect the other Gmail account in Cowork (may need to disconnect/reconnect)
- Follow Steps 1-2 with the new account (Cal.com notifications may be here)
- Save to a different JSON (e.g.,
extracted-contacts-account2-YYYY-MM-DD.json) - Run the import script with
--fileflag:npx tsx import-contacts.ts --file extracted-contacts-account2-2026-03-11.json - The dedup logic will automatically prevent duplicate entries since it checks email against existing Baserow data
Data Field Mapping
| Source Field | Baserow Field | Notes |
|---|---|---|
| Invitee Name / Event Title | Name | As-is from source |
| Reply-To header / Attendee email | Lowercased, trimmed | |
| Form “Phone” field | Phone Number | Often empty from Calendly notifications |
| ”Please share…” / Event description | Notes | Combined with meeting date and source |
| Platform name | Connected From | Enum: Calendly, Cal.com, Google Calendar, Reclaim, TidyCal |
| Event date | Notes (embedded) | “First meeting: YYYY-MM-DD” at top of Notes |
Edge Cases
| Scenario | Handling |
|---|---|
| Canceled meeting | Include 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 Calendly | Create separate contact per guest |
| Personal contacts (coaching, doula) | Include, add “Personal contact, non-business context” in Notes |
| Duplicate email in batch | Script skips batch duplicates automatically |
| Calendar connector timeout | Use Gmail invitation emails as supplementary source |
| No phone number available | Leave 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:
- Log in to Calendly dashboard
- Go to Settings → Account → Export Data (or Scheduled Events)
- Download CSV with all event details including form responses
- The CSV will contain phone numbers if they were collected via form fields
- 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:
- Pulls ALL existing People rows from Baserow
- Builds a Map:
email (lowercase) → row ID - For each new contact:
- If email already exists → SKIP (logged in report)
- If email is new → QUEUE for insertion
- 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
| Issue | Solution |
|---|---|
ERROR: Missing required env var | Check .env file has all 4 variables |
JWT auth failed | Verify BASEROW_EMAIL and BASEROW_PASSWORD. If 2FA is on, set BASEROW_PEOPLE_TABLE_ID directly |
No "People" table found | Table may be named differently. Check Baserow UI for exact name |
POST batch failed: 400 | A field value may be invalid. Script falls back to individual inserts |
| Calendar connector timeout | Use shorter date ranges (1-3 months) |
HTTP 429 Too Many Requests | Wait and retry. Script has no built-in rate limiting for the import script (the main BaserowClient does) |
Files Reference
| File | Purpose |
|---|---|
solanasis-scripts/.env | Baserow credentials (DB token, email, password, database ID) |
solanasis-scripts/scripts/migration-data/table_id_map.json | Table IDs on self-hosted Baserow |
solanasis-scripts/baserow/import-contacts.ts | Main import script (reads .env from parent dir) |
solanasis-scripts/baserow/extracted-contacts-*.json | Extracted contact data (input) |
solanasis-scripts/baserow/dedup-analysis-*.json | Dedup analysis showing new vs existing contacts |
solanasis-scripts/baserow/import-report-*.json | Import results (output) |
solanasis-scripts/baserow/src/lib/baserow-client.ts | Reusable BaserowClient with rate limiting, retry, batch ops |
solanasis-docs/misc/solanasis-email-to-baserow-crm-extraction-plan.md | Detailed extraction plan with decision log |
solanasis-docs/misc/solanasis-email-to-crm-sop.md | This SOP |
Pro Tips
- Reply-To header is most reliable — Calendly sets it to the invitee’s actual email, cleaner than parsing HTML
- Run dry-run first, always — prevents accidental duplicate inserts if script is run twice
- Calendly CSV export is the fastest way to get phone numbers that forms collected
- For Cal.com: booking notification format differs from Calendly — may need parser adjustments
- 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)
- Batch size limit: Baserow API supports max 200 rows per batch — script handles this automatically
Lessons Learned (2026-03-13 Import)
- 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.
- 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.
- Email aliases cause false negatives. Brad Smith had
brad.smith@philanthropy.networkin Baserow butbrad@philanthropy.networkin 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. - ESM compatibility matters. The project uses
"type": "module"in package.json, so__dirnameis not available. Scripts needfileURLToPath(import.meta.url)instead. Catch this early with a dry-run. - 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.
- 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.