SfSkills Domains Docs GitHub

Home / flow / record-triggered-flow-patterns

record-triggered-flow-patterns

Use when designing or reviewing Salesforce record-triggered Flows, especially before-save vs after-save behavior, entry criteria, recursion avoidance, and when to escalate to Apex.

Open the skill folder on GitHub


Use this skill when the hard part is not "how do I automate" but "what is the right record-triggered pattern for this object and this event?" The purpose is to choose the correct trigger context (before-save vs after-save), control how often the flow runs (entry criteria + prior-value checks), align with Salesforce order-of-execution semantics (so the flow plays well with Validation Rules, Apex triggers, and duplicate rules), and know when the answer is to escalate to Apex rather than force more logic into Flow.

Getting this choice wrong is expensive. The wrong trigger context causes recursion that produces runaway automation under bulk load. The wrong entry criteria causes the flow to run on every save, burning transaction budget on changes it doesn't care about. The wrong order-of-execution assumption causes Validation-Rule-after-field-update surprises that surface in production as "the rule worked yesterday."

Before Starting

Check for salesforce-context.md in the project root. If present, read it first.

Gather if not available:

Questions to Ask Before Configuring

Ask these before opening Flow Builder. Each one decides an element in the XML, and each one traces to a gotcha that only shows up in production. An agent that skips them ships a flow that passes deploy and fires twice.

AskWhy it mattersWhat a good answer adds
"Does this change fields on the record being saved, or does it touch anything else?"Same-record-only is the only case that fits RecordBeforeSave; everything else forces RecordAfterSaveThe triggerType, and whether the flow can have DML elements at all (references/gotchas.md § before-save assignments)
"Is this a state or a transition — 'while Status is Approved' or 'when Status becomes Approved'?"Decides doesRequireRecordChangedToMeetCriteria; the wrong answer creates a duplicate record on every later editThe <start> block: filters + doesRequire… for a transition, filterFormula for a state test (references/gotchas.md § transition vs state)
"What else already runs on this object, in which save context, at which triggerOrder?"Two flows in one save context with no declared order is undefined sequencing, and a before-trigger writing the same field always wins over a before-save flowThe triggerOrder value and the list of fields this flow may not own (references/gotchas.md § triggerOrder ties)
"Does anything downstream update this record again?"An after-save flow that re-saves its own object re-enters the save procedure, and steps 9–17 are skipped on that pass — so the behaviour differs from the first saveThe recursion guard: marker field, changed-field criteria, or a move to before-save (references/gotchas.md § recursive save)
"If the related-record write fails at 2am, who finds out and how?"Without a faultConnector the interview stops and the error is invisible outside the flow error emailA fault path on every Create/Update/Delete/Get, landing on Application_Log__c per templates/flow/FaultPath_Template.md
"Does any of this work need to happen later — hours or days after the save?"A scheduled path is a FlowScheduledPath, batches up to 200 interviews, and an AsyncAfterCommit path runs post-commit where it cannot roll the save backThe scheduledPaths block with offsetUnit, recordField, and a deliberate maxBatchSize (references/gotchas.md § scheduled path batching)
"When a record of this type is deleted, does anything have to be preserved or cleaned up?"RecordBeforeDelete is the only delete context that exists — there is no after-delete record-triggered flow, and cascade deletes may never reach itA third flow (or an explicit decision not to have one) instead of discovering the gap after a mass delete (references/gotchas.md § no after-delete)
"Is this flow's recordTriggerType Create, Update, or CreateAndUpdate?"It decides which $Record parameters the FlowTest Start test point may carry — Create takes InputTriggeringRecordInitial only, Update takes both; carrying the wrong pair fails deploy with a different error for each wrong shapeA FlowTest that matches the flow's actual trigger shape on the first try instead of two rounds of guessing from the error text (references/metadata-examples.md § 4, references/gotchas.md § 11)

What a proper configuration adds over just building the flow: the trigger context matches the requirement instead of defaulting to after-save, entry criteria encode the business transition rather than a state that stays true, every failure path writes a row someone can query, and the object's save context has a declared run order instead of an accidental one.


Core Concepts

Before-Save Is For Fast Same-Record Changes

Before-save record-triggered flows are optimized for updating fields on the record currently being saved. They execute IN THE SAME DML statement as the save — no extra DML, no recursion risk for same-record writes. They should be the default choice when the requirement is enrichment, normalization, or lightweight decisioning on that same record.

