Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

CRM custom-object linkage: bind an AOP to any CRM object, not just Deal

merged#2833CedarCopilot

CedarCopilot wants to merge 9 commits into staging from feat/crm-custom-object-linkage

Live on prod, 0 of 9 surfaces working, 2 days leftTimeline and evidence
  1. Opened
    Sep 19, 2026, 8:23 PM
  2. Sep 20, 2026, 8:53 AM
  3. Merged
    Sep 20, 2026, 9:54 AM
  4. Live on prod
    Sep 20, 2026, 9:54 AM
  5. Observed 1 hour, 9 surfaces, 0 requests
    Sep 20, 2026, 9:54 AM
  6. Watching

    Live on prod, 0 of 9 surfaces working, 2 days left

    Sep 20, 2026, 9:54 AM
  7. Pipelines steady after this deploy
    Sep 20, 2026, 9:54 AM

Behaviors Libra is checking

A user or organization AOP can persist the CRM object type it governs through boundObjectApiName, while an unset binding continues to mean the Deal or Opportunity object.Not checked
prod
CRM object mapping configuration is scoped by the selected AOP and resolves the raw CRM object from that AOP instead of requiring users to select or provide an object API name.Not checked
prod
Users with multiple AOPs can independently configure field mappings, sync filters, stage-required fields, and stage source fields for each bound CRM object, with switching AOPs loading that object's saved settings.Not checked
prod
The CRM agent badge displays a non-Deal link using the governing AOP name and unlinks it by sending recordId rather than treating it as a Deal.Not checked
prod
Non-Deal CRM links are represented with generic recordId, recordName, and recordUrl metadata instead of requiring dealId and dealUrl fields.Not checked
prod
Conversation overview fields and CRM sync links select non-Deal records through recordId and recordUrl just as they previously selected Deals through dealId and dealUrl.Not checked
prod

Libra found 9 production surfaces on prod but could not judge any of them yet. 9 surfaces had no requests at all. Libra checks hourly for 3 days after each deploy.

Summary

Generalizes Cedar's CRM integration so an AOP/playbook can be bound to any CRM object , a HubSpot Lead, an Attio Contact filtered by lead status, a Salesforce partner-directory object , not just Opportunity/Deal. Built for three real, scoped instantiations: Vooma (HubSpot Leads), Mastra (Attio people/lead_status), Warp (Salesforce Partner__c). Full design at docs/design/crm-custom-object-linkage-leads.md.

Core model: each AOP gains one new field, boundObjectApiName. A human or agent configuring anything always passes aopId, never a raw CRM object name , the system resolves objectApiName from that AOP internally. No new persisted "binding" table; it's a join between the AOP and the connection's existing field-mapping storage.

What's included

  • Schema: boundObjectApiName on agentOperatingProcedures/orgAops; generic recordId/recordUrl/recordName on ExternalCrmIntegrationMetadata (one pair covers every non-Deal object, since a conversation is only ever bound to one at a time); objectApiName on crm_conversation_updates (closes a real ID-collision risk between a Deal and a same-ID Lead on one connection).
  • Drivers: Salesforce/HubSpot/Attio all generalized in place (updateOpportunityFields/fetchOpportunityProperties take an optional objectApiName, defaulting to Opportunity , zero behavior change for every existing call site). Attio's select/status UUID-resolution gate, previously hardcoded to the literal 'stage', now works for any select field.
  • Sync mechanics: pull (cron), push (the real write workflow), the before-meeting auto-link backstop, the periodic reconcile sweep, and the manual "sync now" bulk-refresh path are all object-aware. Per-object watermark, per-object connection flags (push/pull/output-gating toggles), per-object stageRequiredFields.
  • Config surface: settings UI gets an AOP picker; the MCP update_crm_field_mappings tool takes aopId; org→rep mapping propagation extended to non-Deal objects.
  • UI: the CRM-synced badge and field renderers recognize recordId alongside dealId.
  • Reporting: deal-metrics/deal-timeline/deal-audit extended toward Lead-band (the other 3 of 6 reporting files deliberately left alone , documented why, not silently skipped).
  • A CI-enforced regression guardrail: five independent pre-existing sites had shipped the identical "force-reassign this conversation to the AOP named Deals, unconditionally" bug before this PR found and fixed them. A new test fails on any future literal AopName.Deals comparison outside a documented allowlist.

How this was built and verified

