Send results back to where the business works.
The warehouse knows the answer. The people who need it live somewhere else. Account health, product usage and a churn score are computed nightly and sit in a table nobody in sales has ever opened.
The answer exists. It just never reaches the people who act on it.
The score is computed in a notebook, exported by hand, and pasted into the CRM by whoever has time. A generic API call that writes it back reports success when Salesforce has quietly rejected half the records.
So the sales team keeps working from last quarter's list, and the data team keeps answering the same question by email.
What it replaces
- A notebook run by hand → your hosted model, called on the same schedule as the load
- A CSV pasted into the CRM → Salesforce upsert on your own key
- “Succeeded” with rejected records → every rejected record named, with Salesforce's reason
- A dashboard nobody opens → a shared Sheet or an email to a named owner
Warehouse in, the answer out to three places at once.
One output port can feed any number of nodes, so the same scored table goes to Salesforce, a Sheet and an inbox in one run.
Built from these nodes, each with its own documentation page
- Snowflake input: the scored table, with its columns read without waking a warehouse
- API Lookup: your own model, hosted where you run it, called for each row
- Manage Columns: column names lined up with the target's field names
- Salesforce output: insert, update, upsert or soft delete, 200 records a request
- Google Sheets output: overwrite or append a tab, and create it if it is missing
- REST API (Write): any endpoint, one record a request or many in a batch
- Send Email: the rows themselves, attached as CSV or Excel
Score it, line it up, and write it back.
A dedicated Salesforce node exists for one unglamorous reason: Salesforce answers HTTP 200 on a request in which individual records failed. A generic writer sees the 200 and reports success; this one reads the result for every record.
Read the scores.
Pick the table from a live browser. Snowflake reports its columns without starting a warehouse, so configuring the node costs nothing.
Score with your model, if the score is a model.
Keep the model where you already host models, behind an HTTPS endpoint. API Lookup sends it each row and adds its answer as new columns. If the score is a formula, write it in Formula instead.
GET /score/${account_id}
Line up the field names.
Each column's name is the Salesforce field it writes, so rename them with Manage Columns.
score → Churn_Score__c account_ref → Legacy_Id__c
Write to Salesforce with upsert.
Upsert on an External ID field creates what is missing and updates what exists, so a re-run never duplicates anything. Choose whether a rejected record fails the run or is skipped and reported.
Object: Account · Operation: upsert · External ID: Legacy_Id__c
Publish the same table where people look.
Google Sheets output overwrites a tab, or appends to a running log, and creates the tab if it is missing. Send Email groups by account owner and attaches each owner's own accounts.
write mode: Overwrite · create tab if missing: on
Nightly, after the load, and safe to run twice.
After the load, in a Flow
Put it after the warehouse load in the nightly Flow, so the scores are always computed from tonight's data.
A Test never writes
A Test run never writes to Salesforce, never sends an email, and skips any request that would change something in an API.
Never sent twice
A request Salesforce may already have processed is never sent again, because a resent insert is a second record. The run stops and names the batch, and an upsert makes the re-run safe.
Delete is a soft delete
Delete moves records to the Salesforce Recycle Bin, where your administrator can restore them. Trinium never deletes a record permanently.
A rejected record is named, with Salesforce's own reason.
Every record Salesforce rejects is reported individually in the run's Messages, in Salesforce's own words and with the fields it named. Under Fail, the batch that failed writes nothing and the run stops; under Continue, the good records land and the rejected ones are listed.
Salesforce has no rollback across requests, so batches already accepted stay written. That is why upsert is the operation to schedule.
salesforce_output: Salesforce rejected 1 of 200 record(s) — row 14: REQUIRED_FIELD_MISSING - Required fields are missing: [Name]
Scope worth knowing. Records go 200 a request through the Collections API, which is 500 API calls per 100,000 rows. Google Sheets has no transaction, so an Overwrite that fails part way leaves the tab partly written.
Bring the answer that never reaches sales.
Thirty days of the full Enterprise product on your own server, against your own data. If you would rather see it first, book a walkthrough with the people who build it.