Salesforce Flow Drift — Runbook
Detect and remediate the two ways a Salesforce-triggered ramp and its Flow fall out of step: a live ramp with no Flow (it can never fire), and an active Flow owned by no ramp (it fires into nothing).
Canonical sources. The sweep is devtools/sf-flow-sweep.ts — read it, not this page, for the classification rules. Provisioning lives in app/api/ramps/helpers/ramps_trigger_helper.py; the label stamp is build_salesforce_flow_label there. The nonprod org bindings are in infra/app/envs.ts.
foo9 is shared. Shared dev and every PR preview publish into the same Salesforce org (00DDo000001B9fNMAS). Deleting a Flow there can break a colleague's preview run. Confirm ownership before removing anything.
TL;DR
Provisioning failed open. Publish committed on a response that reported neither success nor error, so the ramp went live with nothing in Salesforce. Now it fails loudly instead.
Previews publish and never clean up. Their databases are destroyed on PR close; their Flows are not. The Flow Label now names the publishing environment so a sweep can tell whose it is.
One read-only command. bun devtools/sf-flow-sweep.ts --org foo9 pairs both directions and never writes.
Pairing rule
A ramp's Flow api name is its uuid with the dashes stripped, so pairing is an exact lookup, never a name search:
ramp 164a66cd-f4e3-4ebb-afd1-294db0184905
flow ONRAMP_164a66cdf4e34ebbafd1294db0184905FlowDefinitionView is a plain SOQL object — querying it with --use-tooling-api fails with INVALID_TYPE.
Direction A · A live ramp with no Flow
What it looks like
The ramp reads as live in the UI, its workflow is published, and nothing in the product reports the gap. It simply never fires.
Root cause
Publishing calls RampsTriggerHelper.configure_trigger, which drives the Salesforce CREATE_TRIGGER integration and then inspected only the response's status code. Three things made "no answer" indistinguishable from "success":
create_resp.get("status_code", HTTP_SUCCESS) defaulted a missing key to 200, so any unanticipated response shape passed the gate.
When a Prismatic execution ends with no results payload, convert_execution_result reports status_code: 200 with message: "In Progress" and data: None. Accepted is not completed. That status code stays 200 on purpose — callers gate on != 200 as often as on >= 400, and several dereference data unguarded on the non-200 branch — so the signal a caller must read is message plus data, not the status.
The response already carried a completed flag. The publish path never read it.
Even a genuine 200 was never checked against the org, so no code path could notice the Flow was absent.
Publish then committed is_draft = False and is_paused = False and returned 200 to the browser.
Evidence
Three unpaused Salesforce ramps on dev vendor 1 had no Flow in foo9. Two of them published inside the org's Setup Audit Trail retention window and the trail holds zero rows for their uuids — no create, no delete. The Flow was never made, rather than made and later removed. Their trigger JSON is structurally identical to that of the nine correctly-paired ramps, so bad input is ruled out.
| ramp | owner | published | in trail window | trail rows |
|---|---|---|---|---|
Opps to Projects 164a66cd | josh@onramp.us | 2025-08-13 (v5) | no | — |
sfdc test dbebbc40 | ben.turner@onramp.us | 2026-03-25 (v2) | yes | 0 |
Test SF Workflow 1 3db3dca8 | jeff@onramp.us | 2026-03-31 (v1) | yes | 0 |
Only Opps to Projects matters much: it is on its fifth published version with a sixth draft open since 2025-11-20 — someone's real setup, not a throwaway. The other two are named like scratch tests and their owners should confirm before anyone re-publishes.
Remediate
Re-publish the ramp. Provisioning now either creates the Flow or fails loudly with the provider's own error, and the workflow is left unpublished rather than published without a working trigger.
Direction B · An active Flow owned by no ramp
Why they accumulate
Every PR preview points at the same foo9 org. A preview's Neon branch is discarded when its PR closes; the record-triggered Flows that preview published stay behind, active. Of 27 pre-existing ONRAMP_* Flows in foo9, 9 pair to a live dev ramp and 18 pair to no ramp row in the dev database at any vendor in any state. Eight of the 18 are labelled with bare PR or ticket numbers (3383, 3555, 3555_2, 3559_3, 3597, 4292, 4506_test_2, ONRAMP_3445).
A second, smaller source is app-side: archiving a ramp deprovisions its Flow, but a failed deprovision used to be swallowed, leaving the ramp archived in OnRamp and its Flow active in the org. A failed deprovision now raises, and archive_ramp rolls back, so the ramp and its Flow stay in step.
An unconfirmed deprovision deliberately does not raise. archive_ramp rolls the whole transaction back on a raise, so treating "no confirmation" as fatal would make a ramp permanently unarchivable — including any ramp whose Flow is already absent, which can never produce a confirmation. Those are logged at warning for this sweep to pick up instead.
Why a sweep was not previously safe
A Flow's api name carries the ramp uuid and nothing else. Seeing only the org, a sweep cannot tell a closed preview's leftovers from a live ramp's Flow — so it could not delete anything without risking someone's environment. Two signals fix that:
build_salesforce_flow_label prefixes the publishing environment — preview-10383__Opps_to_Projects. Previews are already preview-<N>, so a Flow names the PR that made it. Production is exempt: a production org belongs to one customer who sees these labels in their own Setup UI, so there is nothing to disambiguate and the token would only spend part of the 80-character budget.
A Flow whose uuid appears in any environment's database is owned. Pass one --db per environment you can reach.
An unowned preview-<N> Flow whose PR is still open is spared: that preview is a live environment whose Neon branch the sweep has no credentials for, which is not evidence of abandonment.
The procedure
1 · Sweep
Read-only. Defaults to ~/.pgpass-dev as the one environment it knows.
bun devtools/sf-flow-sweep.ts --org foo9Add an environment per database you can reach, so its Flows pair instead of looking orphaned. Prefer --db-file, which keeps the URI out of the process list:
bun devtools/sf-flow-sweep.ts --org foo9 --db-file ~/.pgpass-dev --jsonVerdicts: PAIRED, SPARE_OPEN_PREVIEW, SPARE_UNKNOWN_PROVENANCE, SPARE_NEVER_PUBLISHED, REAPABLE. Anything the sweep cannot attribute is spared, so a clean run today reports 0 reapable — every existing Flow predates the label stamp.
SPARE_NEVER_PUBLISHED means the Flow's ramp row exists but that ramp has never published a workflow, so the row is not evidence of ownership. The likely cause is a publish that was refused for an unconfirmed provision: the ramp rolled back to draft while the org kept what the run had already created. Grep the Flask log for [sf-trigger-unconfirmed-create] and that ramp's uuid to confirm.
Verify
After a publish, the ramp and the org agree — the sweep reports the Flow as PAIRED, and its Label starts with the environment that published it:
bun devtools/sf-flow-sweep.ts --org foo9 --jsonTroubleshooting
--use-tooling-api was passed. FlowDefinitionView is a plain SOQL object; drop the flag.
The backslash underscore-escape does not survive the shell quoting. Use LIKE 'ONRAMP%' and filter on the prefix client-side.
Expected for Flows published before the Label carried an environment. Newly published Flows classify automatically; the backlog needs attribution by hand.
That is the fix working. Read the error — it carries Salesforce's own message. "Accepted but never confirmed complete" means the Flow was probably not created; publish again.