What before-save can do:

What before-save CANNOT do:

UNVERIFIED (2026-09-05): the Metadata API guide describes RecordBeforeSave as running "to make more updates to that record before it's saved to the database" (api_meta.txt L72539–72542) and defines scheduledPaths on FlowStart without restricting it by triggerType (L72465–72466). The prohibitions in that list are Flow Builder's, documented on help.salesforce.com, which cannot be fetched. Treat them as design rules — the checker enforces the DML ones — but confirm in a sandbox before telling a customer the platform blocks a specific one.

After-Save Is For Committed Side Effects

After-save flows exist for related-record writes, notifications, subflows, actions, and work that depends on the record being committed. They are more flexible, but they are also more expensive (each related write is a new DML statement) and easier to design badly (recursion risk, fan-out risk).

When after-save is the ONLY option:

Entry Criteria Is A Design Tool, Not Just A Filter

A record-triggered flow that runs on every update without clear entry criteria becomes hidden operational debt. Three layers of filtering matter:

  1. Entry criteria on the Flow itself — "Record was updated AND Status = 'Approved'". This is the cheapest filter; records that don't match never start the interview.
  2. "Optimized start settings" — Salesforce evaluates the criteria BEFORE the full Flow loads; faster than running the Flow and exiting early via Decision.
  3. Field-change conditions — "Record was updated AND Status changed from anything other than 'Approved' to 'Approved'". Prevents the Flow from re-running when the record is edited for unrelated reasons.

Rule: Never design a record-triggered flow without entry criteria. "Runs on every save" is a smell even when it happens to match the requirement today — the requirement will narrow, and undoing loose entry criteria at scale is hard.

Order Of Execution Still Applies

Record-triggered flows participate in Salesforce's documented order of execution. Key points:

When designing a new flow, ALWAYS check existing automation on the object first (list_flows_on_object, tooling_query on ApexTrigger, list_validation_rules). Not knowing what's already there is a scale-invariant mistake.

$Record__Prior Makes Recursion Control Possible

On update triggers, $Record__Prior holds the record's field values BEFORE the current save. Using it in entry criteria ("ISCHANGED(Status)" or equivalent) is the canonical way to keep the flow from re-firing on unrelated edits — including edits it makes to itself.

Flow entry criteria:
  {!$Record.Status} = 'Approved'
  AND {!$Record__Prior.Status} != 'Approved'

This condition fires exactly once per actual status-change-to-approved, regardless of how many times the record gets edited afterward.

Common Patterns

Pattern 1: Before-Save Enrichment Pattern

When to use: Only the triggering record needs calculated defaults, normalized values, or field derivation.

Structure:

Before-save record-triggered Flow:
    Entry criteria: (condition to filter records that need enrichment)
    └── [Decision: does field X need normalization?]
         └── Yes → [Assignment: $Record.Field_X = normalized value]
    → [Decision: does field Y need defaulting?]
         └── Yes → [Assignment: $Record.Field_Y = default value]
    (No DML — the assignments persist as part of the save)

Why not the alternative: An after-save flow would spend extra DML, trigger re-save recursion, and run on every edit instead of the relevant ones.

When to use: Saving the parent record should create, update, or notify something else.

Structure:

After-save record-triggered Flow:
    Entry criteria: (narrow condition, e.g. "Amount > 50k AND Stage = 'Closed Won'")
    └── [Get Records: related Account]
    └── [Decision: is Account elite-tier?]
         └── Yes → [Create Records: Retention_Task__c]
                  → [Create Records: Executive_Followup_Task__c]
                  → [Send Custom Notification]
    (Explicit field-change check in entry criteria prevents re-firing on unrelated edits)

Critical: the entry criteria MUST include a field-change check if the Flow updates the SAME record (even indirectly via related Apex). Without it, the after-save flow retriggers itself.

Pattern 3: Escalate To Apex For Complex Transaction Logic

When to use: The automation needs deep branching, complex collections, callouts per record, precise recursion control, or interaction with an existing Apex trigger framework.

Signals that Flow is no longer the right boundary:

Approach: Migrate the logic to an Apex trigger handler (see apex/trigger-framework). Keep the Flow ONLY if it's the orchestration entry point; move the "work" to Apex. When in doubt, run automation-migration-router --source-type process_builder (equivalent principles apply).

