Compare the Conceptual Models of Open CTI and Salesforce Voice
Open CTI is a thin bridge: your CTI adapter runs in an iframe and calls opencti JavaScript APIs to invoke the Salesforce UI. All business logic lives in your adapter. Salesforce Voice with Partner Telephony inverts this: Salesforce owns the rep lifecycle through Omni-Channel, and your connector plays two distinct roles simultaneously.
Your connector calls publishEvent() to push telephony events into Salesforce. When your telephony system fires a “call ringing” event, you translate it into CALL_STARTED; when the call ends, you publish HANGUP. These events drive everything downstream: VoiceCall record creation, AgentWork routing, screen pop, and presence sync.
Connector API methods are what you implement to be notified when telephony actions are initiated from within Salesforce. When a rep clicks Accept in the softphone panel, Salesforce calls your acceptCall(). When they click End Call, Salesforce calls your endCall(). When a supervisor initiates listen-in from Command Center for Service (formerly Omni Supervisor), Salesforce calls your superviseCall().
Return a promise with the result; don’t publish the event yourself. When Salesforce invokes one of your connector methods such as acceptCall(), endCall(), or mute(), return a promise that resolves with the appropriate result object (CallResult, MuteToggleResult). The framework publishes the corresponding event automatically based on your return value.
Do not call publishEvent() yourself from inside these methods. That causes duplicate events and breaks the softphone state machine. Reserve publishEvent() for truly asynchronous backend-initiated events such as an inbound call arriving from the carrier.
In Open CTI, the adapter pushes everything from the iframe. With Salesforce Voice with Partner Telephony, there’s a bidirectional contract via the SDK. This bidirectionality is the core architectural shift. Your connector must implement both directions correctly or the integration is incomplete.
The single biggest data model shift for migration is moving from Task records to VoiceCall records.
In Open CTI, your adapter typically:
- Pre-creates a Task record at call start (via
opencti.saveLogor custom Apex). - Updates that Task at wrap-up with duration, disposition, and notes.
The Task is the canonical “call happened” record. Reports run on Activity and Task.
- Task is generic. It’s used for emails, meetings, and all activities. Voice fields are bolted on (
CallDurationInSeconds,CallType, etc.) but the schema isn’t voice-native. - The adapter is responsible for record creation. If the adapter crashes mid-call or has a bug, you may have no record.
- Single record per call. There’s no clean way to model multiparty, consults, or transfers.
- No native recording link. You need a custom field or a parallel ContentDocument.
For outbound calls, the Salesforce platform creates a VoiceCall record automatically when CALL_STARTED is published. For inbound and transfer calls, the telephony partner creates a VoiceCall record by calling the Create a Voice Call Record Telephony Integration API. VoiceCall is the de facto record for the call going forward, including voice-native fields, multiparty support, recording linkage, and supervisor view.
| VoiceCall Field | Populated By | Replaces in Task Model |
|---|---|---|
CallerId | CALL_STARTED phoneNumber | Custom field on Task |
CallDurationInSeconds | Platform (computed at HANGUP) | Same field on Task, but populated automatically |
Status | Platform (Inbound/Outbound/Missed/Declined) | Custom field on Task |
StartTime / EndTime | Platform | Activity dates |
OwnerId | Platform (assigned rep) | Task.OwnerId, but auto-populated from Omni-Channel assignment |
RelatedRecordId | Your Omni-Channel flow (Contact/Case routed to) | Task.WhatId |
CallDisposition | Connector or rep ACW input | Custom field |
VoiceCallRecordingId | Platform (if recording enabled) | None — required custom field in Open CTI |
AdditionalFields | Connector (callInfo.additionalFields JSON) | Custom fields |
Multiparty linkage (parentVoiceCallId) | Connector at leg creation | None — not modeled in Open CTI |
- Recording is first-class. A linked VoiceCallRecording is automatically created and surfaced on the record page. No custom field, no signed URL configuration.
- Multiparty is modeled. Each leg gets its own VoiceCall, linked via
parentVoiceCallId. Reports across legs are simple SOQL. - Supervisor view runs on VoiceCall records. With Open CTI, you had to build your own supervisor dashboard.
- AI features (transcription, conversation summary, sentiment) attach to VoiceCall natively.
- Auditability and durability. The platform creates the record, not your code. If your connector crashes, the call is still logged.
Don’t call saveLog. The log already exists as VoiceCall. The question is no longer “how do I log this call?” The question is “what other records do I want to relate to this call?”
Salesforce admins can choose to relate or create any record on top of VoiceCall, not just Tasks. Common customer patterns include:
- Case: Customer support orgs typically relate the VoiceCall to a Case (new or existing). The VoiceCall ↔ Case relationship surfaces calls on the case timeline.
- Opportunity: Sales orgs relate VoiceCall to an Opportunity for activity tracking and pipeline reports.
- Lead: Inbound prospecting relates VoiceCall to a Lead.
- Account or Contact: VoiceCall already exposes related Contact/Account via
RelatedRecordId. - Custom object: For industry-specific use cases (Service Order, Patient Visit, Loan Application).
- Task: Only when the customer specifically wants Task-based reporting (legacy reports, mobile activity feed, etc.). Not the default recommendation.
This is built declaratively as a record-triggered flow on VoiceCall: same pattern, different target object.
Salesforce Voice with Partner Telephony has built-in after conversation work (ACW). When the rep clicks End Wrap-Up, WRAP_UP_ENDED is sent to your connector:
To configure ACW duration as a Salesforce admin, see Salesforce Help: Configure After Conversation Work Time.
This matrix maps every meaningful Open CTI capability to its Salesforce Voice with Partner Telephony equivalent.
| Open CTI Capability | Salesforce Voice Replacement | Migration Action |
|---|---|---|
opencti.screenPop() | Omni-Channel Flow → Screen Pop action | Eliminate. Rebuild in Flow Builder. |
opencti.saveLog() | VoiceCall (auto-created) + record-triggered Flow on whichever object the customer chooses | Eliminate. VoiceCall is the de facto record. |
sforce.opencti.searchAndScreenPop() | Omni-Channel Flow (Get Records + Screen Pop action) or LWC + Navigation API | Migrate or rebuild in Flow Builder. |
Custom presence via runApex() or Presence REST API | setAgentStatus() connector method | Eliminate. Omni-Channel owns presence. |
Other runApex() invocations | LWC @wire / imperative Apex + Toolkit API | Replace |
| Custom status list fetch via Apex | Omni-Channel presence status configurations in Setup | Eliminate |
opencti.setSoftphonePanelHeight/Width() | Configure in Contact Center setup | Eliminate |
| Custom adapter softphone panel (iframe contents) | Voice Extension LWC + VoiceExtension FlexiPage | Migrate to Voice Extension. |
| Custom call-lifecycle event listeners in adapter | lightning-service-cloud-voice-toolkit-api events | Migrate to Voice Toolkit. |
| Custom iframe ↔ page postMessage / DOM events | LMS via ServiceCloudVoiceMessageChannel__c | Migrate to LMS. |
opencti.refreshView() | notifyRecordUpdateAvailable() from LDS, or Flow that updates the record | Migrate |
opencti.enableClickToDial() and disableClickToDial() | Automatic. Handled by Salesforce Voice with Partner Telephony. | Eliminate |
| Adapter pushes data to a Visualforce page | LMS channel; Visualforce subscribes via LightningMessageService JS library | Migrate |
opencti.getPhoneContacts() for contact search | Custom LWC with @wire on getRecord or SOSL via Apex | Migrate |
| Custom IVR / routing logic | Omni-Channel Flows or Apex Actions in flows | Migrate |
| External system webhooks | Trigger from VoiceCall record-triggered Flow | Migrate |
| Custom analytics queries | VoiceCall SOQL / CRM Analytics Salesforce Voice template | Migrate |
| Auth / SSO with your backend | Move into connector init() | Keep |
| Custom supervisor dashboard | Command Center for Service (native) | Eliminate. Command Center for Service uses VoiceCall. |
| Custom transfer / consult logic | addParticipant(), dial(isConsultCall), conference(), swap() | Migrate to multiparty connector. |