Vendor sync
A sync copies a connected vendor's data into your workspace database and keeps it fresh. Your agents read local tables instead of paging a rate-limited vendor API on every run, join them against their own state, and act only on what changed since they last looked.
One table per stream, named after the vendor
Turning a sync on backfills the history window you chose, then keeps every table current by polling plus the vendor's own webhooks. Each stream lands in its own table, named after the vendor (heyreach_conversations, calcom_bookings), in the same database your agents already use. A workspace has one sync per vendor, and a vendor's own sub-accounts (HeyReach workspaces and senders, Cal.com event types) are filters on that sync rather than separate syncs. Separate vendors get separate tables; nothing is merged across vendors.
| Vendor | Default tables | Webhooks | Status |
|---|---|---|---|
| Cal.com | 4 | Registered by the sync for you | verified |
| HeyReach | 6 | Registered by the sync for you | verified |
| Smartlead | 8 | Registered by the sync for you | beta |
| EmailBison | 6 | Registered by the sync for you | beta |
| Calendly | 4 | Registered by the sync for you | beta |
| lemlist | 6 | Registered by the sync for you | beta |
| RB2B | 2 | You paste the sync's URL into the vendor's app; the Sync panel shows the steps (Integrations > Webhook) | beta |
| Apollo | 7 | None: the vendor sends no webhooks, so the sync polls | beta |
| La Growth Machine | 5 | Registered by the sync for you | beta |
| Expandi | 5 | You paste the sync's URL into the vendor's app; the Sync panel shows the steps (LinkedIn Settings > Webhooks) | beta |
| Instantly | 6 | Registered by the sync for you | beta |
beta vendors
_raw rather than in a typed column.Cal.com (calcom)
| Table | What it holds | Kind | Default |
|---|---|---|---|
calcom_bookings | Every booking in the history window plus all upcoming ones, kept current by booking webhooks and a repair poll. | entity | on |
calcom_booking_attendees | One row per attendee (or seat) of each synced booking. | entity | on |
calcom_event_types | The bookable event types on the account, re-read every hour. | entity | on |
calcom_schedules | Availability schedules, re-read daily. Not available on an OAuth connection. | entity | on |
calcom_teams | Teams the account belongs to, re-read daily. Not available on an OAuth connection. | entity | off |
calcom_events | One row per webhook delivery, including events with no other record (meetings started and ended, form submissions, recordings). Kept for 90 days by default. | event | off |
HeyReach (heyreach)
| Table | What it holds | Kind | Default |
|---|---|---|---|
heyreach_senders | The LinkedIn accounts connected to the HeyReach workspace, re-read every hour. | entity | on |
heyreach_campaigns | Every campaign with its status and lead progress, re-read every hour and when a campaign completes. | entity | on |
heyreach_campaign_leads | One row per lead in each campaign with its connection, message and sequence status, kept current by reply and acceptance webhooks, a 15-minute poll and a daily re-read of running campaigns. | entity | on |
heyreach_conversations | LinkedIn inbox threads of the sender accounts in the history window, kept current by reply and tag webhooks, a 15-minute poll and a daily re-read of the last three days. | entity | on |
heyreach_messages | Every message of the synced conversations, one row each. Kept for 90 days by default. | event | on |
heyreach_campaign_daily_stats | Per campaign and day: views, likes, follows, messages, InMails, connections and reply rates. The last 7 days are re-read daily because counts land late. Kept for 90 days by default. | event | on |
heyreach_lists | Lead and company lists in the workspace, re-read daily. | entity | off |
heyreach_events | One row per webhook delivery: the only record of individual sends, views and likes when those events are switched on. Kept for 90 days by default. | event | off |
Smartlead (smartlead)beta
| Table | What it holds | Kind | Default |
|---|---|---|---|
smartlead_clients | The agency client sub-accounts on the Smartlead account, re-read daily. | entity | on |
smartlead_campaigns | Every campaign with its status and client, re-read every hour. Each pass also brings in the leads of running campaigns, and once a day the email steps and daily stats. | entity | on |
smartlead_sequence_steps | The email steps of each synced campaign in order, with subject lines and A/B variants, re-read daily. | entity | on |
smartlead_email_accounts | The sending mailboxes with their connection, warmup and daily volume, re-read every hour. SMTP and IMAP passwords are never stored. | entity | on |
smartlead_campaign_leads | One row per lead in each campaign with its status and category, kept current by reply and category webhooks and an hourly read of running campaigns. | entity | on |
smartlead_lead_activities | One row per email sent to a lead and per reply received, across every campaign. Open and click counts ride on the sent rows. Kept for 90 days by default. | event | on |
smartlead_campaign_daily_stats | Per running campaign and day: emails sent, opens, clicks, replies, bounces and unsubscribes. The last 7 days are re-read daily because counts land late. Kept for 90 days by default. | event | on |
smartlead_lead_categories | The global and custom lead categories (Interested, Meeting Request, ...), re-read daily. | entity | on |
EmailBison (emailbison)beta
| Table | What it holds | Kind | Default |
|---|---|---|---|
emailbison_campaigns | Every campaign with its status and lifetime counters, re-read every hour. Once a day the pass also re-reads each campaign's email steps. | entity | on |
emailbison_sequence_steps | The email steps of each synced campaign, with subjects, waits and A/B variants, re-read daily. A step removed from its campaign is marked deleted. | entity | on |
emailbison_leads | Every lead in the workspace with its verification status, read by EmailBison's daily change filter and kept current by reply and unsubscribe webhooks. | entity | on |
emailbison_replies | Inbox replies in the history window with their text, kept current by reply webhooks, a frequent read of the newest replies and a daily full read. | entity | on |
emailbison_events | EmailBison's event log for the event types switched to webhook (replies, interest, unsubscribes, bounces and untracked replies by default). EmailBison keeps 10 days, so the backfill covers 10 days. Kept for 90 days by default. | event | on |
emailbison_sender_emails | The sending mailboxes with their status, daily limit and lifetime counters, re-read every hour. | entity | on |
emailbison_sent_emails | One row per campaign email sent, with opens, clicks and replies; the last 2 days are re-read daily as counts move. Off by default: high-volume senders pass row caps fast. Kept for 90 days by default. | event | off |
Calendly (calendly)beta
| Table | What it holds | Kind | Default |
|---|---|---|---|
calendly_scheduled_events | Every booked meeting that starts inside the history window, plus all upcoming ones, kept current by invitee webhooks and a rolling re-read of recent and upcoming meetings (the last 30 days once a day). Calendly reports no edit times, so a change to an older meeting shows up only when a rescan covers it. | entity | on |
calendly_invitees | One row per invitee of each synced meeting, with their answers, UTM tracking, cancellation, no-show and reschedule links. Read with their meeting. | entity | on |
calendly_event_types | The bookable event types, re-read every hour; ones Calendly no longer returns are marked deleted. | entity | on |
calendly_users | The organization's members and their roles, re-read daily (on a member's token, only that member). | entity | on |
lemlist (lemlist)beta
| Table | What it holds | Kind | Default |
|---|---|---|---|
lemlist_campaigns | Every campaign in the team with its status, re-read every hour; campaigns lemlist no longer returns are marked deleted. | entity | on |
lemlist_sequence_steps | Each campaign's sequence steps (email, LinkedIn and other steps, with conditional branches), re-read daily with the campaigns. | entity | on |
lemlist_campaign_leads | One row per lead in each campaign with its latest state, read from the campaign export: running and paused campaigns every hour, every campaign nightly, and a campaign as soon as a webhook names it. | entity | on |
lemlist_activities | Every campaign activity (sends, opens, clicks, replies, LinkedIn steps, bounces, unsubscribes), one row each. Kept for 90 days by default. | event | on |
lemlist_unsubscribes | Unsubscribed addresses at campaign, team or global scope, re-read daily. | entity | on |
lemlist_campaign_daily_stats | Per campaign and day (UTC): messages sent, opens, clicks, replies, bounces and interested leads. Days with no activity are not stored. The last 7 days are re-read daily because counts land late. Kept for 90 days by default. | event | on |
RB2B (rb2b)beta
| Table | What it holds | Kind | Default |
|---|---|---|---|
rb2b_profiles | Every visitor RB2B identified, with their company and their latest visit. RB2B keeps 90 days on its API, so the sync starts with at most the last 90 days. | entity | on |
rb2b_page_views | One row per page view RB2B sends to the webhook (every visit when Send repeat visitor data is on). RB2B's API has no page views, so these exist only from the webhook, and views sent while it was off are lost. Kept for 90 days by default. | event | on |
Apollo (apollo-io)beta
| Table | What it holds | Kind | Default |
|---|---|---|---|
apollo_io_contacts | The people saved in your Apollo workspace that changed within the history window (every contact, up to 100,000, when history is everything), kept current by reading the newest changes on a schedule. | entity | on |
apollo_io_accounts | The companies saved in your Apollo workspace that changed within the history window (every account, up to 100,000, when history is everything), kept current by reading the newest changes on a schedule. | entity | on |
apollo_io_sequences | Every sequence with its step count and lifetime engagement counts, re-read every hour. | entity | on |
apollo_io_emails | The emails your sequences sent, with status, the replied flag and the reply class (Apollo does not expose reply text). Kept for 90 days by default; the last two days are re-read every hour because statuses change after sending. | event | on |
apollo_io_tasks | Every task in the workspace, re-read daily (Apollo offers no way to read only changed tasks). | entity | on |
apollo_io_deals | Every deal with its amount, stage and outcome, re-read every hour. | entity | on |
apollo_io_email_accounts | The mailboxes connected to Apollo with their daily sending limit, re-read daily. | entity | on |
apollo_io_calls | Calls made with Apollo's dialer, with status and duration. Needs a master API key. Kept for 90 days by default; the last two days are re-read every hour. | event | off |
apollo_io_users | The people on your Apollo team. Needs a master API key. Re-read daily. | entity | off |
La Growth Machine (lagrowthmachine)beta
| Table | What it holds | Kind | Default |
|---|---|---|---|
lagrowthmachine_identities | The senders (a LinkedIn account and its mailboxes) of each synced workspace, re-read daily. | entity | on |
lagrowthmachine_campaigns | Every campaign of each synced workspace with its status and lead count, re-read every hour. | entity | on |
lagrowthmachine_campaign_leads | One row per lead in each running or paused campaign, with its LinkedIn relation, reply and unsubscribe state and its email and LinkedIn counters, re-read every hour. | entity | on |
lagrowthmachine_conversations | Inbox conversations of each synced workspace whose last message falls in the history window, kept current on their last-message time. | entity | on |
lagrowthmachine_messages | The messages of the synced conversations, one row each (La Growth Machine keeps the latest 25 per conversation). Kept for 90 days by default. | event | on |
Expandi (expandi)beta
| Table | What it holds | Kind | Default |
|---|---|---|---|
expandi_li_accounts | The LinkedIn accounts (senders) of each synced workspace, re-read every hour. | entity | on |
expandi_campaigns | The campaigns of each synced LinkedIn account with their status, contact count and Expandi stats, re-read every hour. | entity | on |
expandi_messengers | One row per lead of each synced LinkedIn account (Expandi calls it a messenger), with its connection and conversation status, re-read every hour. | entity | on |
expandi_messages | The LinkedIn messages of the synced leads, read again whenever a lead shows new activity. Kept for 90 days by default. | event | on |
expandi_events | One row per webhook delivery from Expandi, stored whole: the only record of individual visits, invitations, likes and email events, and only from the moment a webhook is set up. Kept for 90 days by default. | event | on |
Instantly (instantly)beta
| Table | What it holds | Kind | Default |
|---|---|---|---|
instantly_campaigns | Every campaign with its status, sending settings and sending accounts, re-read every hour and when a campaign completes. | entity | on |
instantly_campaign_analytics | Instantly's lifetime counters for every campaign: leads, sends, opens, replies, clicks, bounces, unsubscribes and opportunities. Re-read every hour and when a campaign completes. | entity | on |
instantly_campaign_daily_stats | Per campaign and day: sends, leads contacted, opens, replies, clicks and opportunities. The last 7 days are re-read daily because counts land late; a day with no activity is not stored. Kept for 90 days by default. | event | on |
instantly_leads | Every lead with its campaign, status, interest and open, reply and click counts. Re-read in full every 6 hours, and re-read when a webhook names it. | entity | on |
instantly_replies | Emails received in the Unibox in the history window, with their text. Kept current by reply webhooks and a frequent read of the newest; once a day the last 3 days are read again as read marks change. | entity | on |
instantly_sent_emails | One row per email sent, with its campaign, step and lead. Off by default: busy senders pass row caps fast. Kept for 90 days by default. | event | off |
instantly_accounts | The sending email accounts with their status, last error, warmup status and score, and daily limit. Re-read every hour and on an account error. | entity | on |
instantly_account_daily_stats | Per sending account and day: sends, bounces, opens, replies and clicks. The last 7 days are re-read daily. Off by default. Kept for 90 days by default. | event | off |
instantly_events | One row per webhook delivery: the only record of single opens and clicks once those events are switched to webhooks. Off by default. Kept for 90 days by default. | event | off |
From the vendor's connection page
- 01
Connect the vendor
Sync lives on the vendor's page under Connections, in the Sync panel between Connection and Tool permissions. Until the vendor is connected the panel only lists what a sync would copy. Cal.com connected over OAuth syncs by polling only: webhooks, schedules and teams need an API key connection.
- 02
Read the preview
Before anything is created, the panel shows the projected rows, the new tables and the webhook events a month against your plan's limits, plus any warnings. The preview checks the key with one cheap vendor call, so a key the vendor rejects, or a vendor plan without API access, is refused here, with the reason, and never produces a sync. A plan refusal says to upgrade with the vendor and press Start sync again; a new key would not help.
- 03
Start sync
The sync registers its own webhooks with the vendor, then each stream backfills its history window. The panel shows progress per table while it backfills and switches to Live when every table is caught up. A workspace without a database gets one on the first sync if the plan includes it.
RB2B and Expandi have no API for webhooks, so for them the Sync panel shows the sync's webhook URL in a copyable field with numbered steps naming the vendor screen to paste it into, then reports whether events arrive: waiting for the first one, receiving, or gone quiet while the vendor account stays busy. Apollo sends no webhooks at all, so its sync polls.
Customize
Customize opens every setting, and re-runs the preview as you change them so you see the effect on your limits before you save. A save that widens the history window backfills the gap.
| Setting | Default | Choices |
|---|---|---|
| Streams | The vendor's default streams | Switch each stream (one table each) on or off. A stream the connection cannot read says why. |
| Fields | The useful columns of each stream | Add or drop typed columns per stream. Everything else stays in _raw. |
| Freshness | Adaptive, every 15 minutes to an hour | Adaptive, a fixed interval (15 minutes, 30 minutes, 1 hour, 3 hours, 6 hours, 1 day), or only when you run it. |
| History | The last 90 days | 30, 90, 180 or 365 days, everything, or a start date. |
| Scope | Everything the key can see | Narrow to the vendor's own sub-accounts, campaigns, senders or event types, picked from what your connection can see. |
| Webhooks | The vendor's recommended events | Per event: receive it as a webhook, or pick it up by polling. Shows each event's expected volume and how many deliveries a month it would add. |
| Retention | Event tables keep 90 days | A rolling 90 days, or keep everything. |
| Wake an agent | None | One deployed agent, woken by every webhook event or only the ones you pick. |
Read them, never edit them
Synced tables are ordinary tables: query them with agnt_db_select, agnt_db_execute_sql, the /v1/db routes or a dashboard view. Besides the stream's typed columns, every one carries four engine columns:
| Column | What it is |
|---|---|
_raw | The vendor's whole object as fetched. Fields that are not typed columns are read from here, for example _raw->>'field'. |
_hash | A fingerprint of _raw. A row whose content did not change is not rewritten. |
_synced_at | When the sync last wrote the row. How fresh that row is. |
_deleted_at | Set when the vendor no longer has the record. The row stays so you can see it went; filter _deleted_at is null to skip it. |
the sync owns these tables
Polling for correctness, webhooks for speed
- Polling keeps every table correct on its own. Adaptive freshness checks busy tables every 15 minutes and quiet ones up to hourly, and slow-moving tables such as daily stats refresh daily; a fixed interval or manual-only works too. Sync now runs a pass immediately.
- Webhook events arrive at a webhook endpoint the sync creates and manages itself, through the same receiver as any inbound webhook, and registers its URL with the vendor for you (for RB2B and Expandi you paste it in yourself). Where the vendor signs its deliveries or echoes a token back, the sync checks it before anything is counted or stored; elsewhere the URL itself is the secret.
- Every webhook delivery counts toward your plan's webhook events for the period. Past the limit the vendor's deliveries are refused, polling keeps the tables correct, and the sync re-registers any webhook the vendor switched off once the period resets.
- A webhook can arrive before the matching row is written, so freshness is per table: the Sync panel and
agnt_sync_statusshow when each table last synced and when it runs next.
Synced rows are your rows
Synced tables count against the workspace database caps exactly like tables you create, and vendor webhook deliveries count against the plan's webhook events. Event tables (messages, daily stats, booking events) keep a rolling 90 days unless you choose to keep everything; entity tables keep every row. The billing page and the Database page break the rows and webhook events down by sync.
| Plan | Tables | Rows, all tables | Storage | Webhook events / period |
|---|---|---|---|---|
| Starter | 10 | 25,000 | 250 MB | 25,000 |
| Growth | 50 | 250,000 | 1 GB | 150,000 |
| Scale | 250 | 2,500,000 | 10 GB | 750,000 |
Table, row and storage caps apply to serverless databases and are summed across the organization; see Workspace database limits.
React to vendor events as they happen
A sync can wake one deployed agent when a vendor event arrives as a webhook: a reply, a cancelled booking. Pick the agent in Customize, with every webhook event or only the ones you choose, or set it with agnt_sync_update and wake_agent. Events you set to polling never arrive as webhooks, so they never wake the agent; events left out of your choice are still stored and synced, they just do not wake it.
the agent can wake first
Three ways to stop, three different costs
| Action | Kept | What changes |
|---|---|---|
| Pause | Everything | No polling and no webhook work until you resume. Resume picks up where it stopped, with the same webhook URL and woken agent. |
| Turn off sync | Tables, rows, cursors | The vendor webhooks are removed and the sync's webhook URL is retired. Turning it back on continues from where the tables left off, with a new webhook URL and secret, and links the agent it woke to the new URL. |
| Turn off and delete the tables | Nothing | The sync is turned off, then every table it created is dropped after a second confirmation that lists them. Turning it on again backfills from scratch. |
Disconnecting the vendor stops the sync and removes its vendor webhooks too; the synced tables are kept, and connecting again then turning the sync back on resumes from them.
The agnt_sync_* tools
Every workspace MCP server serves the sync family as three tools, agnt_sync_read, agnt_sync_write and agnt_sync_delete, each taking an action; a server set up to list tools per operation lists the eleven per-operation tools below instead. A sync is addressed by provider, the vendor's connection id. The Agnt Wizard has the same tools under the same names, and every result carries a sync_url to the Sync panel.
| Tool | Group | Default permission | Does |
|---|---|---|---|
agnt_sync_catalog | agnt_sync_read | act | What each vendor can sync: streams, tables, columns, how each stream updates, webhook events and how they are set up (automatic, pasted by hand with the steps, or none), filters, defaults, whether it is connected, and whether it is verified or beta. |
agnt_sync_list | agnt_sync_read | act | Every sync in the workspace with its status, tables and freshness. |
agnt_sync_status | agnt_sync_read | act | One sync in full: settings and config_version, per-table freshness, webhooks with why one could not be set up, the webhook URL to paste for RB2B and Expandi (ingest_url), the agent it wakes, usage against the plan. |
agnt_sync_preview | agnt_sync_read | act | What turning a sync on or changing it would do: projected rows, new tables and webhook events against the plan. Changes nothing. |
agnt_sync_runs | agnt_sync_read | act | Run history: kind, status, who asked, timings, row counts per table, errors. |
agnt_sync_changes | agnt_sync_read | act | Rows of one synced table changed since a cursor. |
agnt_sync_enable | agnt_sync_write | ask | Turn a sync on, with optional settings and an agent to wake. |
agnt_sync_update | agnt_sync_write | ask | Change settings, pause (enabled: false) or resume, set or clear the agent it wakes. |
agnt_sync_run | agnt_sync_write | act | Run now: incremental, or a backfill from a start date. |
agnt_sync_disable | agnt_sync_delete | always ask | Turn the sync off. Tables kept; webhook URL retired. |
agnt_sync_drop_tables | agnt_sync_delete | always ask | Delete the sync's tables. Requires confirm_destructive: true. |
A deployed agent whose config has the workspace database enabled gets agnt_sync_status and agnt_sync_changes, and only those two: turning syncs on, changing and dropping them stays with people, the Agnt Wizard and workspace MCP clients. The agent's database policy applies, so a table allowlist must include the synced tables it reads.
Only what changed: agnt_sync_changes
- Pass
stream(the table name) andsince, thenext_cursoryou stored last time. Omitsinceto read the table from the start, or pass"now"to tail from now. Keep paging whilehas_moreis true. - Each change is an
upsert(added or updated) or adelete(the vendor removed the record), with its key;include_rowsadds the typed columns. - The cursor is opaque and belongs to one table. Rows that share a timestamp are never skipped or repeated across pages, and rows newer than 60 seconds wait for a later call.
- Rows removed by the 90-day retention or by deleting a table are not reported.
{
"provider": "heyreach",
"stream": "heyreach_conversations",
"changes": [
{ "key": { "id": "2-ZTQ3Nz..." }, "op": "upsert", "synced_at": "2026-10-10T14:02:11.482913Z" },
{ "key": { "id": "2-YmE1Mm..." }, "op": "delete", "synced_at": "2026-10-10T14:03:40.120544Z" }
],
"next_cursor": "eyJ2IjoxLCJzIjoiaGV5cmVhY2hfY29udmVyc2F0aW9ucyIs...",
"has_more": false,
"freshness": { "state": "live", "last_synced_at": "2026-10-10T14:05:02.311Z", "sync_status": "live" },
"sync_url": "https://app.superagnt.com/dashboard/<workspace-id>/connections/heyreach#sync"
}From a skill or your own code
TypeScript skills and data jobs read the same two through the SDK. A deployed agent's runtime routes serve them too, POST /runtime/agents/:id/sync/status and POST /runtime/agents/:id/sync/changes, under that agent's own key and database policy.
import { createAgntdata } from "@agntdata/sdk"
const agnt = createAgntdata()
// Freshness first: is the conversations table current enough to act on?
const sync = await agnt.sync.status("heyreach")
// Then only what changed since the cursor this agent stored last run.
const page = await agnt.sync.changes({
stream: "heyreach_conversations",
since: storedCursor, // omit on the first run to read from the start
limit: 500,
includeRows: true,
})
// ...act on page.changes, then store page.next_cursor (keep paging while page.has_more)curl -X POST "https://api.superagnt.com/runtime/agents/<deployed-agent-id>/sync/changes" \
-H "Authorization: Bearer $AGNTDATA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "stream": "heyreach_conversations", "since": "now" }'What each reason means
A sync reads Backfilling, Live, Paused or Needs attention. Needs attention always carries one of these reasons, shown in plain words on the Sync panel with the fix, and as attention_reason in agnt_sync_status. Rows already synced stay readable throughout.
| Reason | What it means |
|---|---|
credential_rejected | The vendor rejected the connection's key. Reconnect it on the connection page; the sync resumes on its own. |
connection_removed | The connection was removed. Connect it again, then turn the sync back on. |
account_changed | The connection now answers as a different vendor account. Turn the sync off and on again to start fresh; delete the tables too to remove the old account's rows. |
database_missing | The workspace database is gone (deleted, or removed after a trial ended). The sync resumes once the workspace has a database again. |
workspace_walled | The plan lapsed or the trial ended, so syncing is paused. It resumes when a plan is active. |
webhook_limit_reached | Over this period's webhook events. Vendor webhooks are refused and the sync keeps up by polling until the period resets. |
row_limit_reached | Over the database row limit. New rows wait; updates to existing rows continue. |
table_limit_reached | Over the database table limit. New streams cannot start. |
storage_limit_reached | Over the database storage limit. |
vendor_rate_limited | The vendor is slowing the sync down. It tries again on its own. |
vendor_error | The vendor returned an error. Sync now retries. |
table_conflict | A table with the stream's name already exists and the sync did not create it. Rename your table or switch the stream off. |
vendor_plan_required | The vendor plan on this account does not include API access. Upgrade it with the vendor. |
credential_insufficient | The key cannot read every stream. The streams it cannot read are paused. |
Manage syncs at app.superagnt.com under Connections, on each vendor's page.