SOURCE_ID: import_from_attio NAME: Import Attio Records CATEGORY: CRM Pull records of any Attio object (People, Companies, Deals, Users, Workspaces, custom objects, …) into Floqer — either as a one-time import or as an ongoing source that keeps importing on a schedule. Imported records become available to your Floqer workflows for enrichment, scoring, outreach, and the like. INDEX: 1. Endpoints 2. Object + attributes 3. Pull mode (one-time vs ongoing) 4. Body shape (preview + create) 5. Filter operators by attribute type 6. Dynamic options (mandatory pre-calls) 7. How to configure end-to-end 8. Key notes 9. How updates reach the list 10. Where it fits 11. When to use ================================================================================ 1. ENDPOINTS ================================================================================ Source identifier (used in every endpoint path): `import_from_attio`. POST /api/v1/sources/import_from_attio/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, attributes, and filters before committing. POST /api/v1/sources/import_from_attio Scope: sources:write Creates the source and queues the import. Returns `source_instance_id` (the new source's UUID). POST /api/v1/sources/import_from_attio/options/ Scope: sources:read Resolves dynamic option values for a field. Two `field_name` values matter: `object` and `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. Body: { workflow_id, field_mapping, push_existing?, run? } — `field_mapping` keys are `input.` references on the target workflow, values are the source fields (the Attio attribute api_slugs you imported). See concepts.txt §10 for the full shape. PATCH /api/v1/sources//status Scope: sources:write Pause or resume an ongoing source. Pausing also removes the Attio webhook (§9), so a paused source stops ingesting entirely. ================================================================================ 2. OBJECT + ATTRIBUTES ================================================================================ An Attio import selects ONE object and a set of attributes on it: object required. The Attio object api_slug — "people", "companies", "deals", "users", "workspaces", or a custom object's slug. Resolve the catalogue via the `object` dynamic option (§6). properties required, non-empty. The attribute api_slugs to import as columns — e.g. ["name", "email_addresses", "job_title", "created_at"]. Resolve the catalogue for the chosen object via `properties` (§6). Read-only attributes (created_at, formula fields) are importable. Attio's query API returns every attribute of every matching record — it has no field selection — so `properties` selects which of them become columns on the Floqer row rather than what is fetched. Each returned row carries a `uid` field — the Attio record id, surfaced as a plain string (not wrapped as `{label, value}`). Rows also always carry `attio_record_id` and `attio_record_url` regardless of the attributes selected. All three come back from Get Source Data as well as Preview, so a row can always be linked back to the record in Attio. There is NO `properties_metadata` on this source (Salesforce and HubSpot take one). Labels come from Attio. There is NO `max_count` — the full matching set is imported. ================================================================================ 3. PULL MODE (ONE-TIME VS ONGOING) ================================================================================ `pull_mode` is required on the create endpoint (not on preview): "static" One-time import. Imports matching records once (when `pull_existing`) and stops — no recurring schedule, no webhook. "active" Ongoing source. Imports matching records initially (when `pull_existing`), then keeps importing records CREATED since the last run on the cadence in `schedule`, until `expiration_date`. Edits arrive over a webhook instead — see §9. `schedule` is required when `pull_mode: active`, and ignored otherwise. `pull_existing: false` imports nothing that already exists in Attio — only records created from that point on. ================================================================================ 4. BODY SHAPE (PREVIEW + CREATE) ================================================================================ Preview accepts (all snake_case): object required: Attio object api_slug properties required: array of attribute api_slugs (non-empty) filters optional: filter expression (see below + §5). Omit to import every record of the object. 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 Unknown top-level keys are rejected with 400 — the schema is strict. Filter expression shape (translated into Attio's filter grammar and applied by Attio, so a filtered import never pages records it discards): 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: "", values: [, ...], operator?: "AND" | "OR" // joins this leaf to the previous one } ] } ] } - A leaf's `operator` 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` with multiple values becomes an Attio `$in`. - `variable_type` is accepted but not needed — the real Attio attribute type is resolved server-side and is what decides the translation. Example (companies whose domain contains "acme"): { "object": "companies", "properties": ["name", "domains", "categories", "created_at"], "filters": { "conditions": [ { "conditions": [ { "variable": "domains", "condition": "contains", "values": ["acme"] } ]} ] } } ================================================================================ 5. FILTER OPERATORS BY ATTRIBUTE TYPE ================================================================================ Attio validates the whole query, not each condition: ONE unsupported operator rejects the entire request. So a condition this source cannot express fails with 400 Attio cannot filter on: . Remove or change that condition and try again. rather than being dropped — a dropped condition would import records you filtered out. Check `.extras.type` on the `properties` options (§6) for each attribute you plan to filter on. ──────────────────────────────────────────────────────────────────── text ──────────────────────────────────────────────────────────────────── is / equals exact match (multiple values → $in) contains substring starts with prefix ends with suffix ──────────────────────────────────────────────────────────────────── domain, personal-name ──────────────────────────────────────────────────────────────────── is / equals, contains, starts with, ends with is empty / is not empty ──────────────────────────────────────────────────────────────────── email-address, phone-number ──────────────────────────────────────────────────────────────────── is / equals, contains, starts with, ends with (is empty / is not empty are NOT supported — Attio rejects $not_empty on these two types even though it accepts it on personal-name.) ──────────────────────────────────────────────────────────────────── location ──────────────────────────────────────────────────────────────────── contains, starts with, ends with (matched on the `locality` sub-field — the city) Equality is not available; latitude/longitude are not filterable in Attio at all. ──────────────────────────────────────────────────────────────────── number, rating, currency, date, timestamp ──────────────────────────────────────────────────────────────────── is / equals greater than / greater than or equal to less than / less than or equal to is after / is before (date + timestamp) is between (values[0] low, values[1] high) ──────────────────────────────────────────────────────────────────── select, status, checkbox ──────────────────────────────────────────────────────────────────── is / equals only. For select and status the value is the option title as it appears in Attio. ──────────────────────────────────────────────────────────────────── record-reference, actor-reference, interaction ──────────────────────────────────────────────────────────────────── Not filterable through this source — Attio matches these on nested record/actor ids rather than a plain value. NEGATION IS NOT SUPPORTED for any type — Attio's record-query endpoint has no `$ne` / `$not_in`, so `is not`, `does not contain`, `does not start with` and `does not end with` all fail the request. Express the inverse with a filter on the complement, or filter downstream in the workflow. Composite attributes are matched on their meaningful sub-field automatically (`name` → `full_name`, `domains` → `domain`, `email_addresses` → `email_address`, `phone_numbers` → `phone_number`, `primary_location` → `locality`), so pass a plain value. ================================================================================ 6. DYNAMIC OPTIONS (MANDATORY PRE-CALLS) ================================================================================ For `import_from_attio` the two field names that matter: object Context: none. POST /api/v1/sources/import_from_attio/options/object Body: {} → [{ value: "people", label: "Person" }, …] properties Context: { "object": "people" } — REQUIRED. POST /api/v1/sources/import_from_attio/options/properties Body: {"context": {"object": "people"}} → [{ value: "email_addresses", label: "Email addresses", extras: { type: "email-address", accepts_multiple_values: true } }, …] `extras.type` is the raw Attio attribute type — the same vocabulary §5 is keyed on. Read it before writing a filter. ================================================================================ 7. HOW TO CONFIGURE END-TO-END ================================================================================ Typical flow for an AI agent building an Attio import: Step 1 — Pick the object POST /api/v1/sources/import_from_attio/options/object Body: {} Choose the object's `.value` (e.g. "companies"). Step 2 — Resolve the attribute catalogue for that object POST /api/v1/sources/import_from_attio/options/properties Body: {"context": {"object": "companies"}} Pick a subset of `.value`s as `properties`. Note `.extras.type` for any attribute you plan to filter on (→ §5). Step 3 — Preview before committing POST /api/v1/sources/import_from_attio/preview Body: { "object": "companies", "properties": ["name", "domains", "categories", "created_at"], "filters": { "conditions": [{ "conditions": [ {"variable": "domains", "condition": "contains", "values": ["acme"]} ]}]} } Check the returned `data[]`. Iterate until happy — preview never creates anything. Step 4 — Create the source POST /api/v1/sources/import_from_attio Body: + the create-only fields: "name": "Acme companies", "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 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 Row keys match Preview (the attribute api_slugs you imported). `total_count` grows until the import finishes. Step 5 — Sync the source into a workflow POST /api/v1/sources//sync 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 Attio attribute api_slugs you imported. Body: { "workflow_id": "", "field_mapping": { "input.company_name": "name", "input.domain": "domains" }, "push_existing": true, // optional, default true "run": "all" } // optional, default "all" 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 ================================================================================ - Attio connection is mandatory. If the API-key user has no active Attio connection, every endpoint returns 424 ("User is not connected to 'attio'."). Surface this as a "connect Attio first" CTA. - One unsupported filter operator fails the whole request (§5). The 400 names the offending condition — fix or drop it and retry. - `properties` are validated against the object by Attio itself; a slug that does not exist on the object simply returns no column. Resolve the catalogue via `options/properties` rather than guessing slugs. - `filters` is optional. Omit it to import every record of the object. - Preview `metadata.total_results` is the number of PREVIEWED rows (capped at 100), not the size of the whole import. Attio's query API returns no total and has no count endpoint, so a true total would mean paging the entire object at preview time. - 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. - Records sharing a creation timestamp are neither skipped nor re-imported. ================================================================================ 9. HOW UPDATES REACH THE LIST ================================================================================ Attio has no last-modified attribute — its "attributes on every object" set is exactly `created_at`, `created_by`, `record_id` — so nothing can be re-queried to discover an edit. That splits an active source in two: NEW RECORDS The scheduled pull, on your `schedule` cron. This is the only path new records take. EDITS A `record.updated` webhook Floqer registers in the Attio workspace when the initial import finishes, scoped to the attributes your filter uses. A source with NO filters gets no webhook — with nothing filtered, no edit can change what the list contains. An edit refreshes the row in place. It is NOT re-pushed to connected workflows: the fan-out is for records entering the list, so a workflow that writes back to Attio cannot retrigger itself. A record edited OUT of the filter is not re-imported, but its existing row stays. DELETIONS ARE NOT SYNCED. A record deleted in Attio keeps its row until the source is rebuilt. Pausing the source removes the webhook as well as the schedule; resuming registers a fresh one. ================================================================================ 10. WHERE IT FITS ================================================================================ UPSTREAM (in Attio) Records of the chosen object (People, Companies, Deals, …) live in the user's Attio workspace with their attribute values. THIS SOURCE Creates an Attio import. On creation it imports matching records immediately (when `pull_existing`); an active source keeps importing newly created records on the `schedule` cron until `expiration_date`, and refreshes edited rows over the webhook. 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. ================================================================================ 11. WHEN TO USE ================================================================================ - One-time import of a filtered set of Attio records for enrichment / outreach: `pull_mode: static` with a `filters` expression. - Keep an Attio segment (e.g. "companies whose domain contains acme") in sync with a Floqer workflow: `pull_mode: active`, `schedule: "0 12 * * *"`. - Import an entire object (no `filters`) as a snapshot. When you instead want to push data OUT of Floqer INTO Attio, use the Attio action templates inside a workflow — see https://floqer.com/docs/action-detail/attio_upsert_record.txt. ================================================================================ This file is maintained manually. Last updated: 2026-08-11. Full interactive reference: https://floqer.com/docs/reference