Automatic CRM writeback on quote approval

Approved Dealops 2 quotes can now push to Salesforce or HubSpot through the same server-side path as the manual Sync button, guarded by a single org-scoped feature flag.

PR #6463 Author @dezbah-duchicela Area Dealops 2 server State open Files 18 Diff +2433 / -187 Flag useAutomaticCrmWriteback

What it adds

New autoSyncCrmOnApproval listener for terminal quote.approval_decided events where the decision is APPROVED.

It uses primary-quote state to decide whether an approved quote is safe to auto-push.

What it reuses

The manual Sync button and the listener now share executeCrmWriteBackForQuote.

That keeps stage gates, validation, CRM writeback, timestamping, events, and primary promotion on one path.

Blast radius

Server-only. No Admin UI toggle and no client changes.

The feature flag ships default-off in code; rollout is controlled per org through feature-flag evaluation, intended for Langchain first.

Operational gate

Before enabling an org, run the primary-quote backfill so legacy synced opportunities do not look unsynced.

Live Salesforce/HubSpot verification is still required before real-org enablement.

Feature flag
Approval listener
Shared writeback
Primary quote
Backfill
Dispatcher hardening

1. Why this exists

Current behavior
  • A quote can be approved and still never reach Salesforce/HubSpot.
  • The rep must remember to click Sync manually.
  • An opportunity can have multiple quotes, but the CRM holds one quote’s numbers at a time.
Target behavior
  • Approval can trigger CRM sync automatically.
  • The synced quote becomes the opportunity’s primary quote.
  • A different primary quote blocks auto-sync to avoid silently overwriting the CRM.
Spec nuance: “Primary” is not a new rep action in this PR. It is treated as the consequence of a successful CRM writeback. That is why the listener checks isPrimary before it pushes.

2. What changes

Feature gate

Single flag: useAutomaticCrmWriteback.

The removed org-setting toggle means customers cannot enable this through orgSettings.update.

Listener

New listener: apps/server/src/lib/platformEvents/listeners/autoSyncCrmOnApproval.ts.

Subscribes to quote.approval_decided and only acts on terminal approved v2 quote events.

Writeback extraction

Shared path: executeCrmWriteBackForQuote.ts.

The tRPC route now authorizes the request, then delegates the actual writeback to the shared service.

Promotion

Primary op: promoteQuoteToPrimary.ts.

Demotes any previous primary, promotes the synced quote, and emits quote.primary_set only when the designation changes.

Primary-state decision table

Opportunity state when approval event runs Listener action Reason
No non-deleted quote is primary auto-sync First CRM sync for this opportunity, or a legacy opportunity after backfill says no primary exists.
This quote is already primary re-sync Supports recall → edit → re-approve on the canonical quote.
A different quote is primary skip Prevents a new approved quote from silently overwriting the quote already represented in CRM.
This quote was already submitted at or after this event skip duplicate Protects against sequential duplicate event delivery.

Meaningful files

Gate packages/feature-flags/flags.ts, isAutomaticCrmWritebackEnabledForOrg.ts
Listener autoSyncCrmOnApproval.ts, registered in listeners/index.ts, documented in platform-events README.
Writeback executeCrmWriteBackForQuote.ts extracts the old tRPC inline logic; trpc/router/crm/writeback.ts maps structured failures back to tRPC errors.
Primary state findPrimaryQuoteIdForOpportunity in pricingQuoteRepository.ts plus promoteQuoteToPrimary.ts.
Hardening platformEventDispatch.ts now enqueues listener jobs with deterministic jobIds.
Backfill 2026_07_27__backfill_langchain_primary_quote.ts plans and applies primary quote correction for one org at a time.

3. How it works

Listener gates
  • Event payload is v2.
  • Decision is APPROVED.
  • Workflow is complete.
  • useAutomaticCrmWriteback is enabled for the org.
  • Quote exists and is not soft-deleted.
  • Active approval still resolves to terminal APPROVED.
  • Primary-state eligibility passes.
Retry contract
  • Retry only when the result is UNKNOWN and writebackEntered !== true.
  • Do not retry after executeWriteBack starts.
  • Re-run eligibility before every pre-push retry.
  • On terminal failure: notify once, return, do not rethrow.

Shared writeback path

Manual button crm.writeback.execute still performs request-scoped authorization through tRPC, then calls executeCrmWriteBackForQuote.
Auto listener autoSyncCrmOnApproval performs event/flag/primary eligibility, then calls the same shared function with source: "auto_sync_on_approval".
Common core Registry lookup → quote load → stage gate → validation → CRM push → submittedToCrmAt stamp → quote.crm_submittedpromoteQuoteToPrimary.

Dispatcher hardening

Before

Duplicate or concurrent dispatch of the same platform event could enqueue duplicate listener jobs.

That is dangerous because CRM writeback deletes and recreates records, so it is not safely idempotent.

After

platformEventDispatch passes deterministic BullMQ IDs:

${jobName}__${eventId}

BullMQ collapses duplicate delivery to one job per listener per event.

4. Backfill and rollout shape

Backfill target
latest synced quote

For each opportunity with synced quotes but no primary, mark the quote with the latest submittedToCrmAt as primary.

Safety defaults
dry-run

The script writes only with --apply and refuses to run without a non-empty --org.

Event behavior
no emit

Backfill sets isPrimary directly and does not emit quote.primary_set for historical corrections.

Rollout gate: run and review the backfill before enabling any org. Without it, an already-synced legacy opportunity with no primary quote looks eligible for a fresh auto-sync and could overwrite CRM values.

5. What it doesn’t change

6. Risks / rollback / open questions

Risk register
Rollback

Turn useAutomaticCrmWriteback off for the org.

The listener and promotion op both read the same gate, so the kill switch stops auto-push and shared promotion scaffolding together.

Recovery path

If auto-sync fails, the listener notifies once and does not retry forever.

The rep can still use the manual Sync button, which remains the supported recovery path.

Verification added

Tests cover flag evaluation, listener decision cases, stale-event skips, pre-push retry, deterministic dispatcher job IDs, promotion statuses, and backfill planning.