Skip to content

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.yaml

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_request

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

PieceWhat it doesBuild complexity
Canonical schemaShape definitions for the universal CRM objectsModerate. Get this wrong and everything downstream is wrong.
Overlay DSLDeclarative per-CRM extensions: field mappings, capability guards, fallbacksHigh. DSL design is hard; too narrow loses expressiveness, too broad becomes a programming language.
Runtime resolverGiven 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.
TranslatorOutbound mapping from canonical writes to real CRM API callsPer-CRM, but constrained by the overlay so quirks are explicit not scattered.
Custom-handler escape hatchcustom_handler: <fn> for the long tail the DSL cannot expressEssential. Every successful "declarative" system has one.

Prior art

WhereShape
Terraform providersCanonical resource model (HCL); providers extend with per-cloud capabilities
SQLAlchemy dialectsCanonical query model; per-dialect compilation rules
Apollo FederationCanonical schema; subgraph extensions
StripePer-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 partWhy
Capability vs. shapeTwo CRMs can have the same Appointment shape and very different cancellation semantics. The overlay has to express behavior, not just field renames.
DSL designCover 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 fidelityCanonical → CRM-A → canonical: does state survive? If the CRM stores something the canonical model does not represent, you lose it on round-trip.
DriftThe 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.
VersioningCRM 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)

  1. Mock-API work already in flight (see mock-autoops-api) builds the test environment against a canonical-shape mock. This is the substrate.
  2. 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.
  3. Sketch a second CRM's overlay (ServiceTitan, the next-most-relevant Avoca CRM) without writing the runtime. See if the DSL holds.
  4. 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.