Specification · crm @ 0.2

crm — Relationship Pursuit · Area Specification

last conformance run: 4/4 scenarios pass · 18 acts mapped 2026-08-23 00:39 UTC

SpecActsInvariantsScenarios

Spec

Version0.2-draft
Statuscurated draft — implementation not started
CuratorPeter Varga (single editor; spec PRs separate from implementation PRs)
Calibrationa curated target list worked by a small team, not a mass-market funnel
Provenanceextracted from a production system (the JC360 US market-entry CRM); what stayed behind and why is in §9

This document is the contract all implementations of the crm area share: business acts, invariants, lifecycles, and required read models — never screens or schemas. Anything two reasonable implementations might do differently is marked freedom. Scenario files under scenarios/ are the executable part.

---

1. Calibration: who this is for

An operator or small team pursuing tens of accounts they can name, where each account was researched into the list and relationships decide outcomes: market entries, agency work, enterprise sales with a defined territory, partner development. The unit of value is not volume — it is what you verifiably know about a small number of companies and people.

The reference points:

2. Scope

In scope (contract): accounts (curated, narrative-bearing) → contacts (named-with-source or gap) → activities → pipeline → promotion to a trading customer.

Deferred (§9): introductions-with-evidence, blocker/delay logs, ingestion (email/calendar), scoring, dedup/merge, multi-pipeline.

Rejected — permanent non-goals:

3. Entities and lifecycles

EntityLifecycle
campaignactive → paused (re-enterable) → concluded (terminal, reasoned). A campaign is a goal: the thesis every account's why_them argues against, and the standing brief an agent reads before filling the list
accounta pursuit within one campaign: not_started → researching → approaching → active → won — or parked on_hold (re-enterable), or terminal closed (pursued and lost/passed) / excluded (deliberately never pursued). Account names are unique per campaign
contactgap → named (via resolve_gap only) — a named contact may become departed; never deleted
activityimmutable once recorded (a fact, not a workflow object)
stage referencedata, not code: each pipeline stage carries a label and a probability (freedom to define the set)

won is the handoff moment: a won account may be promoted exactly once, creating (or linking) a core customer through the owning module's API. Pursuit ends where trading begins.

4. The acts

Acts are named abstractly; an implementation exposes each as a command under its own prefix and publishes an act → command map. Required arguments are contract. Refusals name what is missing, in one sentence, on every surface.

4.0 Campaigns

create_campaign (name, goal, target_profile?) The goal is required (CRM-13): a campaign without a stated thesis is a folder, not a pursuit. The goal is the brief — a research session reads it before adding accounts, and every why_them argues against it. Freedom: extra planning fields.

update_campaign (campaign, patch) — the goal can be sharpened, never blanked.

set_campaign_status (campaign, status, reason?) paused and concluded require a reason. Concluded is terminal: its accounts remain fully readable (CRM-6), but it takes no new targets.

4.1 Accounts

add_account (campaign, name, why_them, source_url, tier?, vertical?, trigger_event?, hook?) The gate to the list. campaign is required (CRM-12) and why_them must argue that campaign's goal; source_url grounds it — an account that cannot say why it belongs to this pursuit, with a source, is not a target yet (CRM-3). Refused into concluded campaigns. Freedom: tier semantics, vertical taxonomy, additional research fields.

update_account (account, patch) — corrects or deepens the narrative. Provenance fields may be corrected, never blanked (CRM-9).

set_account_status (account, status, reason?) Lifecycle moves, guarded per §3. on_hold, closed, and excluded require a reason — parking or killing a researched account is a decision someone later asks about (CRM-7).

set_path_in (account, bullets[]) — the ordered "how we get in" plan, replaced whole.

4.2 Contacts

add_contact (account, role_type, — then one of two shapes)

resolve_gap (contact, name, source, title?, confidence_note?) The only way a gap becomes named — and it demands the same provenance as a fresh named contact. Refused without a source.

