A CRM is not one thing — it's a system of record plus half a dozen workflows that all read the same customers. The usual result is half a dozen products (or schemas, or clouds) with sync jobs between them, and a "single customer view" that is really six copies drifting apart. This suite is the opposite: the whole CRM on one DataK3 bucket. Accounts, contacts, leads, opportunities and activities are the shared spine; lead scoring, pipeline forecast, quote/CPQ, campaign attribution and account-360 are workflows that JOIN that spine directly — one copy of the rows, queried by content (SQL), by relationship (graph), and by meaning (vector).
The problem — and the money
The buyer here is the RevOps or sales-systems owner who has to stitch a CRM out of point tools — a
scoring app, a forecasting cube, a CPQ, an attribution product — and then keep them in sync. Each
workflow is worth a tutorial on its own, and has one. The point of the suite is that they compose with
zero glue: a scoring query reads the same leads the core owns; campaign attribution reads the same
opportunities the forecast weights; account-360 rolls up a corporate family over the same graph the
core seeded. No connector, no nightly export, no reconciliation. The payoff is a hard number the
scattered stack cannot produce without a warehouse job — $36,000 of open pipeline across the Acme
corporate family, rolled up over a graph three skills contributed to and one assembled — answered as a
single query over one copy of the rows.
Seven base skills, each independently live-validated, compose into the suite:
| # | Skill | What it owns | Reads (shared master) |
|---|---|---|---|
| 1 | crm/core | accounts, contacts, leads, opportunities, activities, 3× vectors, crm_node/crm_edge | — (the root) |
| 2 | crm/quote-cpq | products, price_books, quotes, quote_lines, … | accounts, opportunities |
| 3 | crm/lead-to-opportunity | organizations, flows, conversions, … | writes into core leads/opportunities |
| 4 | crm/qualification-scoring | lead_scores, scoring_policy | leads, opportunity_vectors |
| 5 | crm/pipeline-forecast | pipeline_stages, forecast_snapshots, deal_risk | opportunities |
| 6 | crm/campaign-to-lead | campaigns, attribution, touchpoints, … | opportunities, leads |
| 7 | crm/account-360 | relationships, account_summary, whitespace | accounts, products, the graph |
This page does not re-teach them — follow each link for the mechanics. Here we show how they
assemble into one bucket. The machine-readable manifest is the
suites/crm contract.
How it composes
Three rules make seven skills one CRM.
1. One bucket, 36 disjoint tables. Every skill's tables are globally unique names, so they coexist in
a single bucket with no collision — 34 SQL/vector tables plus crm_node and crm_edge. Installing the
suite is installing the seven skills, in order, into the same --bucket.
2. Masters are owned once, joined everywhere. crm/core owns accounts/contacts/leads/opportunities/
activities. No other skill re-declares them — they JOIN them. A converted org becomes a row in
core's leads; the scorer writes lead_scores keyed to that same lead_id; campaign attribution keys
to that same opportunity_id. One row, many readers:
In the crm bucket, show the two cross-skill joins that prove shared masters: qualification-scoring's lead_scores joined to core's leads, and campaign-to-lead's attribution joined to core's opportunities with a simple ROI.
data_sqllead_scores ⋈ leads: Jane Doe (acme.io) qualified at 82, Sam Ray (greyparrot.ai) disqualified at 34 — the scorer wrote against the same lead_id core owns. attribution ⋈ opportunities: the Q3 Data Webinar is credited $24,000 on opp-labs (linear model), a 4× ROI on $6k spend — attribution keyed straight to the opportunity core owns. No copy, no join table between products.
export BUCKET=crm
# qualification-scoring (lead_scores) ⋈ crm/core (leads) — same lead_id, two skills
dodil data sql -b "$BUCKET" "
SELECT l.org_domain, l.full_name, s.framework, s.score, s.verdict
FROM lead_scores s JOIN leads l ON l.lead_id = s.lead_id
ORDER BY s.score DESC"
# acme.io | Jane Doe | bant | 82 | qualified
# greyparrot.ai | Sam Ray | bant | 34 | disqualified
# campaign-to-lead (attribution) ⋈ crm/core (opportunities) — same opportunity_id
dodil data sql -b "$BUCKET" "
SELECT c.name AS campaign, o.name AS opportunity,
round(sum(a.weight*a.amount)) AS attributed_pipeline,
round(sum(a.weight*a.amount)/max(c.cost)) AS roi_ratio
FROM attribution a
JOIN opportunities o ON o.opportunity_id = a.opportunity_id
JOIN campaigns c ON c.campaign_id = a.campaign_id
GROUP BY c.name, o.name"
# Q3 Data Webinar | Acme Labs expansion | 24000 | 43. One graph, assembled once. This is the subtle one. crm_graph is created from a node table and an
edge table by a CREATE GRAPH that snapshots its edges at creation — edges added afterwards are
invisible. Three skills contribute edges (core's subsidiary_of/works_at, campaign-to-lead's
influenced, account-360's partner_of/supplies/…), so no skill can create the graph early. In suite
mode every contributor only INSERTs into crm_node/crm_edge; the single CREATE GRAPH is deferred
to account-360, the last contributor. Then the family rollup traverses it:
In crm, run the single deferred graph assembly last (after all skills have added their edges), then roll up open pipeline across the Acme corporate family — but only over subsidiary_of edges, since partner/supplier edges now share the same graph.
data_pgWith every contributor's edges in place — core's subsidiary_of/works_at plus account-360's partner_of — one CREATE GRAPH crm_graph snapshots them. The family rollup constrained to rel='subsidiary_of' returns $36,000 (Acme Labs $24k + Acme EU $12k). NOTE the composition subtlety: the unconstrained graph_khop('crm_graph',1,2,'in') that worked in the standalone core skill now over-counts to $66,000 — it walks ALL incoming edges, so Greyparrot's partner_of edge leaks into the family. In the assembled graph, type your traversal.
# account-360 runs the ONE create, after all edges are written (snapshot rule)
dodil data pg -b "$BUCKET" "CREATE GRAPH crm_graph
NODES (crm_node KEY id) EDGES (crm_edge SRC src DST dst)"
# family pipeline — constrained to subsidiary_of (the suite-correct rollup)
dodil data pg -b "$BUCKET" "
SELECT n.name AS account, count(o.opportunity_id) AS open_opps,
coalesce(sum(o.amount),0) AS family_pipeline
FROM crm_edge e
JOIN crm_node n ON n.id = e.src AND n.kind='account'
LEFT JOIN opportunities o ON o.account_domain = n.biz_key AND o.status='open'
WHERE e.dst = 1 AND e.rel = 'subsidiary_of'
GROUP BY n.name ORDER BY family_pipeline DESC"
# Acme Labs | 1 | 24000
# Acme EU | 1 | 12000 -> Acme family pipeline = $36,000 (Greyparrot's partner deal excluded)IMPORTANT
The composition subtlety, found live. A traversal that is correct for one skill can be wrong once
the suite shares its graph. Standalone, crm/core owns only subsidiary_of/works_at edges, so
graph_khop('crm_graph', 1, 2, 'in') returns exactly the family. In the suite, account-360 adds
partner_of edges into the same crm_edge table, and the unconstrained 'in' walk over-counts
($66k, not $36k). The fix is to type the traversal (rel = 'subsidiary_of'). Assembling into one
graph is the whole point — but it means every graph read must say which edges it means.
Scaffold it — the one-shot
With the DODIL MCP connected, one prompt scaffolds the full suite in order into one bucket:
Scaffold the full CRM suite in one bucket (suites/crm).
Bucket: crm. Seed the shared Acme demo dataset. Install the seven base skills IN ORDER into that
one bucket:
1. crm/core — 5 masters + 3 VECTOR(2048) tables + crm_node/crm_edge (populate node/edge,
but DEFER CREATE GRAPH — suite mode).
2. crm/quote-cpq — products first, so account-360 whitespace has a catalog.
3. crm/lead-to-opportunity — discovery warehouse; conversions write into core's leads/opportunities.
4. crm/qualification-scoring — lead_scores over core.leads (BANT).
5. crm/pipeline-forecast — stages/forecast over core.opportunities.
6. crm/campaign-to-lead — attribution over core.opportunities (linear).
7. crm/account-360 — relationships/whitespace, add the last graph edges, THEN run the single
CREATE GRAPH crm_graph.
Then prove the assembly: 36 tables coexist; lead_scores⋈leads and attribution⋈opportunities join;
the family rollup over subsidiary_of = $36,000. Ask me the union questions once (bucket, seed, industry
overlay, product pitch) — don't re-ask what composition already answers.Want an industry cut? Add overlay: saas | manufacturing | finserv | real-estate — an additive diff
on top of the same 36 tables (subscriptions, dealers, households, or listings + gate defaults), never a
fork. See the SaaS overlay for the pattern.
Building just part of it
You don't have to install all seven. To build a subset, install
crm/core first — it owns the five masters
(accounts/contacts/leads/opportunities/activities) every workflow reads — then add only the workflow
skills you want. Each workflow tutorial names its master dependencies (its skill contract's consumes
block is the machine-readable list): qualification-scoring reads
leads/contacts/activities/opportunities; pipeline-forecast
reads opportunities/accounts. Each workflow can also run fully standalone on an empty bucket — it
ships a stub-masters step that creates just the masters it reads — but once two workflows share the same
masters, install crm/core once instead of letting each stub its own. And skip the graph (no CREATE GRAPH) unless you install account-360, which owns the single deferred
graph assembly.
The questions, asked once
Composition removes the redundant asks. The suite interview is short:
- Asked once (shared): the
bucket,seed_data, the industryoverlay(drives all four overlay diffs at once), andproduct_pitch(shared by lead-to-opportunity and qualification-scoring — one answer, both gates). - Removed by composition: lead-to-opportunity never asks "hand-feed leads or discover?" — its leads
and opportunities come from core; account-360 never asks "seed a product catalog?" — products come
from quote-cpq; qualification-scoring never re-creates
opportunity_vectors— they come from core; campaign-to-lead never asks "project campaign nodes into the graph?" —crm_node/crm_edgeare core's, and the single deferredCREATE GRAPHcovers everyone. - Still per-workflow (genuine knobs): the qualify
framework(BANT/MEDDIC), theattribution_model, the forecaststages/probabilities, the CPQapproval_threshold_pct. Real best-practice decisions — the overlay defaults them, you tune them.
Verify
The full single-bucket assembly was live-validated on 2026-09-02 (org IHDIASH, one throwaway bucket, torn down after) — the composition itself, not a re-test of each skill:
# 1) 36 tables from all seven skills coexist in ONE bucket, NO name collision
dodil data table list -b crm | wc -l # 36 (34 SQL/vector + crm_node + crm_edge), all disjoint
# 2) shared master, two skills: qualification-scoring's lead_scores ⋈ core's leads
dodil data sql -b crm "SELECT verdict, count(*) FROM lead_scores GROUP BY verdict"
# qualified 1 (Jane, 82) · disqualified 1 (Sam, 34)
# 3) shared master, two skills: campaign-to-lead's attribution ⋈ core's opportunities
dodil data sql -b crm "SELECT round(sum(weight*amount)) FROM attribution WHERE opportunity_id='opp-labs'"
# 24000 (Q3 Webinar → opp-labs, 4× ROI on $6k)
# 4) ONE graph, assembled last; family rollup constrained to subsidiary_of
dodil data pg -b crm "SELECT coalesce(sum(o.amount),0) FROM crm_edge e
JOIN crm_node n ON n.id=e.src AND n.kind='account'
LEFT JOIN opportunities o ON o.account_domain=n.biz_key AND o.status='open'
WHERE e.dst=1 AND e.rel='subsidiary_of'"
# 36000 (naive graph_khop('in') over-counts to 66000 once partner_of shares the graph — type your traversal)The 36-table coexistence, both cross-skill JOINs, the single CREATE GRAPH, and the $36,000 family
rollup are all proven live. The Ignite engines each skill deploys are validated in their own tutorials;
the suite test is the data-plane composition — that seven skills share one bucket, one set of
masters, and one graph without collision.
Connect your tools
Everything the seven skills wrote lives in the one DataK3 bucket, reachable by your own stack — psql and
pgvector drivers over the Postgres wire, a Neo4j driver over Bolt, gRPC for the table engine. data connect crm prints the endpoints; point BI, a dashboard, or your app straight at the live rows. Full,
live-validated walkthrough: Connect your tools.
Composes
This page is a composition, not a fork:
- Seven base skills —
crm/core,crm/quote-cpq,crm/lead-to-opportunity,crm/qualification-scoring,crm/pipeline-forecast,crm/campaign-to-lead,crm/account-360— each its own tutorial and its own## Test. - The suite manifest (
suites/crm) — the ordered install DAG, the shared-master wiring, the single deferred graph rule, and the union Q&A above. - Overlays (
crm/overlays/*) — additive industry diffs that apply on top of the assembled suite.
Read the seven to learn each workflow; read this to assemble them into one CRM on one bucket.