FDE PulseFDE jobs open 441New in 7 days 29Companies hiring 47Remote-friendly 24%Median US pay $216kTop hirer Databricks 125
VI

The newspaper of the Forward Deployed Engineer

Guides

Reverse ETL for FDEs: getting AI scores into Salesforce, where the sales team actually works

To a sales team, an accurate scoring model whose output sits only in a warehouse table might as well not exist.

In brief

  • Reverse ETL runs the opposite way to ETL: it takes processed data from the warehouse and pushes it into CRMs, Zendesk and internal tools.
  • For an FDE, this is the step that turns AI output into something sales and support teams see on the screens they use every day.
  • Three common mistakes: syncing the whole table every run, overwriting fields users are editing, and underestimating connector work.
ShareLinkedInFacebookX
GraphicGetting AI results from the warehouse into the CRM
  1. 1AI output tableThe model writes the score, reason and scoring time to the warehouse every night
  2. 2SQL modelJoin the CRM key, round the score, trim length to fit the target field
  3. 3Change diffCompare with the previous snapshot and keep only records that actually changed
  4. 4Scheduled syncSend to the CRM API; update the snapshot only when the send succeeds
  5. 5Read-only field in the CRMSales see the score on the account page and give feedback through a separate field

AI output only has value once it completes the last mile into the exact field users are looking at.

Graphic: FDE Times

You have just put a churn prediction model into production. Scores are calculated every night and written neatly to a table in the warehouse, and accuracy on the test set is solid. Two weeks later, the head of sales asks: “Where do I see that score?”

That question shows the project is only halfway done. The sales team lives in Salesforce, the support team lives in Zendesk, and nobody opens your dashboard. An AI result that has not appeared on the screens people use every day has not yet created any value for the customer.

For an FDE, this final stretch often decides whether a deployment succeeds or fails. The technique for doing it has a name: reverse ETL.

Reverse of what?

To understand reverse ETL, look at the forward direction first. In ETL, as Snowflake describes it, data is extracted from source systems such as applications, databases or APIs, transformed, then loaded into a central store. Data flows from many places into one.

Reverse ETL is the mirror image of that flow: processed data in the central store is moved regularly back into the operational systems where people work. Typical destinations are CRMs such as Salesforce or support tools such as Zendesk, and a very familiar task is pushing customer segments from the warehouse into Salesforce.

In FDE work, the output of an AI model is exactly that “processed data”. Churn scores, ticket classification labels and call summaries all need to travel this last mile.

A worked example, from score table to CRM field

Imagine your customer is a B2B software company with 20,000 accounts in Salesforce. Your model writes results every night to the table ai_outputs.churn_scores, containing an internal account ID, a score from 0 to 1, a one-sentence reason generated by an LLM and the scoring time.

The goal: three new fields appear on every account page in Salesforce, AI_Churn_Score__c, AI_Churn_Reason__c and AI_Scored_At__c. Sales open an account and see them immediately, without switching tabs.

The first step is to write a model, meaning a dataset defined in SQL whose structure matches the target system. Hightouch’s documentation describes a model in exactly these terms: a reusable dataset written in SQL, dbt or a visual builder. This is the layer where you shape the AI output before sending it.

-- model: churn_scores_for_salesforce
select
  m.salesforce_account_id          as sf_id,
  round(s.churn_score, 2)          as ai_churn_score,
  left(s.reason, 255)              as ai_churn_reason,
  s.scored_at                      as ai_scored_at
from ai_outputs.churn_scores s
join crm_mapping.accounts m
  on s.account_id = m.internal_account_id
where s.scored_at = (select max(scored_at) from ai_outputs.churn_scores)
  and m.salesforce_account_id is not null

Note three small decisions in that SQL. The join with the mapping table deals with the fact that internal IDs are not Salesforce IDs. The round function keeps the score readable, and left(..., 255) trims the reason to fit the length limit of the text field.

These details sound trivial, but they are exactly what makes a sync fail at 2am.

Why not send all 20,000 rows every night?

The second step is the sync. According to Hightouch’s documentation, a sync delivers modelled data to sales, support and internal tools, on a schedule or when the data changes. The tool runs on the warehouse itself, so customer data is not copied into a separate system.

The naive approach is to send all 20,000 rows every night. Metabase points to exactly this trap: sync only the records that have actually changed, so you do not exhaust the destination system’s API rate limit. Suppose only about 300 accounts each night have a score different from the day before: you send 300 updates instead of 20,000, or 1.5% of the volume.

If you build it yourself, a simple diff mechanism is to keep a snapshot table of the last sync. Use is distinct from rather than <>, because <> returns NULL and misses changes when either value is empty:

select c.*
from churn_scores_for_salesforce c
left join sync_state.last_sent l on c.sf_id = l.sf_id
where l.sf_id is null
   or c.ai_churn_score  is distinct from l.ai_churn_score
   or c.ai_churn_reason is distinct from l.ai_churn_reason

Update last_sent only when the CRM API returns success. Failed records then reappear automatically in the next diff instead of quietly disappearing.

Who is allowed to edit this field?

The third step is the one most often skipped: data ownership. Airbyte notes that with two-way sync you must handle conflicts when a record is edited in both systems. Picture a salesperson who finds a score of 0.82 absurd, changes it by hand to 0.3, and then watches that night’s sync overwrite it.

The safe approach is to avoid two-way sync by design. The AI_* fields are owned by the warehouse and set to read-only for users in the CRM. If sales want to give feedback, create a separate field such as AI_Feedback__c, owned by users, and pull it back into the warehouse with ordinary ETL.

That feedback in turn becomes valuable data for evaluating the model.

Field Written by Data direction
AI_Churn_Score__c, AI_Churn_Reason__c Warehouse Warehouse → CRM (reverse ETL)
AI_Feedback__c Sales in the CRM CRM → Warehouse (ETL)
Owner, Stage Sales in the CRM Not synced back

The customer should sign off on this short table before you switch the sync on. When someone asks why a number was overwritten, the answer is already on paper.

Steps to take on the customer site

In practice the sequence runs as follows. Start by sitting next to end users and asking which screen, which field and what time of day they want to see the result. Then check the identifier mapping table between the warehouse and the CRM: an account with no matching key has nowhere for its score to be written.

Next, write the SQL model to match the exact types and lengths of the target fields, then build the mechanism that sends only changed records. Agree the field ownership table with the customer, run a test on a CRM sandbox with a few dozen records, and only then switch on the production schedule, with alerts when the error rate rises.

If the customer’s tool is not Salesforce or Zendesk but an internal system, allow extra time. Portable acknowledges that integrating with existing tools can be complex and laborious, and some tools need custom-built connectors.

Before reading on, test yourself: if you drop the left function and the LLM generates a 300-character reason, which field breaks, and what happens to that record the following night? Answer: AI_Churn_Reason__c is rejected, last_sent is not updated, so the account reappears in every diff and fails every time.

That is exactly why error-rate alerts must exist from day one. A “resend on failure” mechanism is only safe when someone can see repeated failures.

Mistakes that make a project look finished when it is not

The most common mistake is sending the whole table on every run, which works fine in the first week and then hits the rate limit as the number of accounts grows. Next comes leaving AI fields editable by users, creating conflicts nobody can trace.

Another mistake is sending raw output: a score of 0.8234719 and a 2,000-character reason that nobody reads. Then there is estimating integration work as if every destination had a ready-made connector. The subtlest mistake is never checking whether users actually look at the field.

If you have worked on a project like the churn example above, do not write “built a churn model” on your CV.

A good model solves the data problem. Reverse ETL solves the rest: making sure that on Monday morning, a salesperson opens an account and knows straight away whom to call first.

Was this article useful?

Use with your AI assistantAsk Claude ↗Ask ChatGPT ↗
5 sources
Read next on the roadmap · Stage 5: DeploymentSCD Type 2 in practice: keeping customer and product history with SQL and dbt snapshotsOne UPDATE overwrites a customer's attributes and quietly rewrites last year's revenue figures along with them.