Integration · Data operations
How to build reverse ETL from the warehouse
The warehouse knows which accounts are expanding and the sales team cannot see it. What follows is the model behind pushing modelled data back into the tools people work in, the prompts that build it, and what changes in production.
Built with Tray Headless
- System Snowflake
- Step Detect changed rows
- Step Field ownership
- Step Rate-limited write
- System Salesforce
Only changed rows sync, and the CRM field is claimed explicitly so nothing overwrites a value a person typed.
The short answer
What is reverse ETL?
There are four parts to reverse ETL: syncing only the rows that changed instead of the whole model, an explicit claim on which fields the warehouse owns, rate handling that survives an API limit without losing a row, and never overwriting a value a person entered. The common failure is field ownership. Pushing a computed value into a field a rep also edits produces a fight neither side knows they are having, and the rep stops trusting the record.
What matters here
- Claim the fields the warehouse owns, in writing, before you build. Sharing a field with a person is how trust in the record dies.
- Sync deltas against a checksum or a modified timestamp. Full syncs burn the API limit and hide the change that mattered.
- Handle the rate limit as a normal condition, not an error. Every destination will throttle you and losing a row to a 429 is silent.
- Write to a dedicated field, not a shared one. A computed value belongs somewhere labelled as computed.
- Report rows synced against rows attempted. Silent partial syncs are the default failure and nothing announces them.
Who this is for
You run data or analytics engineering. The warehouse has the best version of the truth and the people who need it work in tools that have never seen it.
How it works in practice
The sequence, from a model updating to a rep seeing the result.
- 1
The model produces a table shaped for the destination
One row per record, with the destination id already resolved. Reverse ETL should not be doing joins.
- 2
Changed rows are detected against the last sync
On a checksum of the synced columns, so an unrelated column changing does not trigger a write.
- 3
Field ownership is checked before writing
Only fields the warehouse claims. Anything shared with a person is written to a separate computed field instead.
- 4
Writes are batched and rate-limited
Backing off on a 429 rather than dropping the batch, because a lost row is silent.
- 5
Nothing overwrites a value a person entered
Human-entered fields are never touched, and the warehouse value goes somewhere labelled as derived.
- 6
Row counts are reconciled after every run
Attempted, succeeded, failed and skipped. A partial sync that reports success is the common failure.
What reverse ETL is made of
Four parts. The second is a negotiation before it is code.
Delta detection
A checksum over the synced columns, compared against the last successful sync. Full pushes are expensive and hide what changed.
Explicit field ownership
A written claim on which destination fields the warehouse writes. Everything else is out of bounds, permanently.
Rate handling
Backoff, batching and resume. Destination APIs throttle, and a row lost to a rate limit disappears without a trace.
Human precedence
A value somebody typed always wins. Computed values go to their own fields, labelled, so nobody confuses the two.
The Tray Headless prompts
Paste these into Claude Code or Codex with the Tray Headless plugin installed. Each stage runs on its own. The systems named in them are the worked example rather than a requirement, and every prompt says so.
Once per project, run
/tray-workflows:set-workspace
to pick the workspace these build in. Point it at a sandbox first.
- 1
Set up and claim the fields before writing anything
This is a negotiation with whoever owns the destination.
Headless skills
build-workflowUse build-workflow. The systems in play are Snowflake and Salesforce, or whatever we run in those seats. Before building, produce the field ownership claim: exactly which Salesforce fields the warehouse will write, on which objects, and confirmation that no human process writes them. Custom fields carrying warehouse output should be named as such, so a Health_Score__c is visibly not a field a rep is meant to edit. My rule: never share a field. If a rep edits it, the warehouse writes a separate field with a clear derived label instead. A computed value and a typed value in the same field is a fight neither side knows they are having, and the rep loses faith in the whole record. Show me any field on my list that a person can currently edit. Those are the ones to move.
- 2
Sync deltas, on a checksum
Full pushes burn the limit and hide the change that mattered.
Headless skills
build-workflowUse build-workflow. Build the model into a table shaped for the destination: one row per record, the Salesforce id already resolved, and only the columns being synced. Reverse ETL should not be doing joins. Compute a checksum over the synced columns per row. Compare against the last successful sync and write only the rows whose checksum changed. That matters more than it sounds. Without it, an unrelated column updating in the model triggers a write to every record, which burns the API limit and makes the change you cared about invisible in the noise.
- 3
Write carefully, and treat the rate limit as normal
A row lost to a 429 disappears without a trace.
Headless skills
tray-patternsBatch writes to the destination limit, and treat a rate limit response as a normal condition rather than a failure: back off, retry, resume from where you stopped. Never let a batch failure lose rows silently. Record which rows were attempted, which succeeded, which failed and why, and make the failed set retryable on its own. Handle these: A destination record that has been deleted since the last sync A destination record that was merged into another A picklist value the model produced that the destination will not accept, which should fail loudly instead of writing blank A field that has been made read-only since the claim was agreed
The merged-record case is the one that silently writes to a dead id for months. Reconcile against the destination periodically rather than trusting the ids you cached.
- 4
Never overwrite a person
The rule that decides whether anybody trusts the data.
Headless skills
tray-gotchasUse tray-gotchas, then enforce human precedence. Before writing any field, check whether the current value was last modified by a person, not by an integration. If it was, do not overwrite it. Write to the derived field and flag the divergence. Report those divergences. A field the warehouse and a rep disagree about repeatedly is either a model that is wrong or a process that should not be manual, and both are worth knowing. Never delete a destination value because the model produced a null. An absent value in the model is usually a join that did not match, not a statement that the value is empty.
- 5
Reconcile every run
A partial sync reporting success is the standard failure mode.
After each run report: rows in the model, rows changed, rows attempted, succeeded, failed and skipped for human precedence. Attempted should equal succeeded plus failed. If it does not, rows went missing and the run should fail rather than report success. Also alert on staleness: if a sync has not completed within its expected window, the destination is now showing values that look current and are not. Stamp the last successful sync on the record so a reader can tell. Then report freshness by object and the divergence count. Both are invisible unless somebody produces them.
- 6
Check it end to end, then hand the field claim over
Because ownership is agreed between two teams, not decided by one.
Run the per-step schema checks and the whole-workflow audit before this touches production. Run in dry-run mode for a week first, computing every write and performing none, then read the diff with the team that owns the destination. Then open the same workflow in Tray Build so analytics engineering and revenue operations can amend the field ownership claim together.
What it connects to
One source, several destinations, and the discipline to write narrowly.
Snowflake
Read the modelled tables and hold the sync state and checksums. The source of truth for everything written.
Reads and writes
Salesforce
Write only the claimed fields, never a value a person entered, and read back for reconciliation.
Reads and writes
Marketo
Push audience and scoring attributes where campaigns run, subject to the same ownership rules.
Writes
Zendesk
Push account context onto the organisation, so support sees what the warehouse knows.
Writes
dbt
Read test results so a failed test blocks the sync instead of pushing bad data into the tools people trust.
Reads
Same build, other stacks
The design does not change if you run something else in one of these seats. The same prompts build it against Microsoft Dynamics 365, Google BigQuery, Microsoft Teams, Jira, HubSpot or Databricks.
Named systems are the ones most teams run, not the only ones that work. Each is an authentication in your Tray workspace, referenced by name, so the workflow never holds a credential. Where we have a connector page, the name links to it.
Running it in production
This writes into the systems people work in every day. A bad row is seen and believed.
It runs on the platform, not on your laptop
Scheduled syncs with backoff and resume run on the same engine, so a rate limit at 2am is a pause instead of a lost batch.
Every write is attributable
Which run, which model version, which checksum. When a rep disputes a value, that is the answer rather than a shrug.
Credentials are managed, never written into the build
A credential that can write across the CRM is powerful. It lives in your workspace, scoped to the claimed fields where the destination allows it.
Both teams own the claim
The field ownership list opens in Tray Build so analytics engineering and the destination owner amend it together.
A dbt test failure stops the sync
Pushing data that failed its own tests into the tools people trust is worse than pushing nothing.
Questions people ask
Why claim fields explicitly?
Because a field written by both a model and a rep is a fight neither side knows they are having. The rep corrects it, the sync reverts it, and within a month nobody trusts the record.
Why sync on a checksum?
Because an unrelated column changing in the model would otherwise trigger a write to every record. That burns the destination API limit and buries the change that actually mattered.
What should happen when the model produces a null?
Nothing. An absent value is almost always a join that did not match rather than a statement that the field is empty, and deleting a destination value on that basis destroys real data.
How should rate limits be handled?
As a normal condition. Back off, retry and resume from where you stopped, recording which rows were attempted. A row lost to a 429 disappears without any signal at all.
Should a failing dbt test stop the sync?
Yes. Pushing data that failed its own tests into the tools people make decisions in is worse than pushing nothing, because it looks current and gets believed.
Vibe-coding app guides
Vibe-code an entire app with Helix
This moves the data between systems. It does not give anybody a screen to work in. Build that app in Claude Code, Codex or Cursor, then deploy and run it governed on Tray Helix. Same kind of guide, same kind of prompts.
How to build an executive KPI dashboard (opens helix.tray.ai in a new tab)
Related guides
Data operations
How to build a CRM to warehouse sync
Capture history instead of current state, handle deletes and field changes, land raw then model, and prove the row counts. The Headless prompts that build it.
Data operations
How to build a customer 360 and master data sync
Pick a survivorship order per attribute, resolve identity on more than email, and publish a golden record every system can point at. The prompts.
Data operations
How to build data quality monitoring
Test what breaks decisions, alert the owner rather than a channel, and say what depends on a failure. The Headless prompts that build it.
Platform engineering
How to build a change data capture pipeline
Read the log instead of polling a timestamp, overlap the snapshot with the stream, and keep deletes and order intact. The Headless prompts that build it.
Last reviewed September 2026.