Integration · Data operations
How to build an ERP to warehouse sync
A nightly export of the general ledger looks fine until finance compares the warehouse total to the ERP and the two disagree by one reversed journal. This is the design behind a sync whose totals match, the prompts that build it, and what production adds.
Built with Tray Headless
- System NetSuite
- Step Pull what changed
- Step Land the raw copy
- Step Model finance tables
- System Snowflake
Raw lands before anything is modelled, and the daily reconciliation compares totals to the ERP, not row counts to yesterday.
The short answer
What is an ERP to warehouse sync?
An ERP to warehouse sync pulls only the records that changed since the last run, lands an untouched raw copy before any modelling, keeps posted, reversed and voided entries as separate facts instead of overwriting one with the next, and reconciles totals per account and period back to the ERP every day. Where this usually goes wrong is treating the ERP like any other app. A journal that is reversed and reposted looks like an update, and a sync that overwrites it reports the right row count and the wrong balance.
Stage 4 of 7: Load business apps into the warehouse. Part of Data integration, end to end : every stage, the systems it runs on and the guide that builds it.
What matters here
- Pull by last modified date, then re-pull a trailing window. ERPs change records in ways that do not always move the timestamp, and a short overlap catches them.
- Land the raw copy before modelling. A bad transformation is then a rerun, not a re-extract from a system with tight API limits.
- A reversal is a new fact, not an edit. Keep the original, the reversal and the repost, or the period balance stops matching.
- Reconcile money, not rows. Totals per account and period against the ERP's own trial balance is the check finance trusts.
- Closed periods are read-only in the ERP. Treat a change to one in the warehouse as an alert, never as a routine update.
- API limits are shared with every other integration on the ERP. Schedule the heavy pulls outside the close and the billing run.
Who this is for
You run data operations or analytics engineering. Finance reports come out of the warehouse, the numbers drift from the ERP often enough that the controller checks both, and month end is when anyone notices.
How it works in practice
The path from a journal posting in the ERP to a finance table someone can sign off on.
- 1
The sync asks the ERP what changed
Records modified since the last run, plus a trailing overlap for changes that did not move the timestamp.
- 2
The raw records land untouched
In a raw schema in the warehouse, with the run id and the time they were pulled.
- 3
Finance tables are built from the raw copy
Posted, reversed and voided entries kept as separate facts, so history is never rewritten.
- 4
Totals are reconciled to the ERP
Per account, subsidiary and period, against the ERP's trial balance.
- 5
A difference goes to a named owner
With the accounts and periods that disagree, not a failed job and a log link.
What the sync is made of
Four parts. The third is the one that quietly breaks a balance.
A change pull with an overlap
Records modified since the last checkpoint, re-reading a trailing window, because some ERP changes leave the modified date alone.
A raw landing zone
An untouched copy of every record as the ERP returned it. Models are rebuilt from it, so a modelling bug costs a rerun rather than a fresh extract.
Entries kept as history
A reversal and a repost are new facts next to the original. Overwriting turns a correct ledger into one that adds up to the wrong number.
Reconciliation in money
Totals per account and period against the ERP's own trial balance, every day, with the difference sent to an owner.
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
Pull only what changed
A full export every night burns the ERP's API limits and still misses the edge cases.
Headless skills
build-workflowtray-patternsUse build-workflow. The systems in play are NetSuite and Snowflake, or whatever we run in those seats. For each record type we need (transactions, transaction lines, accounts, subsidiaries, customers, vendors, items, accounting periods), pull the records modified since the last successful checkpoint. Re-read a trailing window of two days on every run, because some ERP changes do not move the last modified date and the overlap is what catches them. Page through results and keep the checkpoint per record type. Commit it only after the batch has landed in the warehouse. Use saved searches or the query API rather than one call per record. A sync that calls the ERP once per transaction will hit the account's concurrency limit and slow down every other integration that shares it.
API limits on the ERP are shared across every integration in the account. Schedule the heavy record types outside the billing run and the close.
- 2
Land the raw copy first
A modelling bug should cost a rerun, not a new extract.
Headless skills
build-workflowUse build-workflow. Write every record to a raw table in Snowflake exactly as the ERP returned it, with three extra columns: the run id, the time it was pulled and the record type. Do not rename, cast or drop anything on the way in. Upsert on the ERP's internal id and record type, so a record pulled twice by the overlap window lands once. Keep the raw tables append-friendly and partitioned by pull date, so a full rebuild of the finance models never has to go back to the ERP.
- 3
Keep posted, reversed and voided entries apart
Overwriting a reversal is how a correct ledger adds up to the wrong number.
Build the finance tables from the raw copy, not from the ERP. Model journal lines as facts that are never updated in place: The original posting stays, with its period and posting date A reversal is its own line, linked to the original A repost is its own line, linked to the reversal A voided transaction is marked voided with the date, not deleted Take the accounting period from the ERP's own period on the transaction, never from the transaction date. A transaction dated in March and posted to April belongs to April. Flag any change to a transaction in a period the ERP shows as closed. Closed periods should not change, so one that does is something finance needs to hear about the same day.
- 4
Reconcile totals to the ERP every day
Row counts can match while the balance does not.
Headless skills
tray-patternsUse tray-patterns. Once a day, after the sync completes: Pull the trial balance from the ERP per account, subsidiary and open period Sum the same from the warehouse finance tables Compare, with a tolerance of zero for posted amounts For every difference record the account, subsidiary, period, both totals and the gap. Resolve an owner per subsidiary from a mapping table, and send them one message listing what disagrees. Do not alert a shared channel, where a difference nobody owns stays open. Report: run time, records pulled per type, records in the overlap that changed, reconciliation differences and API calls used against the account's limit.
- 5
Test it against a real close, then hand it to finance
Because the close is when every edge case shows up at once.
Headless skills
tray-gotchasUse tray-gotchas, then run the per-step schema checks and the whole-workflow audit. In a sandbox account, post a journal, reverse it and repost it. Confirm three lines arrive and the period total is right. Void a transaction and confirm it is marked, not removed. Change a record without moving its last modified date and confirm the overlap window picks it up. Then open the workflow in Tray Build so data operations can add record types and adjust the schedule, and finance can own the subsidiary to owner mapping.
What it connects to
The ERP is read, the warehouse holds the raw copy and the finance tables, and differences go to a person.
NetSuite
Return the records modified since the last run, the accounting periods and the trial balance used to reconcile.
Reads
Snowflake
Hold the raw copy of every record and the finance tables built from it, with history kept rather than overwritten.
Writes
Snowflake
Hold the daily reconciliation results, so a difference has a record of when it opened and closed.
Reads and writes
Slack
Send each reconciliation difference to the owner of that subsidiary, with the accounts and periods involved.
Writes
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 SAP S/4HANA, Google BigQuery, Microsoft Teams, Oracle, Databricks or Google Chat.
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
Finance reporting runs on this. If the totals drift, the warehouse stops being where anyone looks.
It runs on a schedule the ERP can afford
Heavy pulls sit outside the billing run and the close, because the API limit is shared with every other integration on the account.
Credentials are held by the platform, never hardcoded
A read-only ERP role scoped to the record types the sync needs, and write access to the raw and finance schemas only.
Every run is logged
Records pulled, checkpoints moved and reconciliation results, so an auditor can trace a warehouse total back to the ERP run that produced it.
Finance owns the reconciliation
Which subsidiaries are checked, who gets each difference and what counts as closed, all open in Tray Build.
The raw copy is kept
So a model change, a new report or an audit question can be answered from the warehouse without re-extracting a year of history.
Questions people ask
Why not export the whole ledger every night?
It uses the API limit that every other integration on the ERP shares, it gets slower every month, and it still overwrites reversals unless the model keeps them apart.
Why land a raw copy before modelling?
So a mistake in the finance models is fixed by rebuilding from the warehouse, not by pulling a year of records back out of the ERP.
What should the reconciliation compare?
Totals per account, subsidiary and period against the ERP's trial balance. Row counts can match while a balance is wrong.
What happens when a closed period changes?
The sync records the change and alerts finance the same day. A closed period should not move, so a change is something to investigate, not to absorb.
Related guides
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.
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.
Finance
How to build financial close automation
Model the dependencies instead of the checklist, pull the data before anybody asks, and show the critical path instead of a percentage. 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.
Last reviewed October 2026.