Implemented via 8 parallel subagents against a shared foundation (schema + driver interface), each required to write tests and self-run a thermo-nuclear code-quality review before reporting back. A full aggregate review across the whole branch (not just each slice) then found 4 real blocking gaps no individual slice's author could see in isolation , most importantly, the actual push-write path (update-external-crm-workflow.ts) had never been touched by any of the 8 slices, meaning nothing in the branch could yet write a field update to a non-Deal record. Two more rounds of targeted fixes closed all 4 gaps, and a final confirmation pass caught one more integration-seam bug (a field-mapping-resolution function neither fix's file list included, now called in a new non-Deal context by both). Every fix has a regression test that traces the exact bug it closes.

Verified at final HEAD: pnpm --filter @zero/server run types clean · pnpm deps:check clean (no dependency-direction violations) · 1897 tests across 175 files, all passing.

What's deliberately NOT in this PR

  • Superglue tool changes , drafted and live-tested against the real wire request (payload renames, new generic read workflows), but never saved (sg tool save) , external system, needs explicit review/approval. Draft IDs below.
  • **Live data ope
Show production surfaces and changed-file mapping

Production surfaces

SurfaceRequestsErrorsp95UsersVerdict
/api/trpc/crm.searchConversationsMinimal0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
POST /api/trpc/crm.searchConversationsMinimal0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
/api/trpc/crm.getConversations0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
POST /api/trpc/crm.getConversations0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
crm.searchConversationsMinimal0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
trpc.crm.searchConversationsMinimal0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
trpc.crm.getConversations0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
/api/trpc/crm.loadCalendarConversationsBatch0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.
/api/trpc/integrations.slack.syncConversationChannels0 → 00 → 0 (0%)not measured0No traffic
No requests recorded since this deploy.