update_contact (contact, patch) Corrections, departed marking, notes. The relationship-graph fields — mutual_via, mutual_url, linkedin_path — are human-only (CRM-4): an agent's write touching them is refused regardless of the member's role, with a sentence saying why.

4.3 Activity

log_activity (account, summary, occurred_at, contact?, direction?, medium?) occurred_at is when it happened, which is not when it was typed (CRM-5). Interpretation and summary are free prose; facts inside them follow CRM-10.

4.4 The bridge

promote_to_customer (account, terms?, credit_limit?) Allowed only for won accounts, at most once (CRM-8). Creates the core customer through core's exported API — a logged act marking where this area's job ends and o2c's begins — and links it on the account. Freedom: carrying contacts across.

Act count

9 write acts + 5 read models — comfortably inside the 25-tool budget, leaving room for extensions.

5. Invariants

Namespaced CRM-n (areas own their invariant namespaces; o2c's unprefixed INV-n is legacy).

  1. CRM-1 Never invent a person. A named contact requires a source and carries a confidence note. Facts — names, titles, dates, URLs — come only from sources. Empty beats guessed; guessing is the only real failure here.
  2. CRM-2 A gap is a finding. A role with no publicly named holder is recorded as a gap row with a gap note, appears in the gap worklist, and becomes named only through resolve_gap with full provenance.
  3. CRM-3 Every account earned its place. No account without why_them and a source_url. This is a curated list, not a funnel.
  4. CRM-4 Some fields are human-only. Relationship-graph fields are written by people, never by agents — whatever the member's role — because automating them risks the human relationships (and accounts, in both senses) this work depends on. The refusal says so in one sentence.
  5. CRM-5 Activities record when it happened, not when it was typed.
  6. CRM-6 Nothing is deleted. Exclusion and closure are reasoned statuses; departed contacts and their history remain.
  7. CRM-7 Parking or killing requires a reason. on_hold, closed, excluded carry prose that will be read back.
  8. CRM-8 Promotion is a bridge act. won → core customer, through the owner's API, logged, at most once per account.
  9. CRM-9 Provenance survives updates. Sources and confidence may be corrected, never removed.
  10. CRM-10 Interpretation is free, facts are sourced. Narrative fields may summarize and judge; factual claims inside them trace to sources or are marked as unknown.
  11. CRM-12 Every account pursues a campaign. Pursuit state — why_them, tier, status, path-in — is campaign-relative; the same company may be a target of two campaigns as two accounts. (The org/pursuit normalization this implies is deferred: §9.)
  12. CRM-13 No campaign without a goal. The goal is data, not a chat prompt: it is the brief an agent reads before filling the list, and the thesis every why_them argues.
  13. CRM-11 Platform inheritance. Every write is a logged command with an actor; refusals (including CRM-4 denials) are logged; reads are never logged.

6. Required read models

Read modelMust answer
accountfull narrative (why/trigger/hook/sources), path-in bullets, contacts including gaps, recent activity, available next actions with refusal reasons
contactidentity, provenance (source + confidence), gap state, activity touching them
pipelineaccounts by status and tier with stage probabilities — the weighted "where are we" view
gapsevery unresolved gap: role, account, gap note, age — the "what we verifiably don't know" worklist, first-class
coveragelist health: accounts by status/tier, gap counts, accounts with no activity in N days — staleness is visible, not discovered
campaignsevery campaign with its goal, status, and health (accounts by status, open gaps, staleness) — the per-goal Today

pipeline, gaps, coverage and the account list must answer per-campaign and across campaigns (an optional campaign filter is contract; its shape is freedom).

7. Contract vs freedom — summary

ContractFreedom
the 9 acts, required args, refusal semanticsextension commands, extra optional args
lifecycles in §3, all 11 invariantsschema, id formats, tier/vertical taxonomies
read model content (§6)shapes, pagination, extra views
provenance mandatory; gaps first-classwhich fields beyond the graph set are human-only
stage probabilities from reference datathe stage set itself
promotion via core's API, oncecarrying contacts/notes across on promotion
buyer/persona doctrine— it lives in workspace content (seeded guidance, account notes), not module code

8. Conformance

As for o2c: an act → command map covering §4, every scenario under scenarios/ passing (steps name acts; refusals are contract), and the mechanically checkable invariants asserted by the platform gates. One note specific to this area: the conformance runner executes as an agent, so CRM-4 scenarios assert the refusal side; the human-path acceptance is asserted by a gate test, not a scenario.

9. Deferred — with reasons (the extraction record)

This spec was extracted from a production single-tenant CRM built for a specific agency agreement. These stayed behind:

ItemWhy deferred, not lost
Introductions with mandatory evidenceThe source system's introduction table made commission conditional on documented evidence — NOT NULL constraints that were literally sentences of §V of an agency agreement. Generalizable someday as "milestone with mandatory evidence," but shipping contract law as a default CRM concept helps nobody.
Blocker / delay logEncoded §3 of the same agreement (principal-attributable delays extending the term). A generic "blocked, on whom, since when" may return; the contractual accrual math stays bespoke.
Agreement fields (agreed_with_principal_at, tier-1-only hooks)Deployment policy, not area semantics — in Saybooks terms, workspace content.
Org / pursuit normalizationThe fully normalized model splits company (org facts, people, the human-entered network) from pursuit (campaign membership), so one company in two campaigns shares its contacts. Deferred until a company actually appears in two campaigns: today the cost of overlap is a duplicated account row with duplicated contacts — annoying, visible, fixable then. This line exists so the fault line is on record as seen, not missed.
Email/calendar ingestionIntegration territory; the area records, it does not watch.
Lead scoring, dedup/merge, multi-pipelineFunnel-CRM machinery; against this area's calibration until real demand says otherwise.

---

Change log: 0.2-draft amended (2026-08-24) — contacts gain optional email and phone on add_contact / resolve_gap / update_contact: in many industries reach details are plainly listed, and recording them is a sourced fact like a LinkedIn URL (CRM-1 covers the where-from; nothing changes about CRM-4 — the relationship-graph fields stay human-only). · 0.2-draft (2026-08-23) — campaigns become first-class (CRM-12, CRM-13): the 0.1 extraction had erased the campaign by promoting it to "the whole deployment"; a second research fill made it visible again. Acts +3, read models +1, org/pursuit split explicitly deferred. · 0.1-draft (2026-08-22) — initial curation, extracted from the JC360 US CRM. New platform mechanism motivated by this area: field-level human-only enforcement (CRM-4).

Acts

ActKindRequired
add_accountwritecampaign name why_them source_url
update_accountwriteaccount
set_account_statuswriteaccount status
set_path_inwriteaccount bullets
add_contactwriteaccount role_type
resolve_gapwritecontact name source
update_contactwritecontact
log_activitywriteaccount summary occurred_at
promote_to_customerwriteaccount
accountreadaccount
contactreadcontact
pipelineread
gapsread
coverageread
create_campaignwritename goal
update_campaignwritecampaign
set_campaign_statuswritecampaign status
campaignsread

Invariants

IdInvariant
CRM-1Never invent a person: named contacts require a source; facts come only from sources.
CRM-2A gap is a finding: recorded with a note, listed in the worklist, resolved only with provenance.
CRM-3Every account earned its place: why_them and source_url are mandatory.
CRM-4Relationship-graph fields are human-only: refused to agents regardless of role.
CRM-5Activities record when it happened, not when it was typed.
CRM-6Nothing is deleted: exclusion and closure are reasoned statuses.
CRM-7Parking or killing an account requires a reason.
CRM-8Promotion is a bridge act: won → core customer via the owner's API, at most once.
CRM-9Provenance survives updates: correctable, never removable.
CRM-10Interpretation is free; facts are sourced.
CRM-11Every write is a logged command with an actor; refusals are logged; reads are not.
CRM-12Every account pursues a campaign; pursuit state is campaign-relative.
CRM-13No campaign without a goal — the goal is the brief agents read before filling the list.

Scenarios

Each scenario is a file of acts with expected outcomes. ok means the act must succeed with the listed fields; refused means the act must be refused with a sentence containing the listed text. Refusals are contract.

no invented people: provenance or gap, nothing in between pass

CRM-1 (never invent a person), CRM-2 (a gap is a finding), CRM-3 (every account earned its place). The area's character in one flow: what you know arrives with sources; what you don't know is recorded as exactly that.
#ActExpectWhy
1create_campaignok
2add_accountrefused why_themCRM-3: an account that cannot say why it belongs, with a source, is not a target yet
3add_accountok status="not_started"
4add_contactrefused sourceCRM-1: a name without a source is not usable in a first approach — refused, not warned
5add_contactok status="named"
6add_contactok status="gap"CRM-2: the absence of a name is a finding worth recording, not a blank to fill later
7gapsok length=1the gap worklist is first-class: 'what we verifiably don't know' is a view, not a feeling
8resolve_gaprefused sourceCRM-2: resolving a gap demands the same provenance as a fresh contact — the gap does not lower the bar
9resolve_gapok status="named"
10gapsok length=0

the fields no agent may touch pass

CRM-4: relationship-graph fields (mutual_via, mutual_url, linkedin_path) are written by people only, whatever the role. The conformance runner executes as an agent, so this scenario proves the refusal side; human acceptance is a gate test. CRM-11: the denial is logged.
#ActExpectWhy
1create_campaignok
2add_accountok
3add_contactok
4update_contactokordinary fields are fine — interpretation is the agent's job (CRM-10)
5update_contactrefused humanCRM-4: whoever's delegate the agent is, the relationship graph is entered by a person — the refusal explains why, in one sentence
6update_contactrefused human

pursuit ends where trading begins pass

CRM-6/7 (reasoned statuses, nothing deleted), CRM-5 (occurred_at), CRM-8 (promotion: won only, once, via core's API). The bridge between areas, as a logged act.
#ActExpectWhy
1create_campaignok
2add_accountok
3set_account_statusok
4log_activityokCRM-5: it happened on the 10th; today is when it got typed — the record keeps the 10th
5set_account_statusrefused reasonCRM-7: killing a researched account without saying why is refused — the list's value is that every change to it can be read back
6promote_to_customerrefused wonCRM-8: promotion is for won accounts — pursuit and trade have a boundary, and this is it
7set_account_statusok
8set_account_statusok
9set_account_statusok
10promote_to_customerokcreates the core customer through core's exported API — one logged act marking where crm's job ends and o2c's begins
11accountok status="won"
12promote_to_customerrefused Already promotedCRM-8: at most once — the link exists; a second customer would be a reconciliation problem wearing a convenience

a campaign is a goal, not a folder pass

CRM-12 (every account pursues a campaign; pursuit state is campaign-relative), CRM-13 (no campaign without a goal), CRM-7 extended (pausing/concluding reasoned), CRM-6 (concluded keeps everything).
#ActExpectWhy
1create_campaignrefused goalCRM-13: a campaign without a stated thesis is a folder. The goal is the brief an agent reads before adding anything
2create_campaignok status="active"
3create_campaignok
4add_accountok campaign_id="CAM-0001"
5add_accountokCRM-12: the same company under two campaigns is two pursuits with two whys — pursuit state is campaign-relative
6add_accountrefused already onwithin one campaign, one row per company — a twin is a data bug, not a second chance
7pipelineok length=1
8pipelineok length=2read models answer per-campaign AND across — both are contract
9set_campaign_statusrefused reasonCRM-7 extended: a goal someone stops pursuing is a decision that gets asked about later
10set_campaign_statusok status="concluded"
11add_accountrefused concludedconcluded takes no new targets — but everything it holds stays readable (CRM-6)
12accountok campaign_id="CAM-0002"