Appearance
Are you an LLM? You can read better optimized documentation at /ideas/canonical-crm-overlay.md for this page in Markdown format
Canonical CRM + Declarative Overlay
An architecture idea that emerged from Avoca FDE work. Cross-engagement vision; not a commit-to-build.
Status
Exploration. The Avoca-specific mock-API work (see Voice Agent Testing Primitive and mock-autoops-api) is the cheap version of the same bet: build the test environment against one canonical shape and see how much friction the per-CRM disparities actually introduce.
2026-07-03 update: the Avoca-grounded feasibility case is now written up at Internal CRM abstraction, backed by a full integration survey (19 CRM enum values; every deep integration converges on the same 7-op core). It proposes the minimum viable slice of this idea: canonical interface + adapters + a stateful reference implementation, motivated by making the post-call workflow blackbox-testable.
The headline
One canonical CRM model internally. Each real CRM is a declarative overlay that extends the base with its own capabilities, constraints, and field mappings. In-call logic targets the canonical shape; the runtime resolves which capabilities are available for the active CRM. Going live = enabling the translator that maps canonical writes into the real CRM.
Why this is interesting
Voice-agent platforms (and any "multi-CRM" product) hit the same problem: every CRM has roughly the same data model (customer, vehicle, job, appointment, availability) but wildly different capabilities and naming. Today the disparity leaks into application code as per-CRM branches, scattered guards, and silent failure modes.
Existing unified-API products (Merge.dev, Nango, Apideck) solve part of this on the read side. They normalize shapes. They mostly do not solve declarative capability disparity, where one CRM permits an action and another does not.
The differentiator here is the per-CRM overlay being declarative and runtime-readable, not a black-box translator. Capability gaps become first-class, enumerable, testable.
Sketch
canonical/customer.yaml # shape definition
canonical/appointment.yaml
canonical/availability.yaml
overlays/autoops.yaml # extends canonical, declares constraints
overlays/servicetitan.yaml
overlays/housecallpro.yaml1
2
3
4
5
6
7
2
3
4
5
6
7
Example overlay fragment for the AutoOps SMS-cancel gap:
yaml
# overlays/autoops.yaml
cancellation:
api_allowed: false
when: customer.source == "SMS"
fallback:
type: email_shop
template: cancel_request1
2
3
4
5
6
7
2
3
4
5
6
7
In-call code calls crm.cancel(job) against the canonical layer. The runtime reads the active CRM's overlay, sees the SMS-source guard, and returns a structured CannotCancel(reason="sms_source_policy", fallback=email_shop) rather than letting the call attempt fail downstream.
Architecture pieces
| Piece | What it does | Build complexity |
|---|---|---|
| Canonical schema | Shape definitions for the universal CRM objects | Moderate. Get this wrong and everything downstream is wrong. |
| Overlay DSL | Declarative per-CRM extensions: field mappings, capability guards, fallbacks | High. DSL design is hard; too narrow loses expressiveness, too broad becomes a programming language. |
| Runtime resolver | Given a canonical operation + active CRM, returns either a translated request or a structured "cannot, here's why, here's the fallback" | Moderate. Most of the cleverness is in the overlay DSL. |
| Translator | Outbound mapping from canonical writes to real CRM API calls | Per-CRM, but constrained by the overlay so quirks are explicit not scattered. |
| Custom-handler escape hatch | custom_handler: <fn> for the long tail the DSL cannot express | Essential. Every successful "declarative" system has one. |
Prior art
| Where | Shape |
|---|---|
| Terraform providers | Canonical resource model (HCL); providers extend with per-cloud capabilities |
| SQLAlchemy dialects | Canonical query model; per-dialect compilation rules |
| Apollo Federation | Canonical schema; subgraph extensions |
| Stripe | Per-country payment_method_types; runtime constraints on a canonical PaymentIntent |
The pattern is well-established outside the CRM space. Applying it to CRMs is the novel piece.
What's hard
| Hard part | Why |
|---|---|
| Capability vs. shape | Two CRMs can have the same Appointment shape and very different cancellation semantics. The overlay has to express behavior, not just field renames. |
| DSL design | Cover 80% of the cases declaratively, expose a clean escape hatch for the 20%. Most teams overshoot and ship a turing-complete config language. |
| Round-trip fidelity | Canonical → CRM-A → canonical: does state survive? If the CRM stores something the canonical model does not represent, you lose it on round-trip. |
| Drift | The overlay claims AutoOps does X. Six months later AutoOps changes. How is the overlay kept honest? Probably: nightly probes against the live API, diffed against the overlay's claims. |
| Versioning | CRM APIs version. Overlay has to express "this rule applies to AutoOps v2 but not v3." |
5x filter
Honest assessment: this clears the 5x bar only if it generalizes past Avoca. Internal to one product the value is real but bounded; the in-call code gets cleaner, the test surface gets sane, the per-CRM disparities stop being a footgun. That alone is a 2-3x quality improvement, not 5x.
The 5x case is: anyone building a multi-CRM product (CRM migration tools, voice-agent platforms beyond Avoca, dispatcher/copilot products, integration testers) needs this. The market is the same one Merge.dev / Nango serve, but the declarative-capability differentiator is real and underserved.
Worth pursuing as a research / sketching exercise alongside the mock-API work, which is already the cheap version of validating whether the canonical shape holds.
Validation path (cheap)
- Mock-API work already in flight (see
mock-autoops-api) builds the test environment against a canonical-shape mock. This is the substrate. - Sketch the AutoOps overlay as a side artifact while building the mock. Enumerate AutoOps's quirks (SMS cancels, far-future-date conventions, etc.) in a structured form.
- Sketch a second CRM's overlay (ServiceTitan, the next-most-relevant Avoca CRM) without writing the runtime. See if the DSL holds.
- Decide: does the overlay shape generalize? If yes, scope a real plan. If no, the mock-API value still stands.
The validation costs are mostly absorbed by work that is already happening.
Open questions
Should the canonical layer be a durable workflow intermediary?
Raised 2026-05-21 by Sandy. The idea above frames the canonical layer as an abstraction for in-call logic. The open question reframes it as a durable workflow intermediary: in-call writes land in Avoca's canonical store first, then a sync workflow propagates them to the external CRM. The external CRM remains the system of record. The canonical store is a staging surface for the sync workflow, not a long-term record.
This is the outbox / write-ahead-log pattern, not "Avoca becomes the CRM."
Why the reframe is interesting:
- Workflow durability: if the external CRM API hiccups mid-call (or after), the canonical write still landed. The sync step retries from a single durable record instead of "the booking is wherever the failure happened."
- Replay surface: every pending sync is a structured row with explicit retry semantics. Beats logs-as-truth.
- Decoupling from CRM SLAs at call time: the call's success no longer depends on the external CRM being up at the moment of the write.
- Bounded storage: once a record is confirmed synced, the canonical row can be archived or pruned. Avoca isn't growing a duplicate customer database; it's a transient staging table for the sync workflow.
- Already mostly there: Avoca runs Inngest for post-call workflows. The canonical record is essentially a structured workflow checkpoint with a typed payload, not a new system.
What's hard:
- Sync lag visible to dispatchers. A dispatcher checking the external CRM right after the call ends needs to see the booking. Async-with-retry breaks that expectation unless the happy-path sync is sub-second.
- Idempotency. Retrying a sync after a partial external-CRM failure must not double-book. Per-canonical-row idempotency keys passed to the external CRM API.
- Failure handoff. If sync ultimately fails (after retries), what happens? Surfacing the failure to someone (Avoca eng? the shop?) and what they're expected to do.
- Scope drift. Easy to let the "transient staging table" grow into "Avoca's mirror of the CRM." Resisting that drift is a discipline decision, not an architectural one.
What this is not: Avoca as system of record. The canonical store's job is to land safely in the external CRM, then step aside. Also not the "convince shops to switch to Avoca's CRM" play, which Sandy deliberately separated 2026-05-21 as a different conversation.
The two architectures stack cleanly: the declarative overlay gives you the abstraction (in-call logic targets canonical shape); the durable workflow intermediary gives you the reliability (writes survive external-CRM hiccups). The overlay's structured CannotCancel(reason=..., fallback=...) response composes naturally with the workflow surface — a "cannot sync, here's why" is just another row state.
Origin
Surfaced 2026-05-21 during the mock-AutoOps test-environment work. The "single mock API, route to all CRMs" thesis appeared first, then refined to "canonical base + declarative overlay per CRM" once the capability-disparity problem (concretely: AutoOps SMS cancels) made clear that translation alone is not enough.
Related
- Voice Agent Testing Primitive — the testing substrate this idea sits on top of
mock-autoops-api— Avoca-specific mock work that validates the canonical-shape thesis cheaplycanonical-crm-overlaybrief — scoping document for the next exploration step- AutoOps SMS cancel policy — concrete example of the capability disparity this architecture addresses