Skip to content

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).

Owner: CRM / WorkflowsWhen to use: a ramp that never fires, or an ONRAMP_* Flow nobody claimsTriaged: 2026-08-04

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

🅰 Ramp with no Flow

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.

🅱 Flow with no ramp

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.

🅲 How to look

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_164a66cdf4e34ebbafd1294db0184905

FlowDefinitionView 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":

🚫 Absent status read as success

create_resp.get("status_code", HTTP_SUCCESS) defaulted a missing key to 200, so any unanticipated response shape passed the gate.

🚫 "In Progress" read as done

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.

🚫 `completed` thrown away

The response already carried a completed flag. The publish path never read it.

🚫 Nothing verified afterwards

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.

rampownerpublishedin trail windowtrail rows
Opps to Projects 164a66cdjosh@onramp.us2025-08-13 (v5)no
sfdc test dbebbc40ben.turner@onramp.us2026-03-25 (v2)yes0
Test SF Workflow 1 3db3dca8jeff@onramp.us2026-03-31 (v1)yes0

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:

🏷 Environment in the Label

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.

🔑 uuid is globally unique

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
2
Attribute
ask the owner
3
Reap
by hand, deliberately

1 · Sweep

Read-only. Defaults to ~/.pgpass-dev as the one environment it knows.

bash
bun devtools/sf-flow-sweep.ts --org foo9

Add 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:

bash
bun devtools/sf-flow-sweep.ts --org foo9 --db-file ~/.pgpass-dev --json

Verdicts: 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:

bash
bun devtools/sf-flow-sweep.ts --org foo9 --json

Troubleshooting

🚫 `sObject type 'FlowDefinitionView' is not supported`

--use-tooling-api was passed. FlowDefinitionView is a plain SOQL object; drop the flag.

🚫 A `LIKE 'ONRAMP\_%'` query returns 0

The backslash underscore-escape does not survive the shell quoting. Use LIKE 'ONRAMP%' and filter on the prefix client-side.

⚠ Everything reports `SPARE_UNKNOWN_PROVENANCE`

Expected for Flows published before the Label carried an environment. Newly published Flows classify automatically; the backlog needs attribution by hand.

⚠ Publish now fails where it used to succeed

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.

Internal documentation — gated behind Cloudflare Access.