Chapter 31 / 40
CRM
Status: Planned
Sodimo adopts Twenty v2.0.0 as a pinned podman quadlet on the Framework Desktop. Sodimo does not build a CRM; what Sodimo builds is the MCP interaction layer on sodimo-core — six crm_* tools that call Twenty’s REST/GraphQL API and write a run_ledger row on every mutation. The CRM itself is upstream-unchanged, upgraded on Sodimo’s cadence.
Twenty v2.0.0 ships a rich, documented REST + GraphQL API with webhook support. There is no direct-Postgres shortcut and no turbular adapter; that route was evaluated and dropped (D-154). The API is the interface.
For the quadlet spec (image tag, env, routing at crm.sodimo.eu, secrets, backup), see Quadlet reference, §35f.
Users and usage
- Rani (sales) drives two kanban pipelines day-one. Contact list, deal detail, activity timeline, IMAP inbox linked to deals.
- Paul (ops) reads AR-aging digests, triages recouvrement queues, owns custom-field correctness.
- Michel (admin) owns roles and SSO.
- Ralph (factu) is a workflow recipient — notified by Twenty when a deal wins on the Export Clients pipeline.
- The AI is a service principal. It reads and writes through the six MCP tools below; every mutation is attributed to a
run_idin the D1 run ledger.
Two pipelines on day one (confirmed 2026-04-22):
- New Leads - Wine — 8 stages, acquisition funnel: Lead Received → Qualification → Product & Price Presentation → Follow-up / Feedback → Negotiation → Order Confirmation → Delivery & Onboarding → Post-Sale Follow-Up.
- Export Clients — 5 stages, closing motion, maps 1-to-1 to Sodiwin devis → commande → facture: Order received → Proforma Sent → Proforma confirmed → Payment → Waiting for delivery.
Per-stage rotting thresholds (days until a card turns yellow then red): Order received 1, Proforma Sent 3, Proforma confirmed 1, Payment 3, Waiting for delivery 5. Enforced by a scheduled Twenty Workflow that sets a health field on each Deal.
Objects and custom fields
Twenty’s standard objects (Person, Company, Opportunity, Activity) cover ~90% of Sodimo’s needs unchanged. Custom fields below — compact table. Fields with origin Sodiwin are overwritten nightly by the ETL sync; fields with origin Twenty are authoritative in Twenty and never touched by the sync.
| Object | Field | Type | Origin | Purpose |
|---|---|---|---|---|
| Company | client_code | string | Sodiwin | Join key to Sodiwin CSV dump (primary) |
| Company | category | string | Sodiwin | Sodiwin account category |
| Company | type | string | Sodiwin | Sodiwin account type |
| Company | delivery_address | string | Twenty | Distinct from billing address |
| Company | accise_1, accise_2 | string | Twenty | Alcohol-excise duty regime |
| Company | ar_balance, ar_aging_bucket, ar_last_payment_at | numeric / enum / date | Sodiwin | AR state, read-only from Twenty |
| Company | brand_exclusivity[], brand_contract_expiry | array / date | Sodiwin | Which of the 21 brands and until when |
| Company | halal_cert, halal_cert_expiry | bool / date | Twenty | Cert flag and renewal date |
| Company | last_order_* | several | Sodiwin | Date, amount, top SKUs — denormalized |
| Opportunity | client_code | string | Sodiwin | Denormalized for join-free lookups |
| Opportunity | sodiwin_numero_commande | string | Sodiwin | Set once a commande is issued |
| Opportunity | health | enum(green/yellow/red) | workflow | Set by the rotting workflow |
| DeltaAlert (custom object) | kind, threshold, detected_at, company_id | — | Sodiwin | One row per alert; definition pending (D-126) |
Every AI-driven write is additionally attributed to a run_id in the D1 run ledger — see Run-ledger attribution below.
MCP surface
Six tools on sodimo-core. Each calls Twenty’s REST API (GraphQL where it’s materially cleaner — crm_search). Every write emits a run_ledger row before the Twenty call. Full signatures live in What the AI can access.
| Tool | Twenty call | Callers | Ledger |
|---|---|---|---|
crm_list_deals | GET /rest/opportunities?filter=… | AI, Rani, Paul | read-only, no ledger |
crm_get_contact | GET /rest/people/{id} + linked Company | AI, Rani | read-only |
crm_upsert_contact | POST /rest/people or PATCH /rest/people/{id} | AI | write: tool=crm_upsert_contact |
crm_advance_stage | PATCH /rest/opportunities/{id} (stage field) | AI | write: tool=crm_advance_stage |
crm_add_activity | POST /rest/activities + link target | AI | write: tool=crm_add_activity |
crm_search | POST /graphql (structured filter) | AI | read-only |
The tools are a narrow Sodimo-shaped surface, not a passthrough. They compose multi-step Twenty calls (e.g. crm_upsert_contact may create a Company then a Person and link them in one tool call) and normalize the field shape so the AI sees client_code rather than Twenty’s schema-generated field name.
Why the API, not Postgres: Twenty’s API is versioned, rate-limited, and covers webhooks; the schema is a private implementation detail. Direct-Postgres would couple Sodimo to every Twenty migration. Webhooks on Deal stage changes flow back into the Worker the same way they would flow into an n8n box — Twenty is the source of truth, the Worker is the observer + augmentor.
Routing and access
crm.sodimo.eu → Cloudflare Tunnel → Caddy → twenty:3000. Cloudflare Access (Google Workspace IdP) gates every route at the edge; inside Twenty, Google SSO (AUTH_GOOGLE_ENABLED=true) handles user identity so the Twenty audit log attributes edits to the correct Workspace account. No public port; no password form.
Tom’s break-glass path is Tailscale + podman exec on the Framework Desktop. Employees never see Tailscale.
Full quadlet spec (image, env, volumes, backup) is Quadlet reference, §35f.
Workflows, recouvrement, delta alerts
Workflows. Twenty’s native Workflow engine (triggers + actions + HTTP Request + code) covers the post-event automation Sodimo needs. The canonical flow is Deal won on Export Clients → email Ralph + create “Émettre facture” task + HTTP Request to the Worker’s ledger_write endpoint for attribution. The per-stage rotting flow and the “always a next activity” flow are scheduled Workflows with the same shape.
Recouvrement and delta-alerts split. Workflow-native handling (scheduled search + send email) stays inside Twenty for tier-1 recouvrement and simple delta surfaces. Claude-drafted text (tier-2 recouvrement letters) and cross-system augmentation run on the Worker and push results back via crm_add_activity or crm_upsert_contact. The split rule is documented in Open decisions (D-155) and implemented in sodimo/mcp.
Sodiwin sync
The nightly ETL (chapter ETL) lands Sodiwin rows in D1. A post-ETL job reads D1 and calls crm_upsert_contact with merge_policy="sodimo-fields-only" — Sodiwin-owned fields (AR, exclusivity, last-order) overwrite; Twenty-owned fields (stage, rep, notes, delivery_address, halal flags) are untouched. Direction is strictly D1 → Twenty for the sodimo-fields set.
Webhook in the other direction: Twenty fires a webhook on Deal stage change; the Worker receives it, writes a run_ledger row, and (for Export Clients stages) triggers the Sodiwin push (API-first per D-156, FTP+CSV fallback).
Run-ledger attribution
Twenty’s audit log records every row-level change with actor + timestamp + before/after. For AI writes, Principle 2 requires a second record: the originating run_id. Every crm_* write tool emits a run_ledger row on the Worker side before it calls Twenty’s API. Query surface: “every CRM write the AI made last week, correlated to the skill” is one SQL against D1; “every edit to deal X, human or AI” is one query against Twenty. The two join on run_id for AI writes.
Where it lives
- Quadlet spec: Quadlet reference, §35f — image, env, volumes, backup.
- MCP tool source:
sodimo/mcp,src/tools/crm/. Tool signatures in chapter What the AI can access. - Sync job:
sodimo/etl, nightlyd1-to-twentyjob, runs ~03:30 CEST after the Sodiwin dump and D1 upsert. - Operational:
podman logs twenty.service,podman exec -it twenty-db psql, Twenty’s own Settings for roles, custom fields, workflows.
Post-Rani-call outcomes (2026-04-22)
Resolved: two-pipeline shape (8 + 5 stages, rotting thresholds), Pipedrive-native custom fields on Company, IMAP/SMTP against Sodimo Dovecot/Postfix enabled at deploy, Pipedrive migration is light one-shot CSV import (Rani re-authors templates in Twenty), Sodiwin push is API-first with FTP+CSV fallback per the 2020 Supli precedent.
Still open: delta-alert kinds and thresholds (D-126), tiered recouvrement split between Twenty-Workflow and Worker-Claude (D-155), per-field origin policy for ambiguous fields like contact email/phone, lead-dedup policy across Rani-phone / SIRENE / Outscraper / Sodiwin, Christian Semat outreach to confirm whether Sodiwin exposes a REST/GraphQL API.