SOURCE_ID: import_from_salesforce NAME: Import Salesforce Records CATEGORY: CRM Pull records of any Salesforce object (Account, Contact, Lead, Opportunity, custom objects, …) into Floqer — either as a one-time import or as an ongoing source that re-pulls on a schedule. Imported records become available to your Floqer workflows for enrichment, scoring, outreach, and the like. 1. ENDPOINTS Source identifier (used in every endpoint path): `import_from_salesforce`. POST /api/v1/sources/import_from_salesforce/preview Scope: sources:read Returns a sample of the records that WOULD be imported for the given body, without creating anything. Use this to validate the object, fields, and filters before committing. POST /api/v1/sources/import_from_salesforce Scope: sources:write Creates the source, imports matching records immediately (when `pull_existing`), and — for an active source — keeps re-importing on a schedule. Returns `source_instance_id` (the new source's UUID). POST /api/v1/sources/import_from_salesforce/options/ Scope: sources:read Resolves dynamic option values for a field. Two `field_name` values matter: `salesforce_objects` and `salesforce_properties` (see §6). GET /api/v1/sources//data Scope: sources:read Paginated rows imported into the created source. `` is the UUID returned by Create. Query: `page_no` (default 1), `page_size` (default 20, max 200). Source-agnostic; see concepts.txt §10. POST /api/v1/sources//sync Scope: sources:write Connects the created source to a workflow and (by default) backfills it with the source's current rows. `` here is the UUID returned by Create. Body: { workflow_id, field_mapping, push_existing?, run? } — `field_mapping` keys are `input.` references on the target workflow, values are the source fields to pull (the Salesforce field API names you imported). This step is source-agnostic; see concepts.txt §10 for the full shape. 2. OBJECT + FIELDS Unlike a list-based source, Salesforce Pull Data selects ONE object and a set of fields on it: object required. The Salesforce object API name — "Account", "Contact", "Lead", "Opportunity", or a custom object like "My_Object__c". Resolve the catalogue via the `salesforce_objects` dynamic option (§6). properties required, non-empty. The field API names to SELECT and return on each row — e.g. ["Id", "Name", "Industry", "Website", "CreatedDate"]. Resolve the field catalogue for the chosen object via `salesforce_properties` (§6). Field names are validated against the object server-side; any field that doesn't exist on the object is rejected with a 400 listing the offending names. Relationship fields (dot paths like "Owner.Name") are supported and are not field-existence-checked (they reference fields on related objects). 3. PULL MODE (ONE-TIME VS ONGOING) `pull_mode` is required on the create endpoint (not on preview). Values (same as every CRM source): "static" One-time import. Imports matching records once (when `pull_existing`) and stops — no recurring schedule. `max_count` caps the import size. "active" Ongoing source. Imports matching records initially (when `pull_existing`) AND re-imports records created/modified since the last run on the cadence in `schedule` (a cron string), continuing until `expiration_date`. `max_count` is ignored for an active source. `schedule` is required when `pull_mode: active`. Otherwise it's ignored. 4. BODY SHAPE (PREVIEW + CREATE) Preview accepts (all snake_case): object required: Salesforce object API name properties required: array of field API names (non-empty) filters optional: filter expression (see below + §5). Omit to pull every record of the object. properties_metadata optional: array<{name, label}> — sets the `label` on each returned cell. Every cell comes back as {label, value} regardless; with metadata the label is the human label, without it the label is the field's API name. max_count optional: integer | string (preview always samples) Create accepts all the preview fields plus: name required: display name for the new source pull_mode required: "static" | "active" schedule cron string; required when pull_mode === "active" expiration_date ISO date (YYYY-MM-DD); optional (active source only) pull_existing boolean; optional, defaults to true max_count cap on rows imported (static only). Optional. Omit: imports every matching record (up to the source-wide ceiling of 50,000). Set to N: caps the import at N rows. N MUST NOT exceed the Preview's `metadata.total_results` — Recommended: set it equal to `metadata.total_results`, or omit to import all matches. Do not pick generic "safe" values like 100. Unknown top-level keys are rejected with 400 — the schema is strict. Each returned row also carries a `uid` field — the record's Salesforce `Id`, surfaced as a plain string (not wrapped as `{label, value}`). Use it as the stable per-row identifier; it appears in both Preview and Get Source Data rows. Filter expression shape (compiled into a SOQL WHERE clause): filters: { operator?: "AND" | "OR", // combinator between groups (default AND) conditions: [ // array of GROUPS { operator?: "AND" | "OR", // joins this group to the previous one conditions: [ // array of LEAVES { variable: "", condition: "", variable_type: "", values: [, ...], operator?: "AND" | "OR", // joins this leaf to the previous one case_sensitive?: boolean } ] } ] } - A leaf's `condition` is the comparison operator; the valid set depends on `variable_type` (§5). - A leaf's `operator` ("AND"/"OR") joins it to the PREVIOUS leaf in the same group (the first leaf's `operator` is ignored). Group `operator` works the same way between groups. - `values` is always an array. Single-value operators read `values[0]`; `is between` reads `values[0]` (low) and `values[1]` (high); `is` / `is not` with multiple values become SOQL `IN` / `NOT IN`. Example (Name contains "uni"): { "object": "Account", "properties": ["Id", "Name", "Industry", "Website", "CreatedDate"], "filters": { "conditions": [ { "conditions": [ { "variable": "Name", "condition": "contains", "variable_type": "string", "values": ["uni"] } ]} ] } } 5. FILTER OPERATORS BY FIELD TYPE `condition` is interpreted according to the leaf's `variable_type`. Set `variable_type` to the family that matches the Salesforce field — look at the field's type via `salesforce_properties` (`.extras.type`) and map it to one of the families below. (For numeric fields use `number`; for picklists use `select`.) ──────────────────────────────────────────────────────────────────── variable_type: string (string, textarea, email, url, phone, id, …) ──────────────────────────────────────────────────────────────────── is / equals field = 'value' (multiple values → IN (...)) is not field != 'value' (multiple values → NOT IN (...)) contains field LIKE '%value%' (any of the values → OR) does not contain NOT (field LIKE '%value%') (AND across values) starts with field LIKE 'value%' ends with field LIKE '%value' does not start with NOT (field LIKE 'value%') does not end with NOT (field LIKE '%value') is empty field = null is not empty field != null ──────────────────────────────────────────────────────────────────── variable_type: number (number, currency, percent) ──────────────────────────────────────────────────────────────────── is / equals field = value is not field != value greater than field > value less than field < value greater than or equal to field >= value less than or equal to field <= value is between field >= values[0] AND field <= values[1] is empty field = null is not empty field != null ──────────────────────────────────────────────────────────────────── variable_type: date / datetime ──────────────────────────────────────────────────────────────────── is field = is not field != is after field > is before field < is between field >= values[0] AND field <= values[1] is empty field = null is not empty field != null For `date` send "YYYY-MM-DD"; for `datetime` an ISO timestamp is used. ──────────────────────────────────────────────────────────────────── variable_type: boolean ──────────────────────────────────────────────────────────────────── is / equals field = value (value: true | false) is not field != value is empty field = null is not empty field != null ──────────────────────────────────────────────────────────────────── variable_type: select (picklist / multi-select) ──────────────────────────────────────────────────────────────────── is / equals field = 'value' is not field != 'value' contains field INCLUDES ('value') (multi-select) does not contain field EXCLUDES ('value') (multi-select) is empty field = null is not empty field != null Valid picklist values are in the field metadata's `extras.picklistValues[].value` — discoverable via `salesforce_properties`. Values are escaped server-side before being placed in the SOQL query. 6. DYNAMIC OPTIONS (MANDATORY PRE-CALLS) Two dynamic-options field names are available for this source. Both are POSTs to /api/v1/sources/import_from_salesforce/options/ with body `{ "context"?: {...} }`. salesforce_objects Context: none. Returns: {value: , label: , extras: {...}} Use the returned `value` directly as the source body's `object`. salesforce_properties Context: { "salesforce_object": "" } — REQUIRED. Returns: {value: , label: , extras: } Use the `value`s inside `properties[]`. For `properties_metadata`, send `{name: value, label: label}` per field (name + label only — the full metadata is not needed). Use `extras.type` to choose each filter leaf's `variable_type`, and `extras.picklistValues` for valid picklist values. Connection check: every endpoint for this source verifies the API-key user has an active Salesforce connection first. Missing connection → 424. 7. HOW TO CONFIGURE END-TO-END Typical flow for an AI agent building a Salesforce Pull Data source: Step 1 — Pick the object POST /api/v1/sources/import_from_salesforce/options/salesforce_objects Body: {} Choose the object's `.value` (e.g. "Account"). Step 2 — Resolve the field catalogue for that object POST /api/v1/sources/import_from_salesforce/options/salesforce_properties Body: {"context": {"salesforce_object": "Account"}} Pick a subset of `.value`s as `properties`. Build `properties_metadata` as `{name: .value, label: .label}` per field. Note `.extras.type` for any field you plan to filter on (→ the filter `variable_type` in §5). Step 3 — Preview before committing POST /api/v1/sources/import_from_salesforce/preview Body: { "object": "Account", "properties": ["Id", "Name", "Industry", "Website", "CreatedDate"], "properties_metadata": [{"name": "Name", "label": "Account Name"}, ...], "filters": { "conditions": [{ "conditions": [ {"variable": "Name", "condition": "contains", "variable_type": "string", "values": ["uni"]} ]}]} } Check the returned `data[]` and `metadata.total_results`. Iterate until happy — preview never creates anything. Step 4 — Create the source POST /api/v1/sources/import_from_salesforce Body: + the create-only fields: "name": "University Accounts", "pull_mode": "static" | "active", "schedule": "0 12 * * *", // only when pull_mode=active "expiration_date": "2027-01-01", // optional, active only "pull_existing": true, // optional, defaults true "max_count": // MUST NOT exceed Step 3's // metadata.total_results If Step 3 returned // total_results: 13, use max_count: 13 // (or omit to import all matches). // Don't pick 100 "to be safe". Response: { "status": 201, "data": { "source_instance_id": "", "name", "created_at" }} Step 4b — (Optional) Poll imported rows while the backfill runs GET /api/v1/sources//data?page_no=1&page_size=20 ( = UUID from Step 4) Row keys match Preview (field API names you imported). `total_count` grows until import finishes. Step 5 — Sync the source into a workflow (so its records flow downstream) POST /api/v1/sources//sync ( = the UUID from Step 4 ) Build `field_mapping` (two lookups): a. GET /api/v1/workflows → pick the destination workflow_id. b. GET /api/v1/workflows//sheets//inputs → each input has a `reference` like `{{input.email}}`. c. Map each workflow input (reference WITHOUT braces) to a source field — the source fields are the Salesforce field API names you imported (Step 2 / the preview `data[]` keys). Body: { "workflow_id": "", "field_mapping": { "input.account_name": "Name", "input.website": "Website" }, "push_existing": true, // optional, default true "run": "all" } // optional, default "all" (none | first_10 | all) See concepts.txt §10 for the full sync semantics (source-agnostic). The initial import runs asynchronously — the create call returns as soon as the source is created and the import has been queued, not when records have finished arriving. 8. KEY NOTES - Salesforce connection is mandatory. If the API-key user has no active Salesforce connection, every endpoint returns 424 ("User is not connected to 'salesforce'."). Surface this as a "connect Salesforce first" CTA. - Fields are validated against the object. Sending a field that doesn't exist on the object returns a 400 listing the offending field names — run `salesforce_properties` for the object to get valid names. - `filters` is optional. Omit it to pull every record of the object (capped by `max_count` on a static source). - `variable_type` drives operator interpretation. Pick it from the field's `extras.type` family (§5). The wrong family can produce an unexpected SOQL comparison; numeric fields should use `number`, picklists `select`. - Total counts are best-effort. `metadata.total_results` comes from a Salesforce COUNT() query and reflects the current matching set. - Credits are consumed when the source is created. If credit consumption fails (e.g. insufficient balance), the source is still created but its import can't start; the API returns 402 so you can top up and retry. - `properties_metadata` is optional and only needs `{name, label}`. Each returned cell is `{label, value}`; with metadata the `label` is the human label, without it the label falls back to the field's API name. Field VALUES are returned either way — metadata only controls the labels. - `max_count` is honored for a static source and ignored for an active one (an active source stays in sync indefinitely). On a static source `max_count` MUST NOT exceed the Preview's `metadata.total_results` Set it equal to the Preview total, or omit to import every match (up to the source-wide 50,000 ceiling). 9. WHERE IT FITS UPSTREAM (in Salesforce) Records of the chosen object (Accounts, Contacts, Leads, …) live in the user's Salesforce org, with their field values and relationships. THIS SOURCE Creates a Salesforce Pull Data source. On creation it imports matching records immediately (when `pull_existing`); an active source also re-imports created/modified records on the `schedule` cron until `expiration_date`. DOWNSTREAM (in Floqer) Imported records become available to your Floqer workflows — wire the source into a workflow to enrich, score, or run outreach on each record. 10. WHEN TO USE - One-time import of a filtered set of Salesforce records for enrichment / outreach: `pull_mode: static` with a `filters` expression. - Keep a Salesforce segment (e.g. "Accounts in the Technology industry") in sync with a Floqer workflow on a schedule: `pull_mode: active`, `schedule: "0 12 * * *"`. - Pull an entire object (no `filters`) as a one-time snapshot, capped with `max_count`. When you instead want to push data OUT of Floqer INTO Salesforce, use the Salesforce action templates (e.g. `salesforce_create_record`, `salesforce_upsert_record`) inside a workflow — see https://floqer.com/docs/action-detail/salesforce_create_record.txt. Last updated: 2026-05-26.