Pattern 4: Fast-Field-Update Plus Async Fan-Out

When to use: Same-record enrichment is cheap (before-save) but related-record work is heavy and doesn't need to be in the save transaction.

Structure:

Before-save Flow: sets fields on $Record, publishes Platform Event with record id + change context
Platform-Event-Triggered Flow: processes the event async, does the heavy related-record work

Decouples the user-facing save (fast) from the downstream side effects (async, retry-able). Great for fan-out scenarios that would exceed DML limits inline.

Decision Guidance

SituationRecommended ApproachReason
Update fields on the saving record onlyBefore-save Flow (Pattern 1)Fastest, simplest, no DML, no recursion
Normalize fields on saveBefore-save FlowSame reasons
Create or update related records after commitAfter-save Flow (Pattern 2)Related side effects require after-save
Need field-change detectionUse entry criteria + $Record__Prior comparisonPrevents irrelevant re-runs and recursion
Heavy orchestration or deep transaction control requiredApex trigger handler (Pattern 3)Record-triggered Flow is not always the best boundary
Fan-out exceeds 10 related records per saveAsync via Platform Events (Pattern 4)Keep save transaction fast; defer the heavy work
Object already has Apex triggersConsolidate: Flow OR Apex, not both on same eventCoexistence is possible but an audit liability; prefer one owner
Need callout per recordAfter-save Flow with HTTP Action OR Apex + @future(callout=true)Inline HTTP from Flow is fine for low volume; use async for bulk
Process Builder being migratedautomation-migration-router --source-type process_builderDon't reinvent the migration logic

Recursion Avoidance Recipe

When an after-save Flow updates records on the same sObject:

  1. Entry criteria must include a field-change check: ISCHANGED(Status) or $Record.Status != $Record__Prior.Status.
  2. Update ONLY fields the flow's own condition doesn't depend on. If the entry criteria is "Status changed" and the flow sets Status, it re-fires forever.
  3. Use a marker field if the flow must update a field that retriggers it. Marker: Last_Processed_At__c — set it, then bail out on next fire if it's fresh.
  4. Consider before-save if the update is same-record. Before-save doesn't recurse because the value is written as part of the original save.

Review Checklist

  1. Inventory the object's save contexts. Query FlowDefinitionView for every active record-triggered flow on the object (references/metadata-examples.md § 8 has the SOQL), plus ApexTrigger and validation rules. You need the existing TriggerOrder values before you can pick yours.
  2. Answer the seven questions above. The first two fix triggerType and recordTriggerType; the third fixes triggerOrder; the rest decide whether you need a scheduled path, a fault path, and a delete flow.
  3. Fill in the skeleton, don't freehand the XML. Start from templates/flow/RecordTriggered_Skeleton.flow-meta.xml and use the matching worked flow in references/metadata-examples.md — § 1 before-save, § 2 after-save with entry conditions plus a scheduled path, § 3 before-delete. Route every Create/Update/Delete/Get faultConnector per templates/flow/FaultPath_Template.md.
  4. Write the FlowTest before you activate. references/metadata-examples.md § 4 shows the InputTriggeringRecordInitial / InputTriggeringRecordUpdated pair — the only mechanism that proves your entry criteria fire on the transition rather than on the state. Match the parameters to recordTriggerType first: Create takes InputTriggeringRecordInitial only (§ 4.2); Update takes both (§ 4.1). Guessing from the deploy error costs a second round-trip (references/gotchas.md § 11).
  5. Run the checker on the source tree, then a check-only deploy: python3 skills/flow/record-triggered-flow-patterns/scripts/check_record_triggered_flow_patterns.py --manifest-dir force-app/main/default, then sf project deploy validate --manifest manifest/package.xml. The checker catches missing faultConnectors, before-save flows carrying DML, after-save flows re-saving their own object without changed-field criteria, Gets inside loops, and unset triggerOrder when the manifest has more than one flow on an object.
  6. Activate deliberately and verify twice. Flip <status> to Active, redeploy, then confirm in Flow Trigger Explorer and in the FlowDefinitionView query that the run order and the active version are what you shipped — not what a stale FlowDefinition pinned (references/metadata-examples.md § 5).
  7. Prove the transition once with real data. Drive the record through the change, then read the Task/related-record channel and Application_Log__c. Two side-effect records for one transition is the doesRequireRecordChangedToMeetCriteria bug, not a coincidence.