Changed files → surfaces

  • apps/server/src/services/crm/conversations.ts/api/trpc/crm.searchConversationsMinimalPOST /api/trpc/crm.searchConversationsMinimal/api/trpc/crm.getConversationsPOST /api/trpc/crm.getConversationscrm.searchConversationsMinimaltrpc.crm.searchConversationsMinimal+2
  • apps/server/src/trpc/routes/integrations.ts/api/trpc/integrations.slack.syncConversationChannels
  • apps/mail/modules/conversations/components/ConversationOverviewCard.tsxno production surface mapped
  • apps/mail/modules/conversations/components/CrmAgentBadge.tsxno production surface mapped
  • apps/mail/modules/conversations/components/conversationFieldRenderers.tsxno production surface mapped
  • apps/mail/modules/crm/types/index.tsno production surface mapped
  • apps/mail/modules/integrations/crm-integration-card.tsxno production surface mapped
  • apps/server/.claude/skills/cedar-configuration/fields-and-values.mdno production surface mapped
  • apps/server/scripts/backfill-crm-object-records.tsno production surface mapped
  • apps/server/src/cron/__tests__/process-external-crm-sync.test.tsno production surface mapped
  • apps/server/src/cron/process-external-crm-sync.tsno production surface mapped
  • apps/server/src/db/aop-schema.tsno production surface mapped
  • apps/server/src/db/crm-schema.tsno production surface mapped
  • apps/server/src/db/migrations/add_aop_bound_object_api_name.sqlno production surface mapped
  • apps/server/src/db/migrations/add_crm_conversation_updates_object_api_name.sqlno production surface mapped
  • apps/server/src/mastra/routeHandlers/event-execution/__tests__/handleExecuteExternalCrm.test.tsno production surface mapped
  • apps/server/src/mastra/routeHandlers/event-execution/handleExecuteExternalCrm.tsno production surface mapped
  • apps/server/src/mastra/skills/account-config/tools/__tests__/applyConfigChangeTool.reconciliation.test.tsno production surface mapped
  • apps/server/src/mastra/skills/account-config/tools/__tests__/applyConfigChangeTool.test.tsno production surface mapped
  • apps/server/src/mastra/skills/account-config/tools/applyConfigChangeTool.tsno production surface mapped
  • apps/server/src/mastra/tools/config/config-propagate-tool.tsno production surface mapped
  • apps/server/src/mastra/tools/conversation/__tests__/findCRMConversationsTool.default-aop-scope.test.tsno production surface mapped
  • apps/server/src/mastra/tools/conversation/__tests__/updateConversationFieldsTool.unchanged-approval.test.tsno production surface mapped
  • apps/server/src/mastra/tools/conversation/findCRMConversationsTool.tsno production surface mapped
  • apps/server/src/mastra/workflows/chat/chat-org-rules.tsno production surface mapped
  • apps/server/src/mastra/workflows/event-execution/pre-execution-setup.tsno production surface mapped
  • apps/server/src/mastra/workflows/sync/__tests__/update-external-crm-workflow.test.tsno production surface mapped
  • apps/server/src/mastra/workflows/sync/update-external-crm-workflow.tsno production surface mapped
  • apps/server/src/services/aop/__tests__/adhoc-chat-aop-scope.test.tsno production surface mapped
  • apps/server/src/services/aop/__tests__/bound-object-resolution.test.tsno production surface mapped
  • apps/server/src/services/aop/__tests__/no-hardcoded-deals-aop-comparison.test.tsno production surface mapped
  • apps/server/src/services/aop/__tests__/propagate-mapping.test.tsno production surface mapped
  • apps/server/src/services/aop/adhoc-chat-aop-scope.tsno production surface mapped
  • apps/server/src/services/aop/bound-object-resolution.tsno production surface mapped
  • apps/server/src/services/aop/linked-deal-output-policy.tsno production surface mapped
  • apps/server/src/services/aop/propagate-mapping.tsno production surface mapped
  • apps/server/src/services/aop/utils.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/active-deal.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/before-meeting-prep-floor.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/conversations.stage-required-fields-object-scope.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/crm-conversation-updates-object-api-name.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/crm-deal-auto-link.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/crm-discover-new-deals.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/crm-field-sync-full.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/crm-pull-refresh.record-refresh.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/crm-reconcile-deals.account-only-object.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/crm-sync-watermark.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/deal-audit.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/deal-metrics.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/deal-timeline.test.tsno production surface mapped
  • apps/server/src/services/crm/__tests__/external-crm-events.test.tsno production surface mapped
  • apps/server/src/services/crm/active-deal.tsno production surface mapped
  • apps/server/src/services/crm/before-meeting-prep-floor.tsno production surface mapped
  • apps/server/src/services/crm/crm-deal-auto-link.tsno production surface mapped
  • apps/server/src/services/crm/crm-discover-new-deals.tsno production surface mapped
  • apps/server/src/services/crm/crm-field-sync.tsno production surface mapped
  • apps/server/src/services/crm/crm-pull-refresh.tsno production surface mapped
  • apps/server/src/services/crm/crm-reconcile-deals.tsno production surface mapped
  • apps/server/src/services/crm/crm-sync-watermark.tsno production surface mapped
  • apps/server/src/services/crm/deal-audit.tsno production surface mapped
  • apps/server/src/services/crm/deal-facts.tsno production surface mapped
  • apps/server/src/services/crm/deal-link-metrics.tsno production surface mapped
  • apps/server/src/services/crm/deal-metrics.tsno production surface mapped
  • apps/server/src/services/crm/deal-status.tsno production surface mapped
  • apps/server/src/services/crm/deal-timeline.tsno production surface mapped
  • apps/server/src/services/crm/external-crm-events.tsno production surface mapped
  • apps/server/src/services/integrations/__tests__/connection-config.test.tsno production surface mapped
  • apps/server/src/services/integrations/connection-config.tsno production surface mapped
  • apps/server/src/services/integrations/crm/__tests__/attio-object-binding.test.tsno production surface mapped
  • apps/server/src/services/integrations/crm/__tests__/field-mapping-writer.test.tsno production surface mapped
  • apps/server/src/services/integrations/crm/__tests__/hubspot-object-binding.test.tsno production surface mapped
  • apps/server/src/services/integrations/crm/__tests__/salesforce-object-binding.test.tsno production surface mapped
  • apps/server/src/services/integrations/crm/attio.tsno production surface mapped
  • apps/server/src/services/integrations/crm/driver.tsno production surface mapped
  • apps/server/src/services/integrations/crm/field-mapping-writer.tsno production surface mapped
  • apps/server/src/services/integrations/crm/hubspot.tsno production surface mapped
  • apps/server/src/services/integrations/crm/salesforce.tsno production surface mapped
  • apps/server/src/services/mail/initial-sync/__tests__/assign-event-aop-correction.test.tsno production surface mapped
  • apps/server/src/services/mail/initial-sync/__tests__/resolve-aop-step.test.tsno production surface mapped
  • apps/server/src/services/mail/initial-sync/finalize-steps.tsno production surface mapped
  • apps/server/src/services/mail/initial-sync/handlers.tsno production surface mapped
  • apps/server/src/trpc/routes/__tests__/integrations-update-write-settings.test.tsno production surface mapped
  • apps/server/src/trpc/routes/crm.tsno production surface mapped
  • docs/design/crm-custom-object-linkage-leads.mdno production surface mapped