Skip to content

Commit cce7a68

Browse files
mpstatonclaude
andcommitted
plan(crm-starter-export): orgs then people — the pipeline shape enriched from the canonical layer
Draft plan for seeding the new CRM from what augment-it already knows: two batch-import CSVs. Phase 1: one row per org (364 for reach-edu) carrying the Master Pipeline Tracker v10 columns joined exact-first via corpus_funder_slug, plus flattened identity links, pulse streams, tags/bucket, and relations — corpora deliberately excluded. Phase 2: one row per PERSON (inverting the ratings export's per-edge shape — CRM contacts must not duplicate), attached by org slug with relevance-ranked primary edge. External IDs (slug / person_uuid) ride every row so the attach is exact, re-imports are idempotent, and future sync stays possible. Three open decisions gate Phase 3 (which CRM + import path; org scope; person floor) — Phases 1-2 are CRM-agnostic either way. Files changed: - context-v/plans/CRM-Starter-Export-Orgs-Then-People.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RW28dw3kQAKXr2ZNefCukE
1 parent a0a9ff7 commit cce7a68

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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

Comments
 (0)