Salesforce Service Cloud Loader
The Salesforce Service Cloud loader lands your support data — cases, case comments and field history, emails, messaging and chat sessions, voice calls, entitlements and milestones, Omni-Channel work items and knowledge articles — in your data warehouse, alongside the contacts, accounts and users those records point at. It reads the Salesforce REST API with SOQL, incrementally where Salesforce gives a reliable change timestamp, and captures soft-deleted records so your warehouse can tell a closed case from a deleted one.
Service Cloud runs on the same Salesforce Platform as Sales Cloud, so this loader uses the same sign-in and the same API. Use it for service objects; use the Salesforce loader for Sales Cloud objects such as leads, opportunities and campaigns. Both can point at the same org.
Prerequisites
- A Salesforce org on an edition that includes API access (Enterprise, Unlimited, Performance or Developer Edition; Professional Edition only with the API add-on). Production and Developer Edition orgs only — sandbox orgs are not supported yet (see Troubleshooting).
- A Salesforce user to connect with — a dedicated integration user is recommended — whose profile or permission set has:
- the API Enabled system permission;
- Read access to every object you want to load (and View All on it, or a sharing model that lets the user see every record — the loader only sees what the user sees);
- field-level read access to the fields you want populated. A field the user cannot see is loaded as
NULLrather than failing the run.
- A connected Warehouse (target warehouse) with write permissions on the target schema.
Feature requirements per object
Several service objects only exist when the matching Salesforce feature is turned on. Once you have connected Salesforce, the stream list only offers the objects your org exposes to the connected user — an org without Lightning Knowledge, Messaging or Service Cloud Voice is not offered those streams. If Zeotap cannot list your org’s objects (for example a timeout), it offers every stream rather than hiding any. A stream whose object your org does not have — selected before a feature was turned off, or through the API — fails with a message naming the object (see Troubleshooting), and the loader’s connection test ends with a warning (never a failure) listing the optional objects your org does not expose.
| Salesforce feature | Objects it provides | Notes |
|---|---|---|
| Core Service Cloud | Case, CaseComment, CaseHistory, CaseContactRole, Account, Contact, Asset, User, Group | Always available. CaseHistory only records fields that have field history tracking enabled on Case. |
| Case Teams | CaseTeamMember | Requires case teams to be set up (Setup → Case Teams). |
| Entitlement Management | Entitlement, ServiceContract, CaseMilestone | Also populates the entitlement and milestone fields on Case (for example EntitlementId, MilestoneStatus, SlaStartDate, IsStopped). |
| Email-to-Case or Enhanced Email | EmailMessage | Without one of them the object is absent. |
| Messaging (Messaging for In-App and Web, WhatsApp, SMS, Facebook Messenger, …) | MessagingSession, MessagingEndUser | |
| Chat (legacy Live Agent) | LiveChatTranscript | Salesforce retired legacy Chat in February 2026. The object still holds historical transcripts in orgs that used it; load it once to keep that history. |
| Service Cloud Voice | VoiceCall | |
| Omni-Channel | AgentWork | One row per work item routed to an agent. |
| Lightning Knowledge | the org’s knowledge article version object (usually Knowledge__kav) | Loaded as the Knowledge stream; see Knowledge. |
Authentication
The Salesforce Service Cloud loader uses OAuth 2.0 (authorization code flow with PKCE) through Zeotap’s own Salesforce connected app, so you do not need to create one.
- Navigate to Loaders in the left sidebar and click Add Loader
- Select Salesforce Service Cloud
- Click Connect with Salesforce
- Sign in to Salesforce as the integration user and click Allow
- You are redirected back to Zeotap with the connection established. The Instance URL is filled in from the OAuth response.
Zeotap requests the following OAuth scopes:
| Scope | Purpose |
|---|---|
api | Read your org’s data through the Salesforce REST API |
refresh_token | Keep a long-lived connection without signing in again |
offline_access | Refresh the access token when it expires, including during scheduled runs |
Zeotap refreshes the access token automatically, including in the middle of a run if Salesforce invalidates the session. If the refresh token itself is revoked — the user’s password is reset, an admin revokes the connected app, or the app’s refresh-token policy expires it — reconnect the loader from its detail page.
Instance URL and My Domain
The Instance URL is your org’s My Domain URL, for example https://yourcompany.my.salesforce.com. It is filled in automatically on connect; you only need to edit it if your org’s My Domain changes. It must start with https://.
Configuration
| Field | Type | Required | Description |
|---|---|---|---|
| Instance URL | Text | Yes | Your org’s My Domain URL, e.g. https://yourcompany.my.salesforce.com. Auto-filled on connect. Must start with https://. |
| API Version | Select | No | Salesforce REST API version, v59.0 through v66.0. Default: v59.0. A newer version exposes fields added in later releases; it must not be newer than your org supports. |
| Start Date | Text (YYYY-MM-DD) | No | Lower bound for the first sync of each incremental stream: only records whose cursor is on or after this date (UTC) are read. Ignored once a stream has a cursor. Leave empty to load the full history. |
| Include deleted records | Toggle | No | Default: on. Reads soft-deleted records (those in the Recycle Bin) as rows with IsDeleted = true. See Deleted records. |
| Include custom fields | Toggle | No | Default: on. Fills the CustomFields JSON column with the object’s custom (__c) fields; off, the column is NULL. See Custom fields. |
| Include email HTML body | Toggle | No | Default: off. Fills EmailMessage.HtmlBody, which is often large; off, the column is NULL. TextBody is always loaded. |
Target schema, stream selection, sync mode (per stream — see Sync Modes) and schedule are set on the loader as for every connector — see Creating a Loader.
Available Streams
Each stream is named after its Salesforce object and lands in a table of the same name. Every stream uses Id as its primary key. Default sync mode is the mode a stream is given when you create the loader; you can change it per stream (see Sync Modes). The streams selected by default are Case, CaseComment, CaseHistory, EmailMessage, Contact, Account and User.
| Stream | What it holds | Cursor | Read | Default sync mode | Deletes captured | Requires |
|---|---|---|---|---|---|---|
Case | Support cases: status, priority, origin, owner, contact and account, entitlement and milestone state | SystemModstamp | Incremental | Merge | Yes | — |
CaseComment | Public and internal comments on cases | SystemModstamp | Incremental | Merge | Yes | — |
CaseHistory | Field-level change history of tracked Case fields (old and new value, who, when) | CreatedDate | Incremental (append-only object) | Append | Yes | Field history tracking on Case |
CaseContactRole | Contacts linked to a case in a named role | SystemModstamp | Incremental | Merge | Yes | — |
CaseTeamMember | Users and contacts on a case’s team, with their team role | — | Full read every run | Full refresh | No — see below | Case Teams |
CaseMilestone | Milestones on each case: target and completion dates, violation state | — | Full read every run | Full refresh | No — see below | Entitlement Management |
EmailMessage | Emails sent and received on cases: addresses, subject, text body (HTML body optional), status | SystemModstamp | Incremental | Merge | Yes | Email-to-Case or Enhanced Email |
Entitlement | Support entitlements: type, dates, entitlement process | SystemModstamp | Incremental | Merge | Yes | Entitlement Management |
ServiceContract | Service contracts and their terms | SystemModstamp | Incremental | Merge | Yes | Entitlement Management |
Asset | Products and installations owned by accounts and contacts | SystemModstamp | Incremental | Merge | Yes | — |
Account | Customer organisations | SystemModstamp | Incremental | Merge | Yes | — |
Contact | Customer people | SystemModstamp | Incremental | Merge | Yes | — |
User | Agents, admins and other users (deactivated users are kept, with IsActive = false) | SystemModstamp | Incremental | Merge | Not applicable — users are deactivated, never deleted | — |
Group | Queues and public groups, distinguished by the Type column | SystemModstamp | Incremental | Merge | Not applicable | — |
MessagingSession | Conversations over Messaging channels: channel, status, agent, linked case, start and end times | SystemModstamp | Incremental | Merge | Yes | Messaging |
MessagingEndUser | The customer identity on a Messaging channel (phone number, PSID, …) linked to a contact | SystemModstamp | Incremental | Merge | Yes | Messaging |
LiveChatTranscript | Legacy chat transcripts and their metadata | SystemModstamp | Incremental | Merge | Yes | Legacy Chat (historical) |
VoiceCall | Calls handled through Service Cloud Voice: direction, duration, queue, agent, linked case | SystemModstamp | Incremental | Merge | Yes | Service Cloud Voice |
AgentWork | Omni-Channel work items routed to agents: assignment, accept and close times, handle time | SystemModstamp | Incremental | Merge | Not applicable | Omni-Channel |
Knowledge | Knowledge article versions, with PublishStatus and IsLatestVersion | SystemModstamp | Incremental | Merge | Not applicable | Lightning Knowledge |
“Incremental” means the stream reads only records whose cursor moved since the last run. “Full read every run” means the stream reads the whole object every run, whatever its sync mode, because Salesforce gives it no change timestamp that can be trusted:
CaseTeamMemberrows are hard-deleted when someone leaves a case team — they never go to the Recycle Bin, so neither an incremental read nor the Include deleted records option can see the removal.CaseMilestoneelapsed and remaining-time values change as time passes without Salesforce updating the row’sSystemModstamp, so an incremental read would keep stale values forever.
That is why both default to Full refresh: each run replaces the table, so it always matches Salesforce exactly — removed team members disappear and milestone times are current — while the loader’s other streams keep running incrementally. If you change their mode, under Incremental (merge on key) each run upserts the current rows on Id, so values stay current but a team member removed in Salesforce stays in the table; under Incremental (append) each run appends another full copy of the object, so the table grows by its whole size every run.
Knowledge
Lightning Knowledge stores articles in an object whose name the org chooses when Knowledge is enabled — usually Knowledge__kav, but the prefix can be renamed. The Knowledge stream resolves the actual object at the start of every run (the first queryable object whose name ends in __kav) and lands it in a table called Knowledge. Every article version is loaded — drafts, published and archived — so filter on PublishStatus = 'Online' and IsLatestVersion = true for the live version of each article.
Salesforce only accepts an article-version query that filters on a single publish status, so each run reads Knowledge as three queries — Online, Draft and Archived — and saves the stream’s cursor (the newest SystemModstamp seen) only when all three have finished. A Knowledge run that fails partway therefore re-reads from the previous run’s cursor; under merge mode the re-read rows collapse to one row per Id.
Columns
Each stream lands a fixed set of columns: the object’s commonly used standard fields, including Id, CreatedDate, CreatedById, LastModifiedDate, LastModifiedById, SystemModstamp and IsDeleted where the object has them, plus the CustomFields column (and HtmlBody on EmailMessage). The column set is the same in every org, whichever features are enabled and however the toggles are set, so the table never has to change shape:
- A field your org does not have (a feature is off, the field was added in a newer API version than the one configured, or the integration user cannot see it) is loaded as
NULL. - Compound
addressandlocationfields are loaded as their individual components (street, city, postal code, …) instead. - Binary (base64) fields, multi-record ID list fields, and write-only fields are not loaded.
Sync Modes
| Mode | Supported |
|---|---|
| Full Refresh | Yes |
| Incremental (append) | Yes |
| Incremental (merge on key) | Yes |
The sync mode is set per stream. A new loader gives each stream the Default sync mode listed in Available Streams, and the stream list shows it pre-selected; change any stream’s mode before or after you save. Streams you do not set follow the loader’s default sync mode. See Loaders → Sync mode per stream for how the two settings combine.
The defaults follow the shape of each object:
| Default | Streams | Why |
|---|---|---|
| Incremental (merge on key) | Case, CaseComment, CaseContactRole, EmailMessage, Entitlement, ServiceContract, Asset, Account, Contact, User, Group, MessagingSession, MessagingEndUser, LiveChatTranscript, VoiceCall, AgentWork, Knowledge | Records that are edited after they are created. A case updated ten times between runs still lands once per run, and the table holds one row per Id with its latest values, including IsDeleted = true once a record is deleted. |
| Incremental (append) | CaseHistory | Each row is a field change that Salesforce never edits afterwards, so appending new rows is exact and cheaper than a merge. |
| Full Refresh | CaseTeamMember, CaseMilestone | No trustworthy change timestamp: they are read in full every run anyway, and only a full refresh drops removed team members and keeps milestone times current (see Available Streams). |
To keep a change log of a mutable object instead — one row per run in which a case changed — set that stream to Incremental (append). Avoid Incremental (append) on CaseTeamMember and CaseMilestone: with no cursor, every run appends a complete copy (the stream list warns when you choose it). Full Refresh on a stream that does have a cursor re-reads the whole object every run, which costs far more API calls on a large org — see API usage and rate limits.
Loaders created before per-stream sync modes existed keep running every stream in the loader’s single mode until you edit them; to adopt the defaults above on such a loader, set each stream’s mode in the stream list and save.
Deleted Records
With Include deleted records on (the default), streams for objects that have an IsDeleted field are read with Salesforce’s queryAll endpoint instead of query. queryAll also returns records in the Recycle Bin, so a deleted case arrives as a row with IsDeleted = true — and, in merge mode, updates the existing row in place. Filter on IsDeleted = false in your models to see only live records.
Salesforce keeps deleted records in the Recycle Bin for 15 days (less if the bin fills up or someone empties it). A record purged from the Recycle Bin before the loader’s next run is gone from Salesforce entirely, and that run never sees its deletion. Schedule the loader to run at least every 15 days — in practice daily or more often — so every deletion is captured.
Streams marked “Not applicable” above have no IsDeleted field: users are deactivated, and queues, groups, Omni-Channel work and article versions are not soft-deleted. CaseTeamMember and CaseMilestone are covered in Available Streams.
With the option off, every stream uses query, which never returns deleted records; rows already loaded for records later deleted stay in the table unchanged.
Custom Fields
Every stream’s table has a CustomFields column. With Include custom fields on (the default), it holds a JSON object of every custom field (__c) on the object that the integration user can read, keyed by the field’s API name, with values typed as JSON (numbers, booleans, strings, null). For example:
{
"Escalation_Reason__c": "Billing dispute",
"Severity_Score__c": 7,
"Requires_Callback__c": true
}CustomFields is NULL when the option is off, and when the object has no custom fields the user can read.
A JSON column lets the loader capture whatever custom fields your org has — and fields added later — without changing the table’s columns. Extract the fields you need in a model or a prepared table, for example JSON_VALUE(CustomFields, '$.Severity_Score__c') on BigQuery or CustomFields:Severity_Score__c on Snowflake.
Some custom fields are deliberately left out:
| Excluded | Why |
|---|---|
| Formula (calculated) fields | Salesforce computes them when they are read and does not update a record’s SystemModstamp when a formula’s result changes, so under incremental sync their values would silently go stale. Large numbers of formula fields also make Salesforce reject queries as QUERY_TOO_COMPLICATED. Recreate the formula in a model over the loaded fields instead. |
| Binary (base64) fields | Not loadable through the query API. |
| Compound address and location fields | Their component fields are included instead. |
Salesforce limits a query request URL to about 16 KB. An object with hundreds of custom fields can exceed that, so custom fields are added in alphabetical order of API name until the query reaches roughly 14 KB; any remaining fields are left out of CustomFields and the run logs a warning naming them. If a field you need is omitted, ask your Salesforce admin whether unused custom fields on that object can be deleted, or take that field into your warehouse another way.
How It Works
For each selected stream, each run:
- Checks what the org exposes. The loader describes the object once per run and queries only the declared columns the describe actually returns. A field the org lacks — a disabled feature, an older API version, field-level security — is left out of the query and loaded as
NULL, instead of failing the whole query withINVALID_FIELD. Because the column set is fixed per table, the table’s shape never changes when a feature or a permission changes. - Bounds the window. An incremental read asks for records whose cursor is after the last run’s saved cursor (or on or after Start Date for a stream’s first run), and no later than two minutes before the run started. Salesforce timestamps have one-second precision and a record can be committed in the same second the loader reads; holding back the last two minutes means such a record is picked up by the next run instead of being skipped.
- Reads in a stable order. Records are requested ordered by the cursor, then by
Id, and paged through Salesforce’s result pages (up to 2,000 records per page), each page written to the warehouse as it arrives. - Saves a cursor that cannot skip records. Many records can share one cursor timestamp (a bulk update touches thousands in the same second). The cursor saved after each page is the last timestamp the page has fully delivered, so a run that stops halfway through a group of records sharing one timestamp resumes at the start of that group rather than after it. A resumed run may re-read a few records; under merge mode they collapse to one row. (
Knowledgesaves its cursor once per run instead — see Knowledge.)
The loader pipeline handles everything around the read: batching writes into the warehouse, retrying a failed stream, persisting the cursor between runs, and refreshing the OAuth token — including a forced refresh and retry when Salesforce reports INVALID_SESSION_ID mid-run.
Every row also carries the platform columns _ss_loaded_at and _ss_run_id, as for every pull loader — see Loaders → Reserved columns.
Schema Mapping
Salesforce field types are mapped to warehouse types as follows:
| Salesforce Type | Warehouse Type | Notes |
|---|---|---|
id, reference | STRING / VARCHAR | 18-character Salesforce ID |
string, textarea, email, phone, url | STRING / VARCHAR | |
picklist | STRING / VARCHAR | The picklist value’s API name |
multipicklist | STRING / VARCHAR | Selected values separated by ; |
anyType | STRING / VARCHAR | Rendered as text (used by history objects’ old and new values) |
boolean | BOOLEAN | |
int | BIGINT (INT64 on BigQuery) | |
double, currency, percent | DOUBLE (FLOAT64 on BigQuery) | Currency is in the record’s currency; percent is the displayed number (12.5, not 0.125) |
date | DATE | |
datetime | TIMESTAMP | Normalised to UTC |
| Custom fields | STRING (BigQuery, Databricks) / VARIANT (Snowflake) | The CustomFields JSON column |
API Usage and Rate Limits
The loader uses the Salesforce REST API, which counts against your org’s daily API request allocation — for Enterprise and Unlimited editions, 100,000 requests per rolling 24 hours plus an allowance per user licence. Each run of each stream costs about:
- one request per 2,000 records read, plus
- one describe request for the object (two for
Knowledge, which first looks up the article object, andKnowledgeruns three queries — one per publish status — so at least three calls more).
An hourly loader over the seven default streams with a few thousand changes an hour uses a few hundred requests a day. The expensive run is the first one, which reads each object’s full history — a stream with 10 million cases costs about 5,000 requests. Use Start Date to limit the backfill if your allocation is tight, and keep streams that have a cursor on an incremental sync mode rather than Full Refresh.
To see where you stand, check Setup → Company Information → API Requests, Last 24 Hours, or the Sforce-Limit-Info response header (api-usage=used/limit) on any API call. When the allocation is exhausted Salesforce rejects requests with REQUEST_LIMIT_EXCEEDED until usage falls back under the limit; the run fails and the next scheduled run retries.
Salesforce also limits an org to 25 concurrent long-running requests (requests taking longer than 20 seconds) — 5 in Developer Edition and trial orgs. Queries over very large objects with no Start Date can take long enough to count; avoid scheduling several loaders, plus other integrations, to start full-history reads at the same moment.
Troubleshooting
| Issue | Solution |
|---|---|
INVALID_SESSION_ID, or HTTP 401 that persists | Zeotap refreshes the token and retries once automatically. If runs keep failing, the refresh token has been revoked (password reset, connected app revoked by an admin, refresh-token policy). Click Reconnect on the loader’s detail page. |
REQUEST_LIMIT_EXCEEDED | Your org’s daily API allocation is used up — by this loader or by other integrations. Run the loader less often, deselect streams you do not need, use a Start Date for large backfills, or buy additional API capacity from Salesforce. The next run retries automatically. |
”<Object> is not available in this org (feature … not enabled or the user lacks access)“ | The object does not exist in your org, or the integration user cannot read it. Enable the feature listed in Feature requirements per object and grant the user Read on the object, or deselect the stream. |
API_DISABLED_FOR_ORG | Your Salesforce edition does not include API access (for example Professional Edition without the API add-on). Upgrade the edition or buy the add-on. |
API_CURRENTLY_DISABLED (“API is disabled for this User”) | The integration user lacks the API Enabled permission. Add it through the user’s profile or a permission set, then reconnect. |
HTTP 404 on the connectivity test or on every stream | The configured API Version is newer than your org supports (orgs are upgraded release by release). Choose an older version — v59.0 works everywhere. |
QUERY_TIMEOUT on the first run of a large stream | Salesforce stops a query that runs too long. Set a Start Date so the first run reads a bounded range of history, or move it later; incremental runs after the backfill are small. |
A custom field is missing from CustomFields | Check, in order: the integration user’s field-level security on the field (unreadable fields are skipped); whether it is a formula field (excluded by design); whether the object has so many custom fields that the field fell past the query-length budget (the run log names omitted fields). |
A field is NULL in every row | The org does not have it (feature disabled), the configured API version predates it, or field-level security hides it from the integration user. |
| Deleted records never appear | Check that Include deleted records is on, and that the loader runs at least every 15 days: records purged from the Recycle Bin before the next run cannot be seen. Records deleted before the loader’s first run are only visible while still in the Recycle Bin. |
A case’s ContactEmail or ContactPhone is out of date | These fields on Case are read from the related contact; editing the contact does not update the case’s SystemModstamp, so an incremental read does not re-read the case. Join Case.ContactId to the Contact stream for current contact details. The same applies to any field Salesforce derives from a related record. |
| ”… not a Salesforce datetime; reset the stream’s cursor” | The stream’s saved cursor is not a Salesforce datetime, so the run stops rather than guess where to resume. Contact Zeotap support to reset the stream’s cursor; the next run then starts from Start Date, or the full history if none is set. |
| Records changed in the last couple of minutes are not in the table yet | Expected: each run stops two minutes before its own start time so it never misses a record committed mid-read. They arrive with the next run. |
| Connecting to a sandbox org fails | Sandbox orgs are not supported yet: sign-in always goes through login.salesforce.com, which does not accept sandbox credentials. Connect a production or Developer Edition org. |
Next Steps
- Create a model over the
Casetable, and relate it toContactandAccount - Build support metrics — first response time, time to resolution, SLA attainment — from
Case,CaseHistoryandCaseMilestone - Build an audience from customers with open escalated cases
- Sync data back to Service Cloud with the Service Cloud destination
- Load Sales Cloud objects from the same org with the Salesforce loader