Salesforce support sync (beta)
Unthread can synchronize support tickets, messages, and attachments between an Unthread project and Salesforce Cases. This guide covers the standard setup and the optional setup for organizations that store Case messages in a custom Salesforce object.
Beta: Salesforce sync is currently in beta. Contact your Unthread account manager to have it enabled for your workspace before beginning setup.
Before you begin
You need:
- An Unthread administrator account with Salesforce sync enabled.
- A Salesforce administrator who can install managed packages and assign permission sets.
- A dedicated or existing Salesforce integration user with API access.
- Read, create, and update access to the Cases, Contacts, Accounts, fields, and record types that you want Unthread to synchronize.
- Create, read, update, and delete access to any custom message object selected in the channel.
The Salesforce user who connects the organization to Unthread is the user whose permissions Unthread uses. Do not connect with a temporary administrator if the intended integration user has narrower permissions.
The base Unthread package supports native
Case, always observes nativeCaseCommentevents, and synchronizes Case-owned Salesforce Files. A channel can write outbound messages to native Case Comments or to one configured custom message object; that custom object is then accepted as an additional inbound source. The package does not installEmailMessageorFeedItemtriggers. Email and feed synchronization are not part of this setup.
1. Install the Unthread managed package
- In Unthread, open Settings > Integrations > Support > Salesforce.
- Install the Unthread managed package.
- Sign in to the Salesforce organization you want to connect.
- Install the package for the users who will administer or run the integration according to your organization's package policy.
- In Salesforce Setup, search for Permission Sets.
- Open Unthread Integration User, select Manage Assignments, and assign it to the Salesforce user that will authorize Unthread.
- Download and deploy the subscriber-owned Unthread Standard Object Access permission set, then assign it to the same user. The download includes a ready-to-deploy Salesforce DX project and instructions.
Assigning both permission sets is required. Salesforce strips standard-object and user permissions from managed-package permission sets, so Unthread Integration User grants only the package Apex API, custom permission, package-owned Case fields, identity objects, and outbox access. Your subscriber-owned permission set or profile must grant:
- API Enabled.
- Account: read.
- Case: read, create, and edit, plus access to the standard Case fields Unthread synchronizes.
- Native Case Comments: access through the Case permissions above.
- Contact: read and create, including
AccountIdandEmailfield access. - Salesforce Files: permission to read and create Files and link or unlink them on Cases in scope.
Do not grant View All or Modify All unless your record-sharing design requires it. The OAuth user must still receive record-level access to each Case Unthread should synchronize. Grant subscriber-owned mapped Case fields and custom-message objects separately. Salesforce documents the assignment process in Manage Permission Set Assignments.
2. Add the Unthread fields to the Case page
The package installs these fields on Case:
| Field label | Purpose |
|---|---|
| Unthread Sync Enabled | Opts a Salesforce-origin Case into synchronization. |
| Unthread Sync Status | Shows Disabled, Pending, Linked, or Error. |
| Unthread Ticket URL | Opens the linked ticket in Unthread. |
| Unthread Sync Error | Shows the last actionable synchronization error. |
| Unthread Conversation ID | Stores the linked Unthread ticket identity. |
| Unthread Sync Key | Stores the stable key used to create or reconnect the Case link. |
Add these fields to every Case record page and page layout used by the teams participating in the integration. In organizations using Dynamic Forms, adding a field to a page layout alone may not make it visible on the Lightning record page; add it to the active Case record page in Lightning App Builder as well.
We recommend making Unthread Sync Enabled, Unthread Sync Status, Unthread Ticket URL, and Unthread Sync Error visible to agents. The conversation ID and sync key are primarily useful for administrators and troubleshooting.
Unthread Sync Enabled defaults to false. You can let agents opt in manually, set a different default in your own Case creation automation, or use a Salesforce Flow or trigger to set it when your business rules match. No Unthread-specific button is required.
3. Connect the Salesforce organization
- Return to Settings > Integrations > Support > Salesforce in Unthread.
- Select Connect production org or Connect sandbox.
- Sign in as the Salesforce integration user and approve access.
- Wait for both the connection and package status to become active.
Unthread reads the organization ID, name, and instance URL from Salesforce. Each connected Salesforce organization has its own OAuth credential and HMAC signing key. Unthread provisions the signed webhook destination and key automatically; neither the OAuth connection nor the HMAC key is shared across connected organizations, and you do not need to create or copy a webhook secret.
If package setup reports an error, confirm that:
- The package is installed in the same Salesforce organization that you authorized.
- The authorizing user has the Unthread Integration User permission set.
- The authorizing user has your Unthread Standard Object Access permission set or equivalent profile access.
- The user can read the required Case and Contact fields and update
Case.ContactId. - The user's API access has not been revoked.
After fixing permissions, use Retry package setup in Unthread.
4. Create a Salesforce channel
A Salesforce channel connects one Salesforce organization to one Unthread project and defines two directions of ticket creation:
- Inbound rules decide which Salesforce Cases create tickets in this Unthread project.
- Outbound rules decide which Unthread tickets create Cases in this Salesforce organization.
These are creation rules, not a direction switch for an existing link. After a Case and ticket are linked, updates continue to use the channel configuration stored with that link.
- Open the Unthread project that should receive the Cases.
- Add a Salesforce channel.
- Choose the connected Salesforce organization.
- Configure the inbound rules. Enable Salesforce-to-Unthread creation and add only the conditions needed to route Cases to this project. If more than one Salesforce channel matches, Unthread chooses one deterministically.
- Configure the outbound rules. Choose which tickets in this project should create Salesforce Cases. A ticket that does not match an outbound rule remains only in Unthread.
- If the channel has an outbound rule, choose the Salesforce Case origin that Unthread should set on newly created Cases. The available values come from the connected organization.
- Choose a default Unthread ticket type for Salesforce-origin Cases.
- Review message and field settings, then save the channel.
For Salesforce-origin creation, set Unthread Sync Enabled on a matching Case. Unthread uses the inbound conditions to select a channel. For Unthread-origin creation, create a ticket that matches the channel's outbound rules.
Salesforce-origin tickets are not relayed into another Salesforce channel, even when they match that channel's outbound rules. This prevents loops and duplicate Cases. Outbound Salesforce rules continue to apply to matching tickets that originate in Slack, email, the dashboard, and other non-Salesforce sources. Salesforce-to-Salesforce relays are not supported in this release.
5. Configure message synchronization
For every inbound-enabled Salesforce channel, native Case Comments remain an inbound source. The outbound message adapter controls where replies and internal notes created in Unthread are stored in Salesforce.
Outbound default: Salesforce Case Comments
Keep Case Comments as the outbound adapter unless your organization already has a required custom message object.
- A public Unthread reply creates a Salesforce
CaseCommentwithIsPublished=true. - An Unthread internal note creates a
CaseCommentwithIsPublished=falsewhen Sync internal notes is enabled. - Public and private Case Comments synchronize back to the linked Unthread ticket.
- Salesforce controls whether a public Case Comment sends a customer notification. Configure that behavior and the Case Comment email template in Salesforce Support Settings.
Case Comments created by Unthread carry package-owned identity information outside the CaseComment record. This allows retry, update, delete, and echo suppression without adding customer-specific fields to CaseComment.
Optional outbound storage: a custom message object
Choose Custom object only when outbound Unthread messages must be written to an object owned by your organization. When selected, that custom object is used in both directions:
- Unthread creates, updates, and deletes records on that object.
- Your Salesforce automation calls the package action when those records are created, updated, or deleted in Salesforce.
The outbound choice does not disable native Case Comment intake. Avoid mirroring one custom message into a Case Comment as well, because the two Salesforce records have independent identities and may appear as duplicate messages in Unthread.
Use Inbound message rule to filter messages after Unthread refetches the authoritative Salesforce record. With no rule, every otherwise-valid Case Comment and configured custom-object record is accepted. Rules can use normalized values such as source and visibility or fields from the authoritative Salesforce record, for example Audience__c = Warehouse. Internal messages are still excluded when Sync internal notes is off, even if a rule matches.
Rule changes apply to future events and pending identities that have not been materialized. They do not replay already filtered history automatically; a later Salesforce update reevaluates that record.
Message observation does not opt a Case into synchronization. If a message arrives before its Case qualifies for an inbound rule or is created through an outbound relay, Unthread retains only the Salesforce record identity. It does not retain the body or create an Unthread ticket. If the Case links later, Unthread refetches the current record, applies that channel's message rule, and then creates the message when eligible.
Pending identities have no automatic expiration so a Case can qualify later without losing its message history. They contain Salesforce IDs and event provenance only, never message content or raw webhook payloads.
6. Configure Case field synchronization
Unthread automatically synchronizes the standard fields for which Salesforce has a compatible Case concept:
| Unthread | Salesforce | Matching behavior |
|---|---|---|
| Title | Case.Subject | Bidirectional. |
| Description | Case.Description | Bidirectional. |
| Ticket type | Usually Case.Type, or your selected field | A unique normalized label match unless overridden. |
| Status and sub-status | Case.Status | A unique normalized label match unless overridden. |
| Priority | Case.Priority | A unique normalized label match unless overridden. |
| Assignee | Case.OwnerId | Salesforce Users match by a unique exact email. Queues do not map to Unthread users. |
| Submitter | Case.ContactId | Matches or creates a native Contact by exact email, subject to configuration and permissions. |
| Account | Case.AccountId | Uses the connected organization's Account identity. |
| Compatible custom fields | Your selected Case fields | A unique field name and option label match unless overridden. |
Unthread performs a three-way merge using the last synchronized state. A change made on only one side is copied to the other side. Independent changes on both sides are merged. If both sides change the same field before synchronization completes, the Unthread value wins for that field.
Clearing an Unthread assignee does not clear the Salesforce Case owner because Salesforce requires an owner. Unthread leaves the Salesforce owner unchanged and does not treat that intentional difference as a new Salesforce change.
Advanced mapping overrides
Start with automatic matching. Open Advanced mappings only when:
- Salesforce and Unthread labels do not match.
- More than one field or option has the same normalized name.
- A custom Case field should map to a differently named Unthread custom field.
- An option needs an explicit value translation.
Explicit mappings take precedence over automatic matches. The integration user needs read and write field-level access to every mapped Salesforce field. Required Salesforce fields must either have a mapped value, a Salesforce default, or automation that supplies them.
7. Attachment synchronization
Unthread automatically synchronizes supported files attached directly to the linked Case or ticket. There is no channel-level attachment setting.
- Salesforce files are stored as Salesforce Files and linked to the Case.
- Unthread files remain Unthread files; the package stores identity only, not a second copy of their content.
- Files above the integration's 25 MiB safety limit are skipped.
- A file attached only to a custom message record is not automatically a Case-owned file. Link it to the parent Case if it should synchronize.
Set up a custom Salesforce message object
This section uses Support_Message__c as an example. Substitute your own object and field API names consistently.
Create or verify the object and fields
The integration user must have create, read, update, and delete access to the object and read/write access to every configured field.
| Example API name | Required Salesforce field definition | Purpose |
|---|---|---|
Case__c | Lookup or master-detail relationship to Case; createable and updateable | Parent Case identity. |
Body__c | Long Text Area with a length of at least 32,768; createable and updateable | Message body. |
Visibility__c | Picklist with two distinct active values, for example Public and Private; createable and updateable; a restricted picklist is recommended | Public reply versus internal note. |
Unthread_Message_Id__c | Text, External ID, Unique, length at least 36; 255 is recommended; createable and updateable | Idempotent upsert and echo suppression for Unthread-origin messages. |
Author__c | Optional Text field; createable and updateable | Author name or Salesforce user ID as text. Do not configure read-only CreatedById here. |
Occurred_At__c | Optional Date/Time field; createable and updateable | Original message time. |
Make the Case, body, and visibility values required with field configuration or validation rules. The exact public and private values are your choice, but they must be active picklist API values and must match the values entered in Unthread exactly.
Configure a different Salesforce API field name for every purpose. Reusing one field for two purposes is not supported, even when the API-name casing differs. Unthread writes outbound bodies as plain text in the form From Name: followed by a newline and the message text, so use a Long Text Area rather than a Rich Text Area for the body field.
Prevent a message from being moved to a different Case after creation. Unthread scopes message identity to the linked Case, so reparenting a message without a delete/create pair can leave the original ticket with a stale message. Add a validation rule such as:
AND(NOT(ISNEW()), ISCHANGED(Case__c))
Use an error such as A synchronized message cannot be moved to another Case. If your workflow must support moves, implement that as a delete event for the old Case followed by a create event for the new Case rather than using the base trigger below.
The unique external-ID field may be blank for Salesforce-origin messages. Unthread fills it when writing an Unthread-origin message. Do not populate it with an unrelated Salesforce identifier.
If your object has other required fields, provide Salesforce defaults or automation that fills them. Unthread only writes the fields selected in the channel.
Configure the custom-object adapter in Unthread
In the Salesforce channel's Message sync section:
- Choose Custom object.
- Enter the message object API name:
Support_Message__c. - Enter the Case relationship field:
Case__c. - Enter the body field:
Body__c. - Enter the visibility field:
Visibility__c. - Enter the Unthread message ID external-ID field:
Unthread_Message_Id__c. - Enter the exact public and private picklist API values, such as
PublicandPrivate. - Optionally enter
Author__candOccurred_At__c. - Enable Sync internal notes if private messages should synchronize.
Unthread validates the object and field metadata when the channel is saved. A channel cannot be activated if the selected object lacks create, update, or delete access, or if any field has an incompatible definition.
Add the Apex trigger adapter
The managed package deliberately does not install a trigger on your object. Add the following trigger and handler in your Salesforce organization. Replace Support_Message__c and the example field API names if yours differ.
Salesforce can invoke a trigger with as many as 200 records at once. This example is bulk-safe: it collects the complete trigger batch, performs one package-action invocation outside the record loop, does not query package-owned objects, and does not perform DML on the source records. Do not call the package action separately for each record. Submit every record lifecycle event. The package sends identity only, and Unthread refetches the authoritative record, suppresses its own outbound writes, and applies the channel's inbound message rule.
trigger SupportMessageUnthreadTrigger on Support_Message__c (
after insert,
after update,
after delete,
after undelete
) {
if (Trigger.isInsert) {
SupportMessageUnthreadHandler.afterInsert(Trigger.new);
} else if (Trigger.isUpdate) {
SupportMessageUnthreadHandler.afterUpdate(Trigger.new);
} else if (Trigger.isDelete) {
SupportMessageUnthreadHandler.afterDelete(Trigger.old);
} else if (Trigger.isUndelete) {
SupportMessageUnthreadHandler.afterUndelete(Trigger.new);
}
}
public without sharing class SupportMessageUnthreadHandler {
private static final String OBJECT_API_NAME = 'Support_Message__c';
public static void afterInsert(List<Support_Message__c> records) {
emit(records, 'created');
}
public static void afterUpdate(List<Support_Message__c> records) {
emit(records, 'updated');
}
public static void afterDelete(List<Support_Message__c> records) {
emit(records, 'deleted');
}
public static void afterUndelete(List<Support_Message__c> records) {
// The package action has no separate restore event. An update tells
// Unthread to refetch the restored authoritative record.
emit(records, 'updated');
}
private static void emit(
List<Support_Message__c> records,
String eventType
) {
List<unthread.UnthreadCustomMessageAction.Request> requests =
new List<unthread.UnthreadCustomMessageAction.Request>();
for (Support_Message__c record : records) {
if (record.Case__c == null) {
continue;
}
unthread.UnthreadCustomMessageAction.Request request =
new unthread.UnthreadCustomMessageAction.Request();
request.objectType = OBJECT_API_NAME;
request.objectId = String.valueOf(record.Id);
request.parentCaseId = String.valueOf(record.Case__c);
request.eventType = eventType;
requests.add(request);
}
if (!requests.isEmpty()) {
unthread.UnthreadCustomMessageAction.emit(requests);
}
}
}
The package request deliberately has only four values: object API name, record ID, parent Case ID, and event type. Do not pass body, audience, author, timestamp, the Unthread external ID, changed fields, or arbitrary JSON. Those values come from the channel mapping and the authoritative record fetched by Unthread.
Add Apex tests for the handler as part of your normal Salesforce deployment process. At minimum, cover batch insert, update, delete, and undelete, a record whose Case is not synchronized yet, and a missing Case lookup. Separately test that the validation rule rejects Case reparenting.
The action accepts any well-formed custom-record identity whose parent Case exists. It does not inspect or change the Case's Unthread sync checkbox or link fields. It writes a durable package outbox row and queues delivery asynchronously, so calling it from trigger context performs no synchronous HTTP callout. An early message does not synchronize its Case; Unthread stages only the identity until normal Case intake or an outbound relay links that Case.
Flow alternative
You can use record-triggered Flows instead of Apex. Create flows for the lifecycle events you need, add the package action Send custom Salesforce message event to Unthread, and map only object API name, record ID, parent Case ID, and event type from the triggering record. Salesforce supports record-triggered automation for created, updated, and deleted records; see Start an Automation When a Record Is Created or Changed.
Use the Apex example when bulk behavior, recycle-bin restoration, and one package-action invocation per trigger batch are important. If you choose Flow, test every lifecycle operation the Flow implements. It is safe to emit updates unconditionally; Unthread refetches and converges the current record.
Verify the setup
Use a test Case and ticket to exercise both directions:
- Create a matching Salesforce Case, enable Unthread sync, and confirm that one Unthread ticket is created and linked.
- Create a matching Unthread ticket and confirm that one Salesforce Case is created and linked.
- Change title, description, status, priority, assignee, submitter, Account, and mapped custom fields from Salesforce, then verify Unthread.
- Change the same fields from Unthread, then verify Salesforce. Include field clears and picklist options whose labels match automatically.
- Create public and private messages in both systems and verify their visibility.
- Update and delete messages in both systems.
- Upload and remove supported files in both systems.
- For a custom object, confirm that Unthread-origin records contain
Unthread_Message_Id__cand do not create duplicate Unthread messages when the Salesforce trigger runs.
Troubleshooting
A Salesforce Case does not create an Unthread ticket
- Confirm Unthread Sync Enabled is selected.
- Confirm the Case matches the channel's inbound rules.
- Confirm the connected Salesforce organization is active.
- Check Unthread Sync Status and Unthread Sync Error on the Case.
- Confirm required Account behavior and the default Unthread ticket type are configured.
An Unthread ticket does not create a Salesforce Case
- Confirm the ticket matches an enabled outbound rule that was saved.
- Confirm required Salesforce fields such as
Originhave a default or configured value. - Confirm the integration user can create Cases with the selected record type.
- Review the relay error shown in Unthread.
Salesforce changes do not synchronize back
- Confirm the Case is linked and the channel's inbound synchronization remains enabled.
- Confirm the integration user can read the changed fields.
- Confirm explicit mappings use Salesforce field API names, not labels.
- For a custom message object, confirm the trigger's object API name exactly matches the channel.
Custom messages synchronize only in one direction
- Confirm the integration user has create, read, update, and delete access to the custom object.
- Confirm all configured fields are createable and updateable.
- Confirm the visibility field contains the configured active picklist values.
- Confirm the external-ID field is Text, Unique, External ID, and at least 36 characters long.
- Confirm the trigger calls
unthread.UnthreadCustomMessageAction.emitonce with the full request list. - Confirm the trigger passes only the object API name, record ID, parent Case ID, and lifecycle event type.
- Confirm the parent Case has linked through its own inbound Case rule or an outbound relay. A message event does not link the Case.
- Check the channel's inbound message rule. Missing fields and evaluation errors fail closed, and private records remain excluded when internal-message sync is disabled.
Webhook delivery is delayed or failed
In Salesforce, inspect the package's Unthread Webhook Events records. Events can be pending, retrying, delivered, or dead-lettered. Fix the underlying permission or network error before replaying a dead-letter event. In Unthread, use Retry package setup if provisioning is not active.
For local or sandbox testing through a temporary public URL, the Salesforce administrator must add that HTTPS origin as a Remote Site Setting and reprovision the organization from Unthread whenever the origin changes.