Salesforce-Specific Gotchas

  1. After-save updates to the triggering record can retrigger automation — this is one of the most common Flow recursion smells. Before-save avoids it entirely for same-record writes.
  2. Before-save cannot replace all trigger behaviors — if the logic needs related-record work, notifications, or callouts, the design must move to after-save or another boundary.
  3. A broad start condition becomes hidden operational cost — flows that fire on every edit are harder to debug, more likely to clash with other automation, and more expensive to refactor later.
  4. Multiple automations on one object still interact — record-triggered flows are not isolated from Apex triggers, duplicate rules, or validation behavior. When adding a new flow to an object, list_flows_on_object + tooling_query for existing triggers is non-negotiable.
  5. $Record__Prior has no meaningful value on a create — the platform's own transition switch, doesRequireRecordChangedToMeetCriteria, is defined against "the triggering update" (api_meta.txt L72322–72325), which is why the after-save example in references/metadata-examples.md sets recordTriggerType to Update rather than CreateAndUpdate. Guard prior-value comparisons with a create check. UNVERIFIED (2026-09-05): whether a prior-value comparison on a create throws or silently evaluates against null is not stated in api_meta.txt or apexdev.txt; do not promise either behaviour.
  6. Before-save runs before Validation Rules — a before-save assignment to an invalid value will be caught by a VR, sometimes with a confusing error pointing at the field the user didn't touch.
  7. After-save runs after Apex triggers — if an Apex before/after trigger changes the record, the after-save flow sees the post-Apex state. Don't assume the flow sees the user's input.
  8. Process Builder on the same object runs in a different order than Flow — orgs mid-migration between PB and record-triggered Flows have unpredictable event sequences; complete the migration before adding more automation.
  9. Managed-package record-triggered flows are opaque — you can see that a Flow exists via list_flows_on_object but the contents may be locked. Coordinate with the package vendor before adding more automation on the same save context.
  10. Salesforce's "Flow Trigger Explorer" shows ordering — admins should use it during design, not just during incident response. Ordering disputes are easier to resolve before deploy.
  11. A FlowTest copies the wrong $Record parameters from the wrong example — a Create-triggered flow's Start test point takes InputTriggeringRecordInitial only, not the InputTriggeringRecordInitial / InputTriggeringRecordUpdated pair that an Update-triggered flow's FlowTest uses. Carrying InputTriggeringRecordUpdated on a Create-triggered flow fails deploy either way — with a missing-parameter error alone, or with an incompatible-parameter error once InputTriggeringRecordInitial is also added (references/gotchas.md § 11; UNVERIFIED for CreateAndUpdate).

Anti-Patterns (see references/llm-anti-patterns.md)

Proactive Triggers

Surface these WITHOUT being asked:

Output Artifacts

ArtifactDescription
Trigger-context recommendationClear before-save, after-save, or Apex choice with reasons
Record-triggered flow reviewFindings on entry criteria, recursion risk, order-of-execution fit, coexistence
Refactor planSpecific changes to move a flow into the right trigger pattern
Consolidation proposalWhen multiple flows on the same object should merge

Reference Files

FileRead it when
references/metadata-examples.mdYou are writing or reviewing actual *.flow-meta.xml: three complete flows on Opportunity (before-save, after-save with entry conditions and a scheduled path, before-delete), a fourth Create-triggered before-save flow on Lead (§ 4.2), two FlowTests covering both the Update-pair and Create-only-Initial shapes, the FlowDefinition activation trap, package.xml, deploy order, and the FlowDefinitionView verification query
references/gotchas.mdThe flow deploys and still misbehaves — duplicate side effects, recursion, ties in run order, scheduled paths that batch differently than expected
references/examples.mdYou want the narrative before/after: what a practitioner built first, what broke, and the corrected shape
references/llm-anti-patterns.mdYou are reviewing flow advice or generated XML produced by an AI assistant, or self-checking your own output
references/well-architected.mdYou need the pillar framing, the tradeoffs, or the source list behind a claim in this skill
templates/record-triggered-flow-patterns-template.mdYou are recording the design decision — trigger context, pattern choice, recursion guard — for review
scripts/check_record_triggered_flow_patterns.pyBefore every deploy. --manifest-dir <source tree>; exits non-zero on any finding

Package files