Skip to content

Salesforce Sensor

What it ingests

CRM records and the activity stream from a Salesforce org:

Salesforce objectOntology entitySignal
AccountOrganizationaccount
ContactPerson (identity)contact
OpportunityProjectopportunity
CaseIssuecase
NoteCommentnote
EmailMessageCommentemail
Task (TaskSubtype = Call)Commentcall
EventMeetingmeeting
Task (everything else)ActionItemtask

The engagements are the point. Plenty of systems know which opportunities are open; what Salesforce adds is the record of what was actually said to the customer — note bodies, email bodies, call notes, meeting descriptions. Each activity is resolved to the opportunity, account, case, or contact it concerns.

Read-only and poll-based. Each object kind has its own signal, so you can disable a noisy stream (emails, typically) from the source's bindings without disabling the source.

Credential

References a salesforce_connected_app credential by id. The secret is a JSON blob:

json
{
  "login_url": "https://mycompany.my.salesforce.com",
  "client_id": "<Connected App Consumer Key>",
  "client_secret": "<Connected App Consumer Secret>"
}

login_url is your My Domain (production, sandbox, or scratch org). login.salesforce.com and test.salesforce.com also work. After the token mint, Salesforce returns the instance_url used for subsequent REST calls, so the instance is not stored.

OAuth web-server, username-password, and JWT-bearer are not supported. Client credentials is the server-to-server grant: no browser, no refresh loop, no callback endpoint.

bash
curl -sS -X POST https://your-corveil-host/api/credentials \
  -H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
  -d '{"type":"salesforce_connected_app","name":"acme-salesforce","secret":"{\"login_url\":\"https://mycompany.my.salesforce.com\",\"client_id\":\"3MVG9…\",\"client_secret\":\"…\"}"}'

Config fields

KeyRequiredNotes
enabled_objectsnoComma-separated object kinds. Blank polls every supported kind. Unknown names are rejected at configure time.
default_entity_typenoOverride the graph entity type produced from each record. Blank uses the per-object fallback (Organization for accounts, Project for opportunities, …). Validated against the org taxonomy on save.

Supported object names: accounts, contacts, opportunities, cases, notes, emails, calls, meetings, tasks.

Provider-side setup

  1. In Salesforce Setup, go to App Manager → New Connected App (or External Client App).
  2. Enable OAuth Settings and Client Credentials Flow.
  3. Assign a run-as user with API access (a dedicated integration user is best — the sensor inherits that user's object permissions).
  4. Add the api OAuth scope (Access and manage your data).
  5. Copy the Consumer Key (client_id) and Consumer Secret.

The run-as user needs read access to every object kind you enable. A missing object permission fails that kind's walk and continues with the rest — it does not park the whole sensor.

Wire-up

POST /api/sensors

json
{
  "sensor_type": "salesforce",
  "name": "Acme Salesforce",
  "config": {
    "enabled_objects": "accounts, contacts, opportunities, notes, emails, calls, meetings"
  },
  "credentials": { "connected_app": "<salesforce_connected_app credential id>" }
}

Verify

Run Test connection. It mints a token via client credentials and reads /services/oauth2/userinfo. A failure here is almost always: Client Credentials Flow not enabled, a missing run-as user, or the api scope.

Once polling, each object kind's count appears in the per-poll summary log (accounts_collected, emails_collected, …). A kind stuck at zero while the others move is the signal to check that kind's object permission on the run-as user.

How incremental sync works

  • SOQL on LastModifiedDate, oldest first. GET /services/data/v62.0/query with ORDER BY LastModifiedDate ASC, Id ASC. Query-locator pagination drains a page of 200; a burst beyond the per-poll page cap spills into the next cycle via the watermark.
  • Each object kind has its own watermark, so enabling a new kind backfills it from its own zero without dragging the others back.
  • Each poll re-reads a two-minute overlap. LastModifiedDate is second-precision on many objects; a strict resume would skip a record committed in the same second as the watermark. Re-read events are deduplicated.
  • Deletes are invisible. The sensor uses /query, not /queryAll, so deleted and archived rows never appear. A record vanishing from results is not treated as a deletion.
  • Calls and tasks are disjoint Task queries. Logged calls are Task WHERE TaskSubtype = 'Call'; everything else on Task is the task signal. An operator can disable one without losing the other.
  • Rate limits are shared. Salesforce's daily API request budget is shared with every integration in the org. Daily exhaustion (REQUEST_LIMIT_EXCEEDED) leaves the cursor unadvanced and backs off for the cycle rather than retrying.

Not included

  • OAuth web-server / refresh tokens. Only needed for user-interactive apps.
  • Username-password. Salesforce has deprecated that grant.
  • JWT bearer. Requires a certificate; client credentials is the same server-to-server posture without one.
  • Enhanced notes (ContentNote). v1 walks classic Note. Orgs that have disabled classic notes will see notes_collected stay at zero.
  • Leads, Campaigns, custom objects, Chatter. Deferred.
  • Bulk API 2.0. REST query is enough for incremental deltas; bulk is a backfill optimization.
  • Webhooks / Platform Events / Change Data Capture. Polling is the supported path.