donatelli.tech

Work · 01Integration and state

Bridging two CRMs that offered no way to be told when something changed.

A field-services company ran its sales operation across two CRMs and a work-management board. Leads arrived through six channels — a website form, a phone agent, a booking tool, a shared mailbox, ad-platform alert emails and a field sales app — and each one had to land, once, as one record in the right system. Every stage move on the pipeline board then had to fire its side effects exactly once: stamps, due dates, and downstream objects in operational and finance systems.

Client
A field-services company. Not named.
Layer
1 of 4. Integration and state.
Measured
2026-09-11, from the build records.
Where
Remote. On accounts the client owns.

Neither CRM could say when something had changed. The first exposed no outbound event path the automation platform could subscribe to, as deployed. The second's connector on that platform exposed two object types and nothing else — none of the objects the later pipeline stages needed to write.

01The constraint

What was actually hard.

Not the workflow count; sixteen workflows are a week of clicking. The hard part was that the same lead could arrive twice from two channels, be updated in one CRM while being created in the other, and be delivered again by a webhook retry — and the system had to produce one record, one stage stamp and one notification every time, with no trigger to anchor any of it on. The obvious substitute, a template on a no-code automation product, fails at the first requirement rather than the tenth, because a template needs a trigger and there was none to attach it to.

02The mechanism

How it works.

Everything hangs off one canonical lead record and one shared intake core. The channel adapters are thin; the core is where the guarantees live.

  1. Canonical schema before any adapter. All six channels map into one lead shape, so the core never knows which channel called it.
  2. Dedupe precedence agreed before the first write — phone in E.164 form, then email, then normalised address — written into scope rather than discovered in production.
  3. An idempotency key on every inbound event — call ID, booking UID, message ID — checked against an event log before any write. A retried webhook produces zero new records.
  4. For the CRM with no event path: a scheduled status poll five times a day, sized to the API budget and to the lead latency the client would accept, plus a bridge inside the orchestrator supplying the write path the connector lacked.
  5. Stage truth versus money truth, as one rule written at scope: a money disagreement resolves to the finance system, a stage disagreement resolves to the pipeline board. Without it the two systems sync each other in a loop.
  6. Exactly-once stage stamping. The events engine reads the target stage's stamp before writing and leaves a marker, so its own write cannot re-trigger it.
  7. Cross-system identity through a lookup table, deliberately not the board's native relation columns — which is why the build survived a platform migration with every ID intact.
  8. One error workflow attached to every branch, raising a human-readable alert carrying the execution link.
SIX CHANNELS web form phone agent booking mailbox ad alerts field sales app adapters canonical shape + idempotency key shared intake core normalise · geocode dedupe · qualify called by all six adapters high confidence CRM A no event path CRM B two object types low confidence triage board reviewer promotes status poll 5 × / day EVENTS events engine exactly-once stamps connector → webhook pipeline board stage moves operational · finance systems state store lead map · event log · sync map error workflow one, on every branch
The highlighted element is the shared intake core: the only component all six channels call and the only place the guarantees live. Everything to its left is replaceable per client; everything to its right is the client's own system of record, never replaced, only wired together.

03The figures

What it measured.

Each figure carries the date it was taken. A number without a date is not on this page.

FigureValueMeasured
Workflows16 built · 14 active2026-09-11
Functional nodes2442026-09-11
Custom code67 code nodes · 1,273 lines2026-09-11
State tables6 tables · 118 columns2026-09-11
Channel adapters calling the one shared core62026-09-11
Executions in the platform's seven-day retention window262 of 262 successfulwindow ending 2026-09-11
The last row is a clean window, not a track record: seven days is what the platform retained, and it is stated as seven days everywhere the number appears.
One honest limitation: polling is a recurring cost the client keeps paying.

Five status polls a day consume plan-tier execution quota whether or not they find a lead, and that quota is billed monthly. It is priced as a visible line in the proposal rather than absorbed, because a client should see what connecting a closed system costs them. A CRM that ships a webhook removes the line; until it does, the poll is the honest price of the bridge.

04Doing it again

What it would take to do again.

The template is the intake core, the canonical schema and the three state-table schemas; a second client swaps the CRM write node, the geocode rule and the adapters' field maps. The client-specific parts — dedupe precedence, the conflict rule, the stage side-effect table — are decided at scope, before anything is built, and become the acceptance tests.

A fit

Two or more inbound channels, and at least one system with no trigger.

Not a fit

One channel into one system with a native integration. A template does that, and should.

Up against a constraint like this one?

Thirty minutes, no prep, no pitch. Bring the thing that is slow; if it is not a fit, I say so and point you somewhere useful.