Now part of ingestr

Reverse ETL

Your warehouse knows. Now your CRM does too.

Tested tables from your warehouse, synced into the tools your teams use.

Trusted by forward-thinking teams

Use cases

What you model, where it gets used.

churn_riskHubSpot · Companies

Customer success sees who is at risk before the renewal call.

lead_scoreSalesforce · Lead

Sales works the best leads first.

seats_usedAttio · Companies

Account owners see usage next to the deal.

lifetime_valueCleverTap · Profiles

Campaigns reach your best customers, not everyone.

In your repo

One asset. Pointed the other way.

The same ingestr asset that loads your sources, with a CRM as the destination. It runs after the models it reads.

name: sync_contacts_to_hubspot
type: ingestr

depends:
  - marts.customer_health

parameters:
  source_connection: my-snowflake
  source_table: 'marts.customer_health'

  destination: hubspot
  destination_connection: my-hubspot
  destination_table: 'contacts?id_property=email'
  incremental_strategy: merge

columns:
  - name: email
    primary_key: true

$ bruin run

  1. marts.customer_health4.2s
  2. not_null · email
  3. accepted_values · churn_risk
  4. sync_contacts_to_hubspotmerge on email

Checks passed. HubSpot is up to date.

Strategies

Five ways to write.

Pick one per asset. Each destination takes the ones its API supports.

merge

Update the match, or create it.

The usual choice.

update

Update matches only.

No match: rejected, never created.

append

Always create.

Scope each run to new rows.

delete

Remove the matches.

Archive or hard delete, per API.

replace

Mirror the source.

Removes what is not in it.

Guardrails

Bad data stays in the warehouse.

  1. marts.customer_health
  2. sync_contacts_to_hubspot
  3. HubSpot

$ bruin run

  1. marts.customer_health4.2s
  2. accepted_values · churn_risk12 rows
  3. sync_contacts_to_hubspotheld

1 check failed. Nothing was sent to HubSpot.

Refused up front

A strategy the destination can't do is rejected before the run starts.

Rejects, reported

Rows the API won't take are listed. Fail at the end, stop at the first, or skip.

Nulls, your call

With write_nulls: false, a NULL leaves the stored value alone.

Safety guard

A run with zero source rows removes nothing.

The platform

Part of the Bruin platform.

Runs on ingestr, the engine that loads your sources, in the same repo as your models and checks.

Frequently asked

Questions about reverse ETL.

What is reverse ETL?

Reverse ETL moves data out of your warehouse and into the tools teams work in, such as a CRM. In Bruin it is ingestion in the other direction: the warehouse stays the source of truth, and each row becomes a record in the tool.

Which tools can Bruin reverse ETL write to?

HubSpot (contacts, companies, deals, custom objects and associations), Salesforce (any standard or custom object), Attio (people, companies, deals and custom objects) and CleverTap (user profiles and events).

Can Bruin sync Snowflake or BigQuery to HubSpot or Salesforce?

Yes. Any source ingestr reads can feed a reverse ETL destination, so Snowflake, BigQuery, Databricks, Postgres or even a CSV file can sync into HubSpot or Salesforce.

How does Bruin match warehouse rows to CRM records?

You name the field to match on in the destination table, such as email in HubSpot or an External ID field in Salesforce, and mark the source column that holds the value as the primary key.

Can it update records without creating new ones?

Yes, with the update strategy: matching records are updated and rows with no match are rejected, never created. merge updates or creates, and append always creates.

What happens when HubSpot or Salesforce rejects a row?

By default Bruin sends every valid row, then fails the run and lists the rejects. Set reject_mode to fail_fast to stop at the first bad row, or to skip to report the rejects and still succeed.

Will a NULL in the warehouse wipe a field in the CRM?

By default, yes: a NULL is written through and clears the field. Set write_nulls to false and Bruin leaves the stored value untouched.

Can a sync delete records in the CRM?

Only if you choose a strategy that does. delete removes the records you match; replace removes every record not in the source, including ones created by hand, so use it only when the source is complete. A run with zero source rows removes nothing.

Are reverse ETL writes transactional?

No. The destination API decides what is possible, so a run that fails partway may already have written some records, and Bruin cannot roll them back. It reports the rows the API rejected.

Do the fields need to exist in the CRM first?

In HubSpot, yes: properties must already exist. Salesforce maps source columns to fields by API name, and Attio expects columns to match existing attributes.

How is Bruin reverse ETL different from a separate reverse ETL tool?

It is not a second tool: a sync is an ingestr asset in the same repo and pipeline as your ingestion, SQL models and quality checks. It runs after the models it depends on, so a failing check holds the sync.

Is it open source, or do I need Bruin Cloud?

Reverse ETL ships in ingestr, which is source-available under the Functional Source License, and runs from the open-source Bruin CLI anywhere a binary runs. Bruin Cloud adds schedules, run history and alerts.

How fast does it write?

Rows go out in bulk through each API's batch endpoints, within its rate limits, and Salesforce uses the Bulk API by default. The destination sets the pace: Attio, for example, allows about 25 writes per second.

From the warehouse to the CRM.

$100 in credits and 50 AI tasks. No credit card.

A demo walks through your own data.

Sign up to our newsletter

Practical updates on open-source data pipelines, AI analysts, governance, and what we are shipping at Bruin.

The signup form is hosted by Brevo. Allow marketing cookies to load it.