Records
Records are provider-delivered resources — contacts, deals, orders, charges, messages, events, tickets — stored in one unified place. Everything your connected providers send you, across every resource type, lands in the records store automatically.
Why Records exist
The platform started Meta Lead Ads → CRM lead sync. The leads table is still the home of that original pipeline. But real integration work touches every resource type a provider exposes: HubSpot contacts, Stripe charges, Shopify orders, Zendesk tickets, Slack messages, Google Calendar events, Twilio SMS, Zoom recordings. Records gives each of these a first-class home without requiring a per-type admin UI.
The model
Every record has:
provider_type— which adapter delivered it (hubspot, stripe, …)resource_type— what kind of thing it is (contact, deal, charge, …)external_id— the provider's native id, for idempotent re-ingestionsource— how it arrived:webhook,poll,import, orflow_stepdata— the JSONB payload, free-form and provider-specific
The unique index on (account, provider_type, resource_type, external_id) makes ingestion idempotent: the same provider record can't be stored twice, and re-delivery merges updates into the existing row.
The three sources
Webhooks
When a webhook adapter opts in by setting recordResourceType in its parseWebhookPayload(), every incoming event mirrors into records automatically. Currently opted in: Meta Lead Ads, HubSpot (contact/deal/company/ticket), Google Calendar, Twilio, Zoom, Slack, WhatsApp.
Poll triggers
Poll-kind flow trigger subscriptions can specify a target resource type in their config, and the scheduler ingests every polled item before firing the flow:
{
"kind": "poll",
"providerType": "hubspot",
"triggerName": "contact_list",
"config": {
"pollAction": "list_contacts",
"cursorPath": "results.0.updatedAt",
"recordResourceType": "contact",
"recordItemsPath": "results",
"recordExternalIdPath": "id"
}
}CSV import
Use /admin/records/import to bulk-upload records from a CSV. Pick a provider type (use manual for hand-curated data) and a resource type, paste your CSV, and submit. An external_id or id column makes re-imports merge into existing rows.
Browsing records
/admin/records is the unified browser. The resource-type pills at the top let you pivot instantly between contacts, deals, messages, events, etc. The free-text search scans the JSONB data across every field. Click any row to open the detail view with the full pretty-printed payload.
Re-running flows against a record
The detail page has a Re-run flows button that calls POST /connect/v1/records/:id/replay. This re-dispatches the record through every active flow subscribed to (providerType, resourceType) — useful after fixing a broken flow step, or when a flow was added after the original event. The response reports how many flows matched and how many ran.
Records ↔ Tables crosslink
The record.to_table flow step copies a record into a user Table via upsert. Use it to deduplicate records across providers, build a canonical customers view, or stage records for flows that read from tables instead of webhooks.
{
"kind": "record.to_table",
"config": {
"tableSlug": "customers",
"keyField": "email"
},
"inputMapping": {
"recordId": "{{trigger.recordId}}"
}
}API reference
GET /connect/v1/records # records.view
?resourceType=<type>&providerType=<provider>&search=<text>&page=<n>
GET /connect/v1/records/summary # records.view
GET /connect/v1/records/:id # records.view
DELETE /connect/v1/records/:id # records.write
POST /connect/v1/records/:id/replay # records.write
POST /connect/v1/records/import # records.write
{ "providerType": "...", "resourceType": "...", "csv": "..." }Relationship to Leads
The leads table and the /admin/leads page are still live and unchanged. Lead webhooks continue to flow through the legacy pipeline (upsert into leads → CRM sync → flow dispatch), and those leads also mirror into records with resource_type='lead' as a side effect.
The endgame — tracked in the seed at .planning/seeds/generic-records-model.md — is for the Leads page to become a saved view over records. That refactor is a separate milestone because it touches the legacy CRM sync path.