|
| 1 | +--- |
| 2 | +title: "CRM Starter Export — the pipeline shape, enriched from the canonical layer: orgs first, then people attached" |
| 3 | +lede: "Two CSVs that seed the new CRM from everything augment-it already knows: one row per organization carrying the Master Pipeline Tracker's columns plus identity links and pulse streams (never corpora), then one per person with the key that attaches them to the right org." |
| 4 | +date_created: 2026-07-27 |
| 5 | +date_modified: 2026-07-27 |
| 6 | +authors: |
| 7 | + - Michael Staton |
| 8 | +augmented_with: |
| 9 | + - Claude Code on Claude Fable 5 |
| 10 | +semantic_version: 0.0.0.1 |
| 11 | +tags: |
| 12 | + - Plan |
| 13 | + - Augment-It |
| 14 | + - CRM |
| 15 | + - Export |
| 16 | + - Canonical-Layer |
| 17 | + - Batch-Import |
| 18 | +status: Draft |
| 19 | +--- |
| 20 | + |
| 21 | +# CRM Starter Export — orgs, then people |
| 22 | + |
| 23 | +## Why care? |
| 24 | + |
| 25 | +Months of capture work live in the canonical layer — 364 organizations |
| 26 | +visible to reach-edu (255 with identity links, 67 with pulse streams), |
| 27 | +417 persons, 378 person→org affiliations, plus the operator-maintained |
| 28 | +Master Pipeline Tracker (96 rows, 36 columns). The new CRM starts empty. |
| 29 | +This plan turns what we already know into its starter data: **batch-import |
| 30 | +CSVs that replicate the pipeline export's shape and enrich it from the |
| 31 | +canonical layer** — identity links and pulse streams included, corpora |
| 32 | +deliberately excluded — in the only order batch insertion works: |
| 33 | +**organizations first, people second, attached by a stable key.** |
| 34 | + |
| 35 | +## Ground truth (verified 2026-07-27) |
| 36 | + |
| 37 | +- **The pipeline shape to replicate:** record set |
| 38 | + `2026-06-10_Master-Pipeline-Tracker--Active-Pipeline_v10` (row-store, |
| 39 | + `rs_mq7k9jaw_wsjkfl`). 24 human columns (Prospect/Organization, Type, |
| 40 | + Owner, Stage, Total Commitment, FY26/FY27 revenue + weighted, Last |
| 41 | + Contact, Notes/Context, Next Step block, event/RSVP, staleness autos) + |
| 42 | + 12 augmentation columns — including **`corpus_funder_slug`** and |
| 43 | + **`record_uuid`**, which make the pipeline↔canonical join mostly exact, |
| 44 | + not fuzzy. |
| 45 | +- **Canonical org fields available:** slug, complete/conventional names, |
| 46 | + `aliases[]`, `domains[]`, `org_links[]` (19 kinds in live use — |
| 47 | + website, linkedin_company, x_profile, facebook/instagram/bluesky, |
| 48 | + youtube, wikipedia, substack, team_page, …), `media_streams[]` |
| 49 | + (name/url/kind), per-client `tags` (24 org tag observations so far), |
| 50 | + and the new org↔org relations (parent/child/peer + kind + description). |
| 51 | +- **Canonical person fields:** person_uuid, name (+ first/surname where |
| 52 | + captured), email, linkedin_profile_url, headline, `personal_links[]`; |
| 53 | + affiliation edges carry role (`kind`) and operator-rated `relevance`. |
| 54 | +- **House precedent:** `scripts/export-affiliation-ratings-csv.mjs` / |
| 55 | + `export-event-attendees-csv.mjs` — direct-SurrealDB export scripts, |
| 56 | + arg-parsed, writing under `clients/<client>/outputs/<dated-dir>/`. The |
| 57 | + affiliation export's header comment also settles a design question |
| 58 | + below (per-edge vs per-person rows) — and this plan deliberately |
| 59 | + inverts it for CRM contacts. |
| 60 | + |
| 61 | +## The two invariants |
| 62 | + |
| 63 | +1. **External IDs ride every row.** Orgs export `external_id = slug`; |
| 64 | + people export `external_id = person_uuid` and `org_external_id = slug`. |
| 65 | + Whatever the CRM is, these columns land in it (native external-id field |
| 66 | + or a custom field) — they are what makes the people-attach step exact, |
| 67 | + re-imports idempotent, and any future sync possible. Without them the |
| 68 | + CRM join degrades to name-matching forever. |
| 69 | +2. **No corpora.** `org_corpus` / `personal_corpus` stay home. The CRM |
| 70 | + gets identity and pulse surface; the corpus stays augment-it's. |
| 71 | + |
| 72 | +## Phase 1 — `scripts/export-crm-orgs-csv.mjs` |
| 73 | + |
| 74 | +One row per organization with `client_access CONTAINS <client>`. |
| 75 | + |
| 76 | +**Scope (recommendation: everything, filter in the sheet).** Export all |
| 77 | +364 and include classification columns — `bucket` (derived from the |
| 78 | +org's disk folder under `corpus/`: funders / gov-entities / think-tanks / |
| 79 | +associations-networks / academic-institutions / data-services, else |
| 80 | +blank) and `tags` (the per-client has_tag values). The operator filters |
| 81 | +rows in the spreadsheet before import; the script doesn't guess which |
| 82 | +orgs the CRM deserves. (Redundancy-over-normalization + operator-drives.) |
| 83 | + |
| 84 | +**Column groups, in order:** |
| 85 | + |
| 86 | +| Group | Columns | |
| 87 | +|---|---| |
| 88 | +| Identity | `external_id` (slug), `name` (complete ?? conventional ?? slug), `conventional_name`, `aliases` (pipe-joined), `domains` (pipe-joined), `bucket`, `tags` (pipe-joined) | |
| 89 | +| Identity links (flattened by kind) | `website`, `linkedin`, `x`, `facebook`, `instagram`, `youtube`, `wikipedia`, `bluesky`, `substack`, `team_page` — first URL of each kind; everything else (about, org_profile, publication, …) into `other_links` (newline-joined `kind: url`) | |
| 90 | +| Pulse streams | `streams` (newline-joined `name — url (kind)`), `stream_count` — streams are multi-valued by nature; they land in one long-text column the CRM stores as a note/custom field, not N columns that cap the list | |
| 91 | +| Relations | `related_orgs` (newline-joined `rel: slug (kind)` from `organization.relations` semantics) — the family tree survives the export even if the CRM can't model it yet | |
| 92 | +| Pipeline (the replicated columns) | The v10 human columns as-is: `Type, Owner, Stage, Total Commitment ($), FY26/FY27 Revenue, Probability, Weighted FY26/FY27, Last Contact/Update, Notes/Context, Next Step, Next Step Due/Owner/Status, Upcoming Event, RSVP Status` — filled only on rows the join matched; blank for captured-but-not-in-pipeline orgs | |
| 93 | +| Provenance | `pipeline_matched` (exact / fuzzy / none), `exported_at` | |
| 94 | + |
| 95 | +**The pipeline join, exact-first:** |
| 96 | + |
| 97 | +1. Exact: pipeline row's `corpus_funder_slug` == org slug (the promotion |
| 98 | + path already stamped it). |
| 99 | +2. Alias: pipeline `Prospect / Organization` name (stripped of |
| 100 | + parentheticals) matched against slug + aliases, lowercased — same |
| 101 | + matching family as `searchOrgs`. |
| 102 | +3. Anything still unmatched on either side is REPORTED, not dropped: the |
| 103 | + script prints unmatched pipeline rows (they may name orgs never |
| 104 | + captured — themselves a to-capture list) and marks fuzzy matches in |
| 105 | + `pipeline_matched` for operator review. Human-in-drivers-seat: fuzzy |
| 106 | + rows get eyeballed in the sheet, not auto-trusted. |
| 107 | + |
| 108 | +**Output:** `clients/<client>/outputs/<date>_crm-starter/orgs.csv` (+ an |
| 109 | +`unmatched-pipeline-rows.csv` sidecar when any exist). |
| 110 | + |
| 111 | +## Phase 2 — `scripts/export-crm-people-csv.mjs` |
| 112 | + |
| 113 | +**One row per PERSON, not per affiliation edge** — the deliberate |
| 114 | +inversion of the ratings export's per-edge shape. CRM contact importers |
| 115 | +want one contact row; a person with two affiliations must not become two |
| 116 | +CRM contacts. The strongest edge (relevance-ranked, the same ordering |
| 117 | +`listOrgAffiliations` uses) supplies the org attach; the rest ride along |
| 118 | +in a spillover column. |
| 119 | + |
| 120 | +| Group | Columns | |
| 121 | +|---|---| |
| 122 | +| Identity | `external_id` (person_uuid), `name`, `first_name`, `surname`, `email`, `linkedin` (linkedin_profile_url ?? first linkedin link), `headline` | |
| 123 | +| Org attach | `org_external_id` (slug of the strongest affiliation), `org_name`, `role` (edge kind), `relevance`, `additional_orgs` (newline-joined `slug — role` for edges 2..n) | |
| 124 | +| Links | `other_links` (newline-joined non-LinkedIn personal_links) | |
| 125 | +| Provenance | `exported_at` | |
| 126 | + |
| 127 | +Persons with **zero** affiliations still export (org columns blank) — the |
| 128 | +CRM decides whether orphan contacts import; we don't silently drop 39 |
| 129 | +people (417 persons vs 378 edges). |
| 130 | + |
| 131 | +## Phase 3 — import order + verification |
| 132 | + |
| 133 | +1. Import `orgs.csv`. Map `external_id` to the CRM's external-id or a |
| 134 | + custom field — this is the one non-negotiable mapping. |
| 135 | +2. Import `people.csv`, attaching company by `org_external_id` (CRMs that |
| 136 | + only match companies by name fall back to `org_name` — which is why |
| 137 | + both columns exist). |
| 138 | +3. **Verify per the canonical-layer discipline, adapted:** counts match |
| 139 | + (rows exported == records created + skips explained), spot-check five |
| 140 | + orgs for link fidelity, spot-check three multi-affiliation people |
| 141 | + attached to the right org, and confirm a re-import of the same file |
| 142 | + updates rather than duplicates (the external-id round-trip proof). |
| 143 | + |
| 144 | +## Open decisions (settle before Phase 3; Phases 1–2 are CRM-agnostic) |
| 145 | + |
| 146 | +1. **Which CRM, and CSV-importer vs API batch?** The CSVs serve either |
| 147 | + path. If the target is a Twenty instance, the house already has the |
| 148 | + `twenty-interface` skill + MCP (`create_many_companies`, |
| 149 | + `upsert_many_people` with company attach) and an API batch beats the |
| 150 | + CSV importer; if it's something else, its importer's field-mapping |
| 151 | + screen consumes these files as-is. Naming the CRM also settles where |
| 152 | + `external_id` lands (native field vs custom). |
| 153 | +2. **Org scope confirmation** — recommendation above is all-364 + |
| 154 | + filter-in-sheet; alternative is pre-filtering to `funders/` bucket |
| 155 | + (+ pipeline matches) if the CRM should only ever see funders. |
| 156 | +3. **Person floor** — export all 417, or only persons with a rated |
| 157 | + edge (`relevance` set)? Recommendation: all, with `relevance` as a |
| 158 | + sheet-filterable column. |
| 159 | + |
| 160 | +## Out of scope |
| 161 | + |
| 162 | +- Any corpus content (invariant 2). |
| 163 | +- Ongoing sync (this is the STARTER export; a standing augment-it ↔ CRM |
| 164 | + sync is its own future spec — the external-id columns are what keep |
| 165 | + that door open). |
| 166 | +- Capturing pipeline orgs that were never minted as canonical orgs — the |
| 167 | + unmatched sidecar surfaces them; minting is triage/workbench work. |
| 168 | + |
| 169 | +## Close-out checklist |
| 170 | + |
| 171 | +- [ ] Phase 1 script + run: `orgs.csv` eyeballed by operator, fuzzy |
| 172 | + matches reviewed, unmatched sidecar triaged |
| 173 | +- [ ] Phase 2 script + run: `people.csv` eyeballed |
| 174 | +- [ ] Open decision 1 settled; Phase 3 import run + verification |
| 175 | +- [ ] Changelog entry; gh issues per the loop if run as a feature loop |
| 176 | + |
| 177 | +## Related |
| 178 | + |
| 179 | +- [[Org-Relations-Parent-Child-Peer-Plus-Org-Tags]] — tags + relations |
| 180 | + columns this export carries |
| 181 | +- `scripts/export-affiliation-ratings-csv.mjs` — the export-script |
| 182 | + conventions (and the per-edge shape Phase 2 deliberately inverts) |
| 183 | +- [[../specs/Workspaces-as-Tenant-Primitive]] — why every read here is |
| 184 | + client-scoped |
0 commit comments