Module 4 — Flows & Declarative Automation
Interview weight: 15–20% (present in every loop — even pure developer roles: "Flow or trigger, and why?" is asked in every sample we saw) · Estimated time: 5–7 sessions (~90 min each) Target: By the end, you can answer the declarative-first question with a decision matrix (not a preference), diagnose flow-vs-trigger execution-order bugs, design recursion-safe automation with ownership + bypass flags, and explain fault paths, limits, and when Apex is required — all in 2 minutes closed notes. Flow is the non-negotiable 2026 topic: interviewers' consensus is that developers must respect declarative-first, know when Flow wins, and know when Apex is required.
M0 — THE MAP (read this first, 5–10 min)
The one idea everything hangs on: FLOW IS CODE WITHOUT THE RIGHT TO BE SILENT
Every concept in this module — execution order, recursion, bulk, fault paths, limits, the flow-vs-trigger matrix, bypass flags — is a consequence of one realization:
A Flow is an automation that runs inside the same transaction, obeys the same governor limits as Apex, runs at a specific point in the order of execution (BEFORE before-triggers for before-save flows), and — unlike Apex — has no test classes, no static flags, and no try/catch unless you explicitly build the fault path. Everything a Flow can do wrong is what a junior Apex dev does wrong — but invisibly, in Setup, with no one reviewing it.
Think of it as a production line inside the org:
- The line = the order of execution: before-save Flow → before-triggers → validation → after-triggers → after-save Flow → assignment/escalation rules → workflow. Position on the line decides who wins when two automations touch the same field (the classic "the value is wrong" interview scenario).
- The operators = Flow types: Record-Triggered (before/after), Screen (a human in the loop), Schedule-Triggered (the night shift), Platform Event-Triggered (the radio), Autolaunched (a function call — from Apex, from other flows).
- The toolset = elements: Get Records, Update Records, Loops, Decisions, Formulas, Subflows, Fault paths, Invocable Apex (when the toolset is not enough, you hand the part to Apex and get the result back).
- The safety rail = entry conditions + change detection: the Flow only runs when it should — the "Is Changed" condition, the skip flag, the ownership rule. Without rails, the production line runs itself in circles (recursion) or double-works (dual ownership).
- The quota board = limits: 2,500 elements, 2,000 versions, shared 100 SOQL / 150 DML / 10,000 rows per transaction — Flow is not "unlimited", and bulk (200+ records) is where Flow chokes just like Apex.
- The alarm = the fault path: the one branch Flow doesn't give you by default. A Flow without a fault path is code without error handling — it fails silently and the org keeps operating wrong.
Why this map matters (the bridge): The 2026 interviewer consensus (agent 05 research): "Flow or trigger, and why?" appears in every loop sampled, even pure developer roles. The answer they want is the decision matrix, not a preference. Every incident in this module is a real org where someone chose wrong, forgot the order of execution, skipped the fault path, or let two automations fight — and the fix is always the same five disciplines:
- Know where your automation sits on the order of execution (and who wins).
- One owner per field/automation — entry conditions + change detection + skip flags.
- Bulk-aware design (200+ records, shared governor limits).
- Fault paths everywhere (never silent failure).
- The decision matrix — Flow when it fits, Apex when it doesn't, Invocable Apex as the bridge.
By the end of this module, "knowing it" looks like this: given any one of the 9 problems below, you can (a) name the mechanism that failed, (b) explain the fix on a whiteboard, (c) describe the corrected design (Flow structure or Apex decision) from memory, and (d) say which interview question it maps to.
The incidents (choose your own adventure — recommended order)
| # | Incident | The villain mechanism |
|---|---|---|
| 1 | The Flow That Fought the Trigger | Order of execution + dual field ownership |
| 2 | The Loop That Never Stopped | Cross-object recursion + missing change detection |
| 3 | The 500-Case Email Storm | Bulk + shared governor limits + no fault path |
| 4 | The Flow That Went Rogue at 3:00 AM | Schedule-Triggered Flow vs Scheduled Apex decision |
| 5 | The Wizard Nobody Could Open | Screen Flow access + debug blindness |
| 6 | The Silent Graveyard | No fault path — failures that never spoke |
| 7 | The Flow That Ate the Governor | Flow complexity limits + the Invocable Apex bridge |
| 8 | The Two Automation War | Automation governance + bypass patterns |
| 9 | 🏆 Capstone — The Org That Automates Itself | The multi-automation incident report |
Protocol reminder (from file 00): attempt in writing FIRST (≥2 hypotheses + 2 solution attempts), hard 45-min cap, hint ladder, then reveal, then REDO, then retrieval drill. The sealed answer sheet lives in
04b_Topic04_Flows_Declarative_Answer_Sheet.md. You are expected to fail. The failure is the task.
INCIDENT 1 — THE FLOW THAT FOUGHT THE TRIGGER
STAKES
Two weeks after a consultant "improved" the org with a before-save Record-Triggered Flow that sets Account.BillingCountry from the shipping data, the nightly report shows thousands of Accounts with wrong countries. The Apex trigger that sets the same field "worked for years." The consultant blames the trigger ("it must run after mine"). The dev who wrote the trigger blames the Flow ("it's overriding my logic"). The field is wrong on some records and right on others. Friday, 5:45 PM, postmortem at 9:00 AM Monday.
THE INCIDENT
// The "old" trigger — has set BillingCountry from ShippingCountry for years:
trigger AccountCountry on Account (before insert, before update) {
for (Account a : Trigger.new) {
if (a.ShippingCountry != null) {
a.BillingCountry = a.ShippingCountry; // correct logic, works in prod for years
}
}
}
// The "new" Flow (record-triggered, BEFORE save, on Account):
// Decision: if ShippingCountry != null AND IsChanged(ShippingCountry)
// → Update $Record.BillingCountry = "USA" ← hardcoded, from the consultant's "standard" templateTHE PROBLEM
Who actually wins — and why is the field wrong "on some records and right on others"? Reconstruct the exact order of execution (before-save Flow vs before-trigger vs validation), name the mechanism that decides the final value, and design the permanent fix (ownership + the two gating techniques).
Write: (1) the order-of-execution sequence with the winner at each step, (2) why the result varies by record, (3) the governance fix.
HINT LADDER
- Hint 1 (the avenue): (1) Order of execution: before-save Flow runs BEFORE before-triggers (the interview fact interviewers love to probe). (2) "Some records right, some wrong" = the update pattern determines who writes last. (3) The fix is never "move the Flow" — it's one owner per field.
- Hint 2 (the mechanism): (1) On insert, both run once: Flow sets
BillingCountry = "USA", then the trigger overwrites with the real shipping country → correct. On update ofShippingCountryon an existing record: Flow fires first (sets "USA"), trigger fires second (sets real value) → still correct. BUT on updates where the trigger's condition is false — e.g.,ShippingCountryupdated to null, or a field other than ShippingCountry changed so the trigger sets nothing while the Flow always runs on any update (if its entry condition is loose) → the Flow's "USA" survives. Or if the trigger hadif (a.BillingCountry == null)as its own guard — then the Flow's value survives on create. The "some records" = whichever automation wrote last with a surviving condition. - Hint 3 (the skeleton): Winner = the last writer in the order whose condition fires for that record's update pattern. Order: before-save Flow → before-trigger → validation → after-trigger → after-save Flow → rules. Governance fix: (1) one owner per field — the field's logic lives in exactly one automation; (2) if both must exist, the loser gates with entry conditions / "Is Changed" and a skip flag; (3) never let a hardcoded value in a Flow "improve" on a trigger's real data mapping — the Flow is system-mode, silent, and unreviewed.
THE REVEAL — POSTMORTEM
What actually happened (real class of incidents — the "flow vs trigger same field" scenario from agentforcelens/flowinator; every consultant-touch org):
The order of execution (memorize this sequence — it's a favorite probe):
- Before-save Record-Triggered Flow (and its formulas) — the FIRST automation to touch the record.
- Before triggers (oldest first, no guaranteed order between them).
- Validation rules.
- After triggers (oldest first).
- Assignment & escalation rules.
- After-save Record-Triggered Flow (and scheduled paths).
- Workflow rules (legacy, still in the sequence), process builder (deprecated, already-gone orgs).
Why the value is wrong "on some records": The Flow runs FIRST on every qualifying update; the trigger runs SECOND. Whoever writes last wins — so the final value is the trigger's value whenever the trigger's condition fires. The wrong records are exactly those where the trigger's condition didn't fire: updates that don't touch the mapped fields, ShippingCountry set to null (trigger guards != null), or the consultant's loose entry condition running when the trigger was never meant to. The consultant's defense ("mine runs after") is backwards — and the dev's defense ("it's overriding") is half-right: the Flow's hardcoded "USA" survives exactly when the trigger sleeps. The bug is dual ownership of one field, not order.
The permanent fix (governance, not order-shuffling):
- One owner per field:
BillingCountrygets exactly ONE automation. Decide: the trigger is production-proven real data mapping → delete the Flow (or restrict it to records the trigger doesn't own). If the org policy is declarative-first → the trigger moves to a before-save Flow with the same real mapping (never a hardcoded value). - If both must coexist (merge period): the loser gates on entry conditions — e.g., Flow runs only
IsChanged(ShippingCountry)AND trigger keepsBillingCountry == nullguard — so their firing windows never overlap. (One wins by condition, not by race.) - The skip flag pattern: if the trigger should win for Apex-initiated changes, the trigger sets a
Bypass_Flow__c-style flag (or the Apex uses a Custom Metadata-based bypass) that the Flow's entry condition checks. - The meta-rule (interview gold): "When two automations update one field, the org has two owners, and two owners means no one owns it. Execution order only decides who wins the race; ownership decides who should run at all."
Why the "obvious fixes" failed (the contrast):
- "Reorder the flow to run after the trigger" → you can't reorder relative to before-triggers; the platform fixes before-save Flow before before-trigger. The order is the order.
- "Change the trigger to overwrite always" → masks the bug, doubles the writes, and now the trigger is fighting the Flow on EVERY update — plus validation/recalc cost.
- "The Flow is system mode, it must be right" → Flow runs in system context by default — no sharing enforcement, no FLS, no review. A hardcoded template value in an unreviewed Flow is the most dangerous automation in the org.
KNOWLEDGE EXTRACTION (interview-ready)
- "Order of execution — where do flows sit?" → Before-save Flow runs FIRST (before before-triggers); after-save Flow runs AFTER after-triggers (before assignment rules). One-line: "Before-save Flow is the first automation to touch the record; after-save Flow is the last flow, after the triggers."
- "Two automations update the same field — who wins?" → The last writer whose condition fires for that record's change. The real answer is governance: one owner per field, entry conditions, skip flags. Never answer with just "the trigger."
- "Flow vs trigger for this field?" → The decision matrix (memorize from knowledge spine): simple-moderate, admin-maintainable, standard CRUD/FLS → Flow; complex loops/CPU/recursion control/bulk volume/mocking → Apex. Declarative-first is the 2026 default posture.
- "Do flows run in user context?" → Record-triggered flows run in system context by default (unless configured otherwise) — they bypass sharing and CRUD/FLS. A security-aware candidate mentions this unprompted.
THE REDO
From memory: the order-of-execution sequence (7 steps, with flows positioned), the "some records right, some wrong" explanation, and the 3 governance techniques.
RETRIEVAL DRILL
- Where does a before-save Flow run relative to before-triggers?
- Where does an after-save Flow run relative to after-triggers?
- Who wins when two automations update the same field — the interview-grade answer?
- Two gating techniques to stop dual ownership.
- Default execution context of record-triggered flows?
INTERVIEW MAPPING
"Flow or trigger, and why?" is asked in every loop (agent 05 research). This incident is its scenario form: given a real conflict, reconstruct order + ownership. The order-of-execution answer is the favorite probe; the governance answer is the senior differentiator.
INCIDENT 2 — THE LOOP THAT NEVER STOPPED
STAKES
"Performance is fine in QA," the admin says. Monday morning: the org is slow. Tuesday: query timeouts across every page; the ops dashboard shows Accounts and Contacts being updated thousands of times per minute. Someone added an after-save Record-Triggered Flow on Account: "whenever an Account's owner changes, update the related Contacts' owner." And (from a previous consultant) an after-save Flow on Contact: "whenever a Contact's owner changes, update its Account's owner." The two flows are... talking to each other. A single owner-change on one Account has now cascaded into over 900,000 records updated in one night — and the flows won't stop because every update they make is another qualifying change.
THE INCIDENT
Flow A (after-save, Account): IsChanged(OwnerId) → update Contacts' OwnerId
Flow B (after-save, Contact): IsChanged(OwnerId) → update Account's OwnerId
Sequence: Account.Owner changes
→ Flow A updates 40 Contacts → each Contact.Owner change
→ Flow B updates the Account → Account.Owner change (same value, but "changed")
→ Flow A runs again → 40 Contacts → Flow B → ... (nothing stops it)THE PROBLEM
The platform's "recursion guard" didn't stop this. Why not — what does the built-in guard actually do? Reconstruct the ping-pong, name the three defenses that WOULD have stopped it (in order of preference), and write the corrected design: one flow, entry conditions that make the second pass a no-op.
Write: (1) what the recursion guard does and doesn't do, (2) the three defenses, (3) the corrected single-flow design with no-op conditions.
HINT LADDER
- Hint 1 (the avenue): (1) The built-in guard stops a Flow re-triggering itself in a tight loop (the same flow on the same record) — but a cross-object ping-pong between two flows is NOT stopped: each flow sees a legitimate new change. (2) Defenses: ownership (one flow, not two), change-detection conditions that make the echo a no-op, and bypass flags/limits for the pathological case. (3) The corrected design: ONE flow on Account, updating Contacts, with a condition that prevents its own echo.
- Hint 2 (the mechanism): (1) Guard: record-triggered flows have a built-in recursion guard — a flow won't re-run on its own update of the same record in a loop (the platform's anti-infinite-loop); but Flow A updating Contacts is a different object → Flow B sees a fresh change → B updates Account → A fires again. Cross-object loops are invisible to the guard. (2) Defenses in order: (a) one owner — delete Flow B; owner sync is a one-directional business rule, and the direction is Account→Contacts; (b) echo-proof conditions — the sync flow updates Contacts only when the Contact's owner is NOT already the Account's new owner (
Contact.OwnerId != $Record.OwnerId), so the second pass has zero records to update → no echo; (c) operation guard — entry condition$Record.IsChanged && NOT ISNEW()-style gating plus (for the pathological case) a Custom Metadata bypass or a "last sync" timestamp field to dead-letter runaway. - Hint 3 (the skeleton): Corrected: one after-save Flow on Account (Owner changed) → Get Contacts where
OwnerId != Account.OwnerId→ Update those. The echo pass: Flow A runs again after its own update? No — its updates are to Contacts; the Account change is only from Flow B — which no longer exists. One owner, one direction, condition = only-differs → the system converges in ONE pass. Bonus: bulk-safe (Get Records returns up to 10,000 rows — beyond that, chunk via subflows or move to Apex — Module 1's batch lesson).
THE REVEAL — POSTMORTEM
What actually happened (real class of incidents — the documented cross-object flow loop; every org that "just synced ownership" twice):
What the recursion guard actually does: Record-Triggered Flows have a built-in guard against self-recursion — a flow won't immediately re-trigger on its own update of the same record (the platform detects the same flow firing on the same record from its own writes). That's the trap: teams believe the platform stops infinite loops, but the guard is per-flow, per-record — it does NOT see across objects. Flow A's updates are new Contact changes for Flow B; Flow B's updates are new Account changes for Flow A. Each sees "a legitimate change" and runs. Cross-object ping-pong is the documented infinite-loop class that the guard never promised to stop. The 900,000-record night is the bill.
The three defenses (in order of preference):
- One owner, one direction (the actual fix): owner-sync is a one-directional business rule. Flow B (Contact→Account) is deleted. Flow A alone, with the echo-proof condition:
Update Contacts WHERE OwnerId != $Record.OwnerId. After the first pass, no Contact differs → re-runs are no-ops by construction — the system converges in one pass. This is change detection done right: not "was it changed" but "does it still need changing." - Change-detection conditions (the belt): every entry condition uses
IsChanged(field)and the value-differs guard (!= $Record.X), so echo updates (same value, "changed" on write) produce zero matching records. Write-identical values still count as changes — a Flow that fires onIsChangedbut updates to the same value re-fires forever if anything else listens. - Runaway dead-letter (the suspenders): a Custom Metadata-based bypass flag (ops can kill an automation org-wide without a deploy), plus a "last sync" timestamp field — if the sync re-runs within seconds of itself, log + halt. Same discipline as Module 3's retry exit condition, applied to flows.
Why the "obvious fixes" failed (the contrast):
- "The platform prevents infinite loops" → the guard is per-flow, per-record; cross-object ping-pong is the blind spot. The guard is a seatbelt, not a design.
- "Add a 'processed' flag" → works, but it's a second owner of the field and a data hygiene burden; the condition (only-differs) makes the flag unnecessary.
- "Move it to Apex with a static flag" → valid, but only justified when flow conditions can't express the rule (complex multi-object logic). Declarative-first: the condition DOES express it.
KNOWLEDGE EXTRACTION (interview-ready)
- "Does the platform stop Flow recursion?" → The built-in guard stops a flow re-triggering on its own update of the same record. It does NOT stop cross-object loops (Flow A → object B → Flow B → object A). Design for convergence: one owner, one direction, value-differs conditions.
- "How do you make a Flow idempotent/convergent?" → Entry conditions + update-scope conditions on value difference (
X != $Record.Y), not justIsChanged; after the first pass, zero records match → re-runs are no-ops. - "What's the Apex equivalent?" → Static skip flags + change detection (Module 1 Incident 2's recursion guard lesson — same disease, different tool).
- "Flow limits relevant here?" → Get Records returns up to 10,000 rows; beyond that, chunk (Loop + subflows) or move to Apex. Record-triggered flows process in batches; entry conditions evaluated per record.
THE REDO
From memory: what the recursion guard does and doesn't stop, the three defenses in order, and the corrected single-flow design with the only-differs condition.
RETRIEVAL DRILL
- What exactly does the built-in Flow recursion guard stop?
- Why is cross-object ping-pong invisible to it?
- The three defenses, in order.
- Why is
IsChangedalone not enough — what's the second condition? - Get Records row ceiling in a flow — and the two escape hatches.
INTERVIEW MAPPING
Recursion control is the #1 reason interviewers accept Apex over Flow (research: "recursion control requirements" is a listed Apex-wins criterion). This incident is the scenario that tests whether you understand why — and whether you know the flow-side equivalents (conditions, convergence) before jumping to Apex.
INCIDENT 3 — THE 500-CASE EMAIL STORM
STAKES
The support team mass-closes 500 cases from a list view ("cleanup Friday"). The after-save Flow that sends a confirmation email to the contact on every Closed case... sends 500 emails — but first it spends 45 minutes inside one transaction budget, some emails fail silently (bad addresses), and the contact of every case gets their email after the case list view times out. The admin says "Flow handles bulk automatically." The org now has: a slow transaction, a partial email delivery, zero error records, and a business owner asking why 12 customers got duplicate emails (from a previous "fix" that added a second email step).
THE INCIDENT
Record-Triggered Flow (after-save, Case):
Entry: Status IsChanged = 'Closed'
→ Get Contact (the Case's ContactId)
→ Send Email (email alert, "Case # closed")
→ [no fault path — failures are invisible]
Mass close: 500 Cases → the Flow fires per record, inside the shared transaction:
→ 500 SOQL (Get Contact each) — near the 100-SOQL wall? No: batched 200 → chunked,
but each record's email + the "second email step" fix → 1,000 emails, and the
transaction is now the slowest in org history.THE PROBLEM
"Flow handles bulk automatically" — true or false, and at what cost? Reconstruct what happens to a Record-Triggered Flow on a 500-record batch (batching, limits, fault behavior), name the three fixes (email strategy, error handling, idempotency), and design the corrected flow.
Write: (1) the bulk mechanism + the limits it shares, (2) the three fixes, (3) the corrected design.
HINT LADDER
- Hint 1 (the avenue): (1) Record-triggered flows process in batches of 200 — bulk-aware-ish, but they SHARE the transaction's governor limits (100 SOQL / 150 DML / 10,000 rows) and each record's execution is its own flow run: per-record Get Records = 500 SOQL → wall. (2) Fixes: batch the data (Get Records once, no per-record SOQL), send email via bulk-safe path, add fault path + idempotency (no duplicate email step). (3) The "completed ≠ done" discipline applies: partial failure with no error records = silent corruption.
- Hint 2 (the mechanism): (1) Bulk: the flow runs once per record but in batches of 200 with shared limits; a per-record
Get Recordspattern multiplies SOQL — 500 records ≈ 500 SOQL + email calls → the SOQL wall (Too many SOQL queries: 101). Emails:sendEmailhas its own cap (10 invocations per transaction — a per-record Send Email element explodes; email alerts in flows count against it). (2) Fixes: (a) Get Records once with a filter (all Contacts for the batch) — collection, not per-record; (b) email discipline — one email send per contact or a digest, and the entry condition prevents re-sends (Statusfrom Closed → anything else → Closed = once); (c) fault path on every element + an Error_Log custom object record; (d) idempotency — the "duplicate email" fix added a second send step; the correct fix was ONE step with a guard. - Hint 3 (the skeleton): Corrected: after-save Flow, entry
Status IsChanged = 'Closed'→ Get Contacts (filter: Id IN the batch) → Loop → Send Email (one per contact, guarded by aNotification_Sent__ccheckbox updated in the same transaction) → Update Case.Notification_Sent__c. Fault path on Get/Send → insert Error_Log__c (Case Id, error, timestamp). Bulk: never per-record Get; email only for cases withNotification_Sent__c = false(idempotent against the retry/batch re-runs).
THE REVEAL — POSTMORTEM
What actually happened (real class of incidents — the mass-close email storm; flowinator/salesforceben bulk-flow Q&A; every org's first mass-action Friday):
The bulk truth: Record-Triggered Flows are somewhat bulk-aware — they process the batch in chunks of 200 records, entry conditions evaluated per record — but they share the transaction's governor limits with everything else: 100 SOQL, 150 DML, 10,000 rows, and the 10 sendEmail invocations cap per transaction. The per-record Get Records pattern is the classic Flow equivalent of a per-record SOQL loop in Apex: 500 records ≈ 500 SOQL → Too many SOQL queries: 101 mid-batch, and each email is a sendEmail invocation → the 10-cap blows up. The "Flow handles bulk automatically" is half-true: batching is automatic, limits are not. Flow is not "unlimited" (research trap #7).
The three fixes:
- Bulk data access: Get Records once per batch with a filter over the collection (all Contacts in scope), not per-record. In flows, per-record Get inside a Loop is the #1 bulk killer. (Alternative for heavy logic: the decision matrix says move to Apex/Batch — Module 1's lesson.)
- Email discipline + idempotency: one send step guarded by
Notification_Sent__c = false(checked and updated in the same transaction) — re-runs, retries, and batch re-processing cannot double-send. The "second email step" fix was the duplicate-source; the guard is the fix. - Fault path + error log: every element gets a fault path writing to
Error_Log__c(Case Id, element, error, timestamp). A flow without a fault path fails silently — the 45-minute transaction left zero evidence, and the org "fixed" duplicates blindly.
Why the "obvious fixes" failed (the contrast):
- "Flow handles bulk automatically" → chunks of 200 + shared limits; per-record patterns still hit walls. Same discipline as Apex bulkification.
- "Add a second email step to catch failures" → doubles the sends and the limit pressure; the correct fix is a guard + fault path, not another sender.
- "500 emails is the requirement" → then design for it: digest emails, or queue sends via Platform Events / Apex (Module 3's async boundary), never per-record in one transaction.
KNOWLEDGE EXTRACTION (interview-ready)
- "Are Record-Triggered Flows bulk-safe?" → Partially: they process in 200-record chunks with per-record entry-condition evaluation — but they SHARE the transaction's governor limits (100 SOQL / 150 DML / 10,000 rows) and the 10 sendEmail invocations. Per-record Get Records inside a Loop = the #1 bulk killer.
- "Flow vs Apex for bulk?" → The matrix: simple bulk logic, standard CRUD/FLS, admin-maintainable → Flow with batched Get; heavy CPU/aggregation/volume → Apex (Batch/Queueable) — same decision as Module 1.
- "Email limits?" → 10
sendEmailinvocations per transaction (EmailMessages up to 5,000 in batch); digests and guards beat per-record sends. - "Idempotent flow design?" → A guard field updated in the same transaction (
Notification_Sent__c = false), entry conditions on value-change — retries and re-runs become no-ops.
THE REDO
From memory: the bulk mechanism (chunks + shared limits), the three fixes, and the corrected flow skeleton (batched Get, guarded email, fault path).
RETRIEVAL DRILL
- Chunk size for record-triggered flow batches?
- The limits a flow shares with the transaction (name 4).
- The sendEmail invocation cap.
- The #1 bulk pattern that kills flows.
- The idempotency guard pattern for the email step.
INTERVIEW MAPPING
The interviewer follow-up after ANY flow-design question (research: "what if 500 cases close at once?" is the scripted follow-up). Bulk-aware + limits-aware + fault-path-aware = the complete answer; "Flow handles it" alone fails.
INCIDENT 4 — THE FLOW THAT WENT ROGUE AT 3:00 AM
STAKES
A consultant's "quarterly data cleanup" — a Schedule-Triggered Flow that deletes stale Quote records at 2:00 AM — has been "running" for a month. Wednesday morning: the ops report shows quotes from the current quarter are missing — including ones with approved pricing and one with a linked invoice. The scheduled flow ran every day (the consultant set "every day at 2 AM" instead of "every quarter"), its Get Records filter was wrong (LastModifiedDate < TODAY() — which matches EVERYTHING today-relative... actually LastModifiedDate < TODAY() matches records not modified today — including this quarter's active quotes), and the flow had no fault path and no notification — it deleted silently for a month, one batch per night, "successfully."
THE INCIDENT
Schedule-Triggered Flow "Q3 Quote Cleanup" (created by consultant):
Schedule: Every day, 2:00 AM ← "quarterly" became daily
→ Get Records: Quote WHERE LastModifiedDate < TODAY() ← matches ALL old records (bad filter)
→ Delete Records
→ [no fault path, no email alert, no audit]THE PROBLEM
Name the three decision failures (schedule vs semantics, filter logic, observability), the correct tool decision (Schedule-Triggered Flow vs Scheduled Apex — the matrix), and the corrected design (safe schedule, safe filter, audit trail).
Write: (1) the three failures, (2) the tool matrix, (3) the corrected design.
HINT LADDER
- Hint 1 (the avenue): (1) Failures: (a) schedule — "quarterly" became daily (semantics lost); (b) filter —
LastModifiedDate < TODAY()is date-relative, not "older than 90 days"; (c) observability — no fault path, no alert, no audit. (2) Tool: Schedule-Triggered Flow for admin-friendly scheduled automation (digests, cleanup, status updates); Scheduled Apex when you need retries, callouts, complex logic, or heavy volume. (3) Corrected: date-literal filter (LastModifiedDate < 90 DAYS AGO), schedule = quarterly with a visible naming, fault path + alert email + Error_Log, plus a "dry-run count" report element. - Hint 2 (the mechanism): (a) Schedule semantics: the config said "daily at 2 AM" — nobody read it; scheduled flows have a human-readable schedule in the config that IS the source of truth — the bug is a config bug, not code. (b) Filter:
LastModifiedDate < TODAY()= "not modified today" — this quarter's active quotes match; correct =LastModifiedDate < TODAY() - 90(orN DAYS AGOliteral). Delete in bulk is permanent — no recycle bin safety net beyond 15 days. (c) Observability: scheduled flows run unattended — a fault path with alert email + an audit custom object are mandatory, not optional. Interview note: the "silent scheduled failure" is the exact pattern of Module 3's 3:00 AM blackout, in declarative form. - Hint 3 (the skeleton): Tool matrix one-liner: "Schedule-Triggered Flow = declarative scheduled automation (digests, cleanup, status updates); Scheduled Apex = when the job needs callouts with retries, complex processing, or heavy volume." Corrected design: quarterly schedule (named "Quarterly Quote Retention (90 days)"), Get Records
WHERE LastModifiedDate < TODAY() - 90 AND Status NOT IN ('Approved','Invoiced'), a count-decision (fault path + alert if > expected), Delete, then a summary email + audit insert. Never delete without a guard field and a dry-run first month.
THE REVEAL — POSTMORTEM
What actually happened (real class of incidents — the scheduled-cleanup catastrophe; every "consultant left a scheduled automation" org; the flowinator schedule-flow Q&A):
Decision failure #1 — schedule semantics: "Quarterly cleanup" was configured as every day at 2:00 AM. The schedule lives in the flow's config — a human-readable field that IS the source of truth, and nobody read it. The fix is governance: schedule review as part of the release checklist (same as reviewing cron expressions — Module 1 Incident 4's discipline, declarative form), and naming that encodes intent ("Quarterly Quote Retention (90 days)", not "Q3 Quote Cleanup" — which still ran daily).
Decision failure #2 — the filter: LastModifiedDate < TODAY() is date-relative garbage for "older than 90 days": it matches every record not modified today — this quarter's active quotes included. Correct: LastModifiedDate < TODAY() - 90 (or the N DAYS AGO literal), and — the deeper fix — business guards: Status NOT IN ('Approved','Invoiced'), and a "retain" flag. Delete is permanent (beyond the 15-day recycle bin); every delete automation needs a guard field and a dry-run first run.
Decision failure #3 — observability: unattended jobs MUST have: a fault path (on failure → Error_Log + alert email), a completion summary (email + audit record: records examined/deleted), and an anomaly threshold (if the candidate count is 10× the historical average → halt + alert). The silent month is the bill for skipping all three. This is the declarative version of Module 3's Incident 8 monitoring lesson: automation without telemetry is a liability.
The tool matrix (memorize): Schedule-Triggered Flow — admin-friendly scheduled automation: email digests, status updates, simple cleanup, standard CRUD/FLS, no heavy CPU; Scheduled Apex — callouts with retries, complex processing, heavy volume, transactional guarantees, testable. The 2026 answer names the matrix and picks per-job: "For a simple daily digest — Flow. For the billing sync with retry + dead-letter — Scheduled Apex (Module 3's design)."
Why the "obvious fixes" failed (the contrast):
- "Just fix the schedule" → fixes symptom #1; the filter and the silence would still have killed records next quarter. Fix all three.
- "Recover from the recycle bin" → 15 days; the month of nightly deletes is unrecoverable. Prevent, then protect.
- "Add more entry conditions" → the flow had none relevant — conditions gate when it runs, not what it deletes; the filter + guards gate what it deletes.
KNOWLEDGE EXTRACTION (interview-ready)
- "Schedule-Triggered Flow vs Scheduled Apex?" → Flow: declarative scheduled automation (digests, cleanup, status updates), admin-maintainable. Apex: callouts with retries, complex logic, heavy volume, testability. Matrix answer, not preference.
- "Deleting in a scheduled flow — your rules?" → Never delete without: an explicit retention filter (
< TODAY() - N), business guards (status/flag exclusions), a dry-run first month, fault path + summary email + audit record, and an anomaly threshold. - "Scheduled flow observability?" → Fault path + alert, completion summary, audit object, threshold halts. Unattended automation must page a human.
- "Time-based vs scheduled paths?" → A record-triggered flow can also have scheduled paths (wait N hours after a trigger, then act) — the time-based cousin; same observability rules apply.
THE REDO
From memory: the three decision failures, the tool matrix one-liner, and the 5-point safe-delete design.
RETRIEVAL DRILL
- Flow vs Apex for scheduled jobs — the matrix one-liner.
- Why is
LastModifiedDate < TODAY()wrong for "older than 90 days"? - The 3 mandatory observability elements for unattended flows.
- Delete safeguards (name 4).
- What is a scheduled path (vs a schedule-triggered flow)?
INTERVIEW MAPPING
The "design a scheduled automation" question (agent 05: Schedule-Triggered Flow Q&A). Senior differentiators: business guards on delete, observability requirements, and the flow-vs-apex schedule matrix — none of which the junior candidate mentions.
INCIDENT 5 — THE WIZARD NOBODY COULD OPEN
STAKES
The team built a beautiful 6-screen Screen Flow ("Quote Configurator") with a custom LWC on screen 3. Launch button placed on the Quote record page. Go-live Monday. Monday 9:00 AM: a sales rep clicks the button — nothing happens. 9:15 AM: the manager tries — nothing. 9:30 AM: the admin — it works. Debug mode: works for the admin. The button is there for everyone; the flow runs for exactly one person; and the error (if any) is invisible. Friday demo with the VP is scheduled.
THE INCIDENT
Screen Flow "Quote_Configurator" — launched from a button on the Quote record page.
Symptom: click → nothing (no error, no navigation) for everyone except the admin.
The admin's checks: Flow is Active ✅ · Button is on the page ✅ · Debug mode ✅THE PROBLEM
The flow works for exactly one person. List every access/visibility check that must pass for a screen flow to launch for a user (name the 4 access layers), the two "invisible failure" causes specific to screen flows (where do errors actually go?), and the debugging procedure (debug mode limits, failure email, view flow details).
Write: (1) the 4 access layers, (2) the 2 invisible-failure causes, (3) the debug procedure.
HINT LADDER
- Hint 1 (the avenue): (1) Access layers: (a) profiles/permission sets — "Run Flows" + the flow's specific access (manage vs run); (b) the launch surface — button/action visibility (does the user's profile see the button? Lightning app/record page visibility); (c) the data the flow touches — record access, field-level access on every element (FLS on the screen's fields, object access for Get/Update); (d) the LWC on screen 3 — the component's Apex/aura-enabled + the underlying records. (2) Invisible causes: screen-flow failures surface via flow failure email to the admin (if configured) and in Debug/View Details, not to the user; an access-denied at launch typically logs nothing visible to the user — the flow "does nothing." (3) Debug: Debug mode (admin), check "Flow Run" / View Details, failure email,
FlowOrchestration-style monitoring, and per-element fault paths. - Hint 2 (the mechanism): (1) The classic silent-fail: the rep's profile lacks "Run Flows" (or the flow's run access) → the button exists but the action silently no-ops; OR the flow runs and the first element fails on FLS (a Get Records on a field the rep can't read → flow halts, no UI error because screen flow errors go to the failure email + logs, not the user). The LWC case: the component's Apex class needs "Apex Classes" enabled in the profile. (2) Screen flow errors are delivered via: the failure email (admin-configured recipient), Flow Debug / View Details, and fault paths — the user sees nothing. (3) Debug procedure: replicate in Debug mode with "Run as another user" (the classic: it works for the admin because the admin has full access); check the View Details / Flow Runs list; enable the failure email; check the access matrix per profile; test the LWC standalone.
- Hint 3 (the skeleton): 4 layers: Profile/Permission Set (Run Flows + flow access) → Surface (button/app/page) → Data (object + field access for every element) → Component (LWC/Apex enabled). 2 invisible causes: launch-access denial (silent no-op) + element FLS failure (flow halts, error only in failure email/logs). Debug: Debug-as-user, View Details/Flow Runs, failure email, fault-path logs, standalone LWC test.
THE REVEAL — POSTMORTEM
What actually happened (real class of incidents — the "works for admin only" screen flow; every go-live with a forgotten permission):
The four access layers (memorize — the interview answer):
- Run access: the profile/permission set must grant the flow itself — "Run Flows" plus the specific flow's Run/Manage access (and for button-launched flows, the action's visibility).
- The launch surface: the button/action on the record page — Lightning page visibility per profile, and the button's own visibility rules. A hidden surface = "nothing happens."
- The data the flow touches: object + field-level access on EVERY element — Get Records/Update Records need read/write on the fields used; screen fields need FLS; a single unreadable field halts the run.
- The embedded component: a custom LWC on a screen needs its Apex class enabled ("Apex Classes" in the profile), plus the component's own visibility. The classic "screen 3 fails only for reps" is the missing Apex access.
The two invisible-failure causes (the reason "nothing happens"):
- Launch-access denial: the rep lacks Run access → the action silently no-ops — no error, no log the user can see. The button is visible (surface layer passes) but the launch fails at layer 1.
- Element-level FLS halt: the flow starts, then the first element can't read a field → the run halts — and screen-flow errors are NOT shown to the user; they land in the failure email (if configured) and View Details / Flow Runs. The user sees "nothing."
The debug procedure (say it like you've done it):
- Debug mode — "Run as another user": the single most powerful tool; the admin's Debug works because the admin's access is complete. Run as the rep → the failure reproduces exactly.
- View Details / Flow Runs for the failed interview: the element that failed, the error, the record.
- Enable the failure email (admin-configured recipient) — the production equivalent of a fault path for screen flows.
- The access matrix audit: per profile — Run Flows, flow Run access, button visibility, object/field access per element, Apex class access for the LWC.
- Test the LWC standalone (Jest + a bare page) — isolate component vs flow failure.
Why the "obvious fixes" failed (the contrast):
- "The button is on the page, so it's not access" → the surface is layer 2; the launch is layer 1. Visible ≠ runnable.
- "Debug mode works, so the flow is fine" → Debug runs as the admin; the failure is the user's context, not the flow's logic.
- "Add an error message element at the start" → doesn't catch launch-time access denial (the flow never starts) — the check is the access matrix, not the flow's UI.
KNOWLEDGE EXTRACTION (interview-ready)
- "Why can the admin see/run it and the user can't?" → Four layers: run access (profile/permission set), launch surface (button/page), data access (object + FLS per element), component access (LWC/Apex enabled). Debug-as-user finds it in minutes.
- "Where do screen flow errors go?" → Not to the user: failure email (if configured), View Details / Flow Runs, fault paths. A screen flow that "does nothing" = launch denied or element FLS halt.
- "How do you grant a flow to a permission set?" → Permission set → "Flows" → assign Run access (vs Manage); plus "Run Flows" user permission. Lean-profile best practice (Module 5 preview).
- "Debug mode?" → Admin-only; "Run as another user" is the production triage tool; combined with View Details + failure email.
THE REDO
From memory: the 4 access layers, the 2 invisible-failure causes, and the 5-step debug procedure.
RETRIEVAL DRILL
- The 4 access layers for a screen flow (name them).
- Where do screen flow errors go — and where do they NOT go?
- The two "invisible failure" causes.
- What's the single most powerful debug tool for "works for admin only"?
- Why does "the button is on the page" not prove access?
INTERVIEW MAPPING
The "user can't see/run X but admin can" question is the security-flavored favorite (agent 05: "user can see in UI but not in Apex" — the flow twin). Naming the four layers + debug-as-user is the senior answer.
INCIDENT 6 — THE SILENT GRAVEYARD
STAKES
An after-save Flow updates related Opportunity records whenever an Account changes. For six months, the ops team's weekly "data quality" report shows "Opportunity sync gaps" — dozens of Opportunities with stale Account_Status__c values every week. Nobody knows why. The flow's "Last Run" shows green. Today, a VP's biggest deal closes with the wrong account status because the sync never ran for that Account. The admin's response: "The flow says successful." The developer's first question: "what did the flow's fault path do?" — and there is no fault path.
THE INCIDENT
Record-Triggered Flow (after-save, Account):
Entry: IsChanged(Account_Status__c)
→ Get Related Opportunities (filter: Status = 'Open')
→ Update Opportunities (set Account_Status__c = $Record.Account_Status__c)
→ [END]
Weekly reality: gaps appear on records where the Get returned records but the
Update partially failed (e.g., a locked Opportunity, a validation rule, an
insufficient-permission edge) — and the flow "succeeded" from the platform's view.THE PROBLEM
"The flow says successful" — why is that not evidence? (What counts as success for a flow run, and where do partial failures actually land?) Design the fault-aware version: fault paths on which elements, what each does, the error-log record, and the reconciliation job that catches what even fault paths miss.
Write: (1) the success-definition gap, (2) the fault-aware design, (3) the reconciliation discipline.
HINT LADDER
- Hint 1 (the avenue): (1) A flow's "success" = the interview completed without an unhandled fault. Element-level failures inside a Loop can be silently swallowed (a failed Update inside a Loop with no fault path continues or aborts invisibly). Partial failure ≠ failure in the platform's eyes. (2) Fault-aware design: fault paths on the Get and the Update (log + alert), an Error_Log custom object, and a count-integrity check (updated count vs expected count). (3) Reconciliation: a scheduled flow/job that counts "stale" records (expected value ≠ actual) — the Module 1 Incident 4 "Completed ≠ done" discipline, declarative form.
- Hint 2 (the mechanism): (1) In a flow, elements with fault paths capture which element, which error, on which record; without them the run ends "successfully" as far as the platform's run record shows — errors inside a Loop (element failure for one iteration) can abort the whole interview or be skipped depending on configuration; either way no one is told. (2) Design:
Get Opportunitiesfault path → Error_Log + alert email;Update Opportunitiesfault path → Error_Log per failing record (record Id, error message, timestamp) + alert when threshold exceeded; plus a "last synced" field on Opportunity (Account_Status_Last_Synced__c) so gaps are queryable. (3) Reconciliation: nightly scheduled flow — countAccount_Status__c != Account.Account_Status__c(or stale last-synced) → if > 0, alert + report list. The gap becomes visible within hours, not quarters. - Hint 3 (the skeleton): Fault paths: (a) on Get: log "element failed" + alert; (b) on Update: per-record Error_Log (Opportunity Id, error, timestamp) + threshold alert; (c)
Account_Status_Last_Synced__cfield for queryable gaps; (d) nightly reconciliation flow: stale-count > 0 → alert + email with the list. Success = "no unhandled fault" is NOT success; success = "every record converged, and we can prove it."
THE REVEAL — POSTMORTEM
What actually happened (real class of incidents — the silent partial-failure flow; the "Last Run green but data wrong" org; research trap #6: no fault-path story = production-maturity gap):
Why "the flow says successful" is not evidence: A flow's run record shows success when the interview completes without an unhandled fault. Element-level failures — an Update inside a Loop that fails for one iteration, a validation rule rejecting a record, a locked record, an FLS edge — do NOT necessarily fail the interview; depending on the element and configuration, the failure aborts the run or is simply not surfaced. Either way, the platform does not page anyone, and the run record can still read "success" from the flow's perspective. Partial failure is invisible by default. The fault path is the only built-in instrument — and there wasn't one.
The fault-aware design (memorize the pattern):
- Fault path on every data element (Get, Update, Delete, Send Email): log to
Error_Log__c— element name, record Id, error message, timestamp — and send an alert when a threshold trips. - A queryable convergence field:
Account_Status_Last_Synced__con Opportunity — gaps becomeWHERE Account_Status_Last_Synced__c < TODAY()(or null), which powers both the dashboard and the reconciliation job. - The reconciliation job (the catch-all): nightly scheduled flow — count records where the expected value ≠ the actual value (Opportunity.Account_Status__c != Account.Account_Status__c for open opportunities); if > 0 → alert + email with the list. Fault paths catch what the platform reports; reconciliation catches what the platform doesn't. Two instruments, because each misses a class (fault path misses silent element failures; reconciliation catches them but is periodic).
The meta-rule (interview gold): "In automation, success is not 'no unhandled fault' — it's 'every record converged, and we can prove it.' The proof is a queryable convergence field + a reconciliation job. This is the same discipline as testing: you don't trust the green run; you trust the evidence."
Why the "obvious fixes" failed (the contrast):
- "Check the Last Run status" → it's green; the failure class never touches it.
- "Add more entry conditions" → the entry condition gates when it runs, not what it does — the partial failures happened inside a correctly-gated run.
- "Just re-run it manually" → re-runs are blind without the error log; and re-running without the guard field re-updates everything (no idempotency).
KNOWLEDGE EXTRACTION (interview-ready)
- "What does a flow's 'success' actually mean?" → The interview completed without an unhandled fault. Element-level failures can be invisible; the fault path is the only built-in instrument, and reconciliation is the catch-all.
- "Fault path — what and where?" → An error-handling branch on Get/Update/Delete/Send elements: log to an error object + alert. Interviewers love it — production maturity signal (research trap #6).
- "How do you prove an automation converged?" → Queryable convergence field + scheduled reconciliation (stale-count > 0 → alert). "Completed ≠ done."
- "Flow vs Apex for error handling?" → Flow: fault paths + error-log object (declarative). Apex: try/catch + static state + tests. Both need the same discipline.
THE REDO
From memory: the success-definition gap, the 3-part fault-aware design, and the reconciliation pattern.
RETRIEVAL DRILL
- What does a green flow run actually prove?
- Where do element-level failures inside a Loop go without a fault path?
- The 3 components of the fault-aware design.
- Why do you need BOTH fault paths AND reconciliation?
- The queryable convergence field — what does it power?
INTERVIEW MAPPING
The "fault path" question is the research's explicit maturity signal (trap #6: "No fault-path / error handling story for Flows"). This incident gives you the full story: not just "add a fault path" but why the green run lies and what catches the rest.
INCIDENT 7 — THE FLOW THAT ATE THE GOVERNOR
STAKES
A "simple" autolaunched Flow — a pricing engine that recalculates quote line items — has grown. A new consultant added: a Loop with nested Loop over line items, per-iteration Get Records (formula re-evaluations), and a screen-level call to an external system via HTTP callout element. It worked in debug. In production, on a 400-line quote: the transaction crashes with limit errors, the flow is now 2,800 elements (the platform's max is 2,500 — it can't even save a new version), and the version history has 2,100 versions (max 2,000). The team "can't debug it" because it's too big to open.
THE INCIDENT
Autolaunched Flow "Quote_Pricing_Engine" (called from a record-triggered flow + Apex):
→ Get Quote Line Items (all)
→ Loop over items
→ Loop over price rules
→ Get Records (price rule by Id) ← per-iteration SOQL
→ Formula (recomputed per iteration)
→ [one day] HTTP callout element added (sync, in a transaction)
→ [one day] total elements: 2,800; versions: 2,100
Production: 400-line quote → SOQL limit crash; the flow can't even be saved now.THE PROBLEM
Name the three walls this flow has hit (with the exact numbers), the tool decision (Flow vs Apex — apply the matrix), and the migration path (what moves where, and the Invocable Apex bridge that keeps the flow in control).
Write: (1) the three walls, (2) the matrix decision with justification, (3) the migration path.
HINT LADDER
- Hint 1 (the avenue): (1) Walls: element count (2,500 max), version count (2,000 max), and the transaction governor limits (100 SOQL / 150 DML / CPU) — the nested per-iteration Get is the SOQL killer, and the sync callout in a transaction is forbidden (Module 3 rule #1 — callouts can't happen in a transaction; flows can't do sync callouts from record-triggered context at all). (2) Matrix: complex loops + CPU + per-record data → Apex; the flow is exactly the "complex loop/aggregation logic, CPU-intensive" case from the research. (3) Migration: pricing math → Apex (Invocable class with
@InvocableMethod), the flow keeps its role as orchestrator calling the Apex action; callout → async boundary (queueable, Module 3). - Hint 2 (the mechanism): (1) Numbers: 2,500 max elements; 2,000 max versions (older versions deleted to make room — you can't "save" past the ceiling); shared transaction limits — a per-iteration Get Records inside a nested loop on 400 items × N rules blows the 100-SOQL wall; sync callout element in a flow transaction → the callout is blocked (same "uncommitted work" reality as Module 3 — flows don't get sync callouts; HTTP callout elements are async-only / restricted). (2) The matrix: the research's Apex-wins list: complex loop/aggregation logic, CPU-intensive processing, bulk data volume, recursion control, testability with mocking — this flow hits four of five. Flow-wins list: simple-moderate logic, admin maintainability, standard CRUD/FLS — no longer true here. (3) Path: Invocable Apex (
@InvocableMethod+@InvocableVariable) — the flow stays the entry point (declarative orchestration, admin can still tweak thresholds/conditions), the math moves to a testable Apex class; the callout moves to a Queueable (async,AllowsCallouts); elements shrink below 2,500 by replacing the nested loops with one Apex action. - Hint 3 (the skeleton): Walls: 2,500 elements · 2,000 versions · shared governors (100 SOQL/150 DML/CPU) + no sync callout in transaction. Decision: Apex (complex loops, CPU, volume, recursion, mocking — 4/5 criteria). Path:
QuotePricingServiceInvocable (inputs: Quote Id; outputs: totals) → flow calls the action in one element → loop removed → element count drops; callout → Queueable. Flow keeps orchestration; Apex keeps math; tests keep everyone honest.
THE REVEAL — POSTMORTEM
What actually happened (real class of incidents — the grown-monster autolaunched flow; the "can't save the flow anymore" org; the flowinator limits Q&A + research matrix):
The three walls (exact numbers — memorize):
- 2,500 max elements per flow — the flow is 2,800; it cannot save a new version. The 2,000-version ceiling is separate: version history is capped, so old versions get deleted to make room. A flow you can't save is a flow you can't fix.
- Shared transaction governors: 100 SOQL / 150 DML / 10,000 rows / CPU time — the nested per-iteration Get Records is the declarative twin of the per-record SOQL loop from Module 1 Incident 1: 400 items × N rules ≈ the 101st SOQL crashes the run. Flow is not "unlimited" (research trap #7).
- No sync callout in a transaction: the HTTP callout element in a record-triggered/autolaunched flow hits the same wall as a trigger callout (Module 3 rule #1 — uncommitted work). Flows cannot perform synchronous external calls mid-transaction; external calls must be async (Queueable/Invocable bridge) — the same boundary, declarative form.
The decision matrix applied (say it like a verdict): The research's Apex-wins criteria: complex loop/aggregation logic ✓, CPU-intensive processing ✓, bulk data volume ✓, recursion control ✓, testability with mocking ✓ — four of five hit. Flow-wins criteria (simple-moderate logic, admin maintainability, standard CRUD/FLS) — no longer true. Verdict: the pricing engine moves to Apex. The matrix answer (not preference) is the entire point — this incident is the live demonstration of "when Flow loses."
The migration path (the graceful version):
- Invocable Apex bridge:
@InvocableMethod—QuotePricingServicetakes the Quote Id (or a record collection) and returns the totals. The flow keeps its role as orchestrator: it calls the Apex action in ONE element, keeps the entry conditions, the fault path, and the admin's ability to tweak thresholds — declarative shell, Apex engine. - Nested loops → one action: 400-line quotes now run in one Apex call with proper bulkified SOQL — element count drops below 2,500; the flow becomes saveable again.
- Callout → Queueable: the external price-cache call moves to a Queueable with
AllowsCallouts+ retry/backoff (Module 3 Incident 8's design), enqueued by the flow (or Apex) — never a sync element in the transaction. - Tests: the Apex engine gets test classes with mocks (the flow never had them); the flow's remaining decisions get a small set of flow-level tests via the testing framework.
Why the "obvious fixes" failed (the contrast):
- "Split it into subflows" → valid for modularity, but subflows share the transaction; the SOQL wall and the callout remain. The math must leave the flow entirely.
- "Reduce element count by deleting versions" → you can't delete below the wall while the logic is still there; and version-culling hides the real problem (unmaintainable automation).
- "Use an Apex action for just the callout" → half-right; the callout is async-only anyway, and the CPU/SOQL walls are the actual killers — the whole engine moves, not just the HTTP piece.
KNOWLEDGE EXTRACTION (interview-ready)
- "Flow limits?" → 2,500 max elements; 2,000 max versions; shares the transaction's governor limits (100 SOQL / 150 DML / 10,000 rows / CPU); no sync callouts in transactions. Flow is not unlimited.
- "When does Flow lose to Apex?" → The matrix: complex loops/aggregation, CPU-intensive work, bulk volume, recursion control, mocking/testability → Apex. Simple-moderate, admin-maintainable, standard CRUD/FLS → Flow. Invocable Apex is the bridge for the in-between.
- "Flow → Apex how?" →
@InvocableMethod+@InvocableVariable— the flow calls the Apex action as one element (inputs/outputs defined by annotations), keeps orchestration, gains testability. - "Apex → Flow how?" →
Flow.Interview.createInterview(flowName, inputs).start()— less common; typically you'd trigger an autolaunched flow from Apex when the flow holds the logic. - "HTTP callout element?" → Flows' callout element is for async/external integration scenarios with the same limits as Apex callouts; a sync callout inside a record-triggered transaction is blocked by the same uncommitted-work rule.
THE REDO
From memory: the three walls with numbers, the matrix verdict (4/5 criteria), and the 4-step migration path.
RETRIEVAL DRILL
- Max elements per flow? Max versions?
- Three limits flows share with the transaction.
- The Apex-wins criteria (name 4–5).
- How does a flow call Apex (annotations)?
- How does Apex call a flow?
INTERVIEW MAPPING
The "Flow vs Apex — when do you move?" question, with real walls. The Invocable Apex bridge (research Q: "Can a Flow call Apex?") is the answer that shows you know both sides of the matrix.
INCIDENT 8 — THE TWO AUTOMATION WAR
STAKES
An Account field VIP_Score__c is updated by three automations: a legacy Workflow Rule (field update, "if Industry = Technology, set 10"), a before-save Flow ("if Total_Spend > 100K, set 100"), and an Apex trigger (recomputes from transactions). The business says "the score is wrong" — and it is: the workflow writes 10, the flow writes 100, the trigger recomputes 87 — final value depends on order and conditions, and it changes over time as records are touched (each automation re-fires on different triggers). The consulting firm that owns the org "can't remove anything" — each automation is "someone's deliverable."
THE INCIDENT
VIP_Score__c writers:
1. Workflow Rule (legacy): Industry = Technology → set 10 (runs LAST in order — after-save, before after-save flows? No: workflow runs AFTER after-triggers, BEFORE after-save flows — check the sequence)
2. Before-save Flow: Total_Spend > 100K → set 100 (runs FIRST)
3. Apex before-trigger: recompute from transactions (e.g., 87) (runs SECOND)
Result: the value flaps per update pattern; nobody owns the field;
removing any automation = "breaking a deliverable."THE PROBLEM
Name the automation governance failure (the 4 classic symptoms), the correct consolidation (which writer survives, and why — apply the matrix + the order-of-execution), the bypass patterns that make migration safe (the 3 mechanisms from research), and the sunset procedure (how you retire the losers without breaking the org).
Write: (1) the 4 symptoms, (2) the consolidation decision, (3) the migration + sunset procedure.
HINT LADDER
- Hint 1 (the avenue): (1) Symptoms: dual/plural ownership of one field, value flaps per update pattern, no single source of truth, every automation "someone's deliverable" (ownership = politics). (2) Consolidation: ONE writer. Given the recompute needs transaction data → Apex trigger (or an Invocable call) survives; the workflow (legacy) dies; the flow dies or becomes the trigger's bypass-aware alternative. (3) Bypass patterns: entry conditions + change detection, a skip/guard field, Custom Metadata-based bypass (ops can disable an automation org-wide without deploy). Sunset: deactivate losers one at a time with a watch period, reconcile the field after each.
- Hint 2 (the mechanism): (1) Order of execution recap: before-save Flow (1st) → before-trigger (2nd) → validation → after-trigger → after-save flow → workflow rule (LAST-ish, legacy). The workflow's value wins only when the earlier writers' conditions didn't fire or fired with different conditions — hence "wrong and flappy." (2) The consolidation logic: the recompute needs cross-object transaction data + recursion control + testability → Apex owns
VIP_Score__c(matrix: complex/CPU/recursion → Apex). The workflow rule is legacy and must die (declarative-first orgs still sunset legacy automation — "deprecated" means removed, not preserved). The flow is redundant with the trigger and must die or become the bypass flag's setter. (3) Migration: Custom Metadata recordAutomation_Bypass__mdt(flag:Disable_VIP_Score_Flow__c) checked by the flow's entry condition; trigger sets the skip flag when it has recomputed (or the flow checks a guard fieldVIP_Score_Computed_By__c); deactivate workflow → watch 2 weeks → reconcile field values → delete. - Hint 3 (the skeleton): (1) Symptoms: multiple writers, flappy value, no source of truth, deliverable-politics. (2) Survivor: Apex trigger (needs transaction data, recursion control, tests); flow and workflow both removed. (3) Migration: Custom Metadata bypass flags + entry conditions; deactivate one automation at a time with a 2-week watch + nightly reconciliation; then delete the dead ones. Governance rule to state: "One field, one owner, one path. Everyone else is either gated by bypass or deleted. Deliverables are not an excuse for dual ownership."
THE REVEAL — POSTMORTEM
What actually happened (real class of incidents — the org with 3 writers on one field; the "can't remove anything" consulting org; research Q: "trigger and Flow updating the same field — diagnose"):
The four symptoms (name them like a diagnosis):
- Plural ownership — three automations write one field; no owner in the record-keeping.
- Value flapping — the final value depends on which automation's conditions fired for a given update pattern; re-touching a record re-runs the race → the score changes over time without business action.
- No source of truth — nobody can say "the score IS X because Y computes it."
- Deliverable politics — each automation is someone's invoice; removal is a negotiation, not a technical decision. (The most common real-world blocker — naming it is the senior move.)
The consolidation decision (apply the matrix out loud): The recompute needs transaction data, recursion control, and testability → the Apex trigger survives (matrix: complex/CPU/recursion/mocking → Apex). The before-save flow is redundant (it approximates what the trigger computes exactly) → dies. The workflow rule is legacy (research: Process Builder deprecated, Workflow Rules legacy) → dies. One writer remains. The order-of-execution lesson from Incident 1 applies backwards: the fix isn't "move the writers" — it's "remove the writers until one remains."
The migration + sunset procedure (the safe path — say it like a change manager):
- Bypass patterns first (research's 3 mechanisms): (a) entry conditions + change detection — tighten every automation's firing window; (b) skip/guard field —
VIP_Score_Computed_By__cset by the trigger, checked by the flow; (c) Custom Metadata-based bypass —Automation_Bypass__mdtflags (Disable_VIP_Score_Flow__c) that ops can flip org-wide with zero deploy — the same idea as Module 3's dead-letter switch and Module 1's static flags, in Setup. - Sunset in waves: deactivate the workflow → 2-week watch (nightly reconciliation: expected score vs actual) → deactivate the flow → 2-week watch → delete both. Each wave is reversible; the reconciliation job is the evidence gate (Incident 6's discipline).
- The governance rule to state: "One field, one owner, one path. Every other automation is gated by bypass or deleted — and deliverables are not an excuse for dual ownership. This is the same rule as single-responsibility in code."
Why the "obvious fixes" failed (the contrast):
- "Just delete the workflow, it's legacy" → correct destination, wrong sequence: without the watch period you can't prove the trigger covers all cases — and the deliverable-politics blocker is real.
- "Reorder them so the trigger runs last" → you can't reorder relative to before-save flows/triggers; and order is the symptom, ownership is the disease.
- "Make the flow call the trigger's logic" → the flow and trigger would still be two owners of one field; consolidation means ONE writer, not two writers sharing logic.
KNOWLEDGE EXTRACTION (interview-ready)
- "Multiple automations updating one field — diagnose?" → Order-of-execution determines the flapping; ownership determines the fix. One field = one owner; everyone else gated (entry conditions, guard fields, Custom Metadata bypass) or deleted, in waves with reconciliation.
- "Bypass patterns for automation?" → Entry conditions + IsChanged; skip/guard fields; Custom Metadata-based bypass flags (ops-flippable, no deploy). Same static-flag idea as Apex, in Setup.
- "Legacy automation?" → Process Builder = deprecated; Workflow Rules = legacy; sunset them with watch periods — never keep them "because they work" (dual ownership is the bug they're shipping).
- "Flow + Apex coexistence?" → Legal when ownership is partitioned (different fields/objects) or one is the bypass-gate for the other. Illegal when they race on one field.
THE REDO
From memory: the 4 symptoms, the consolidation verdict + matrix, and the 3-step sunset procedure.
RETRIEVAL DRILL
- The 4 symptoms of the automation war.
- Who survives in the consolidation, and why (matrix)?
- The 3 bypass mechanisms.
- The sunset sequence (waves + evidence gate).
- The one-line governance rule.
INTERVIEW MAPPING
Research scenario #1 verbatim ("trigger and a Flow updating the same field — the value is wrong. Diagnose.") plus the "recursion/duplicate processing between Flow and Apex" Q&A. The governance answer (ownership + bypass + sunset) is the senior differentiator the interviewers are fishing for.
🏆 CAPSTONE — THE ORG THAT AUTOMATES ITSELF
STAKES
Thursday, 6:12 PM. The ops dashboard is red: query timeouts, AsyncApexExecutions warnings, and a data-quality report showing thousands of "stale" records in three different objects. The org has 47 automations: record-triggered flows, scheduled flows, legacy workflow rules, and Apex triggers — added by four different vendors over five years, none with fault paths, several updating the same fields. Leadership wants ONE presentation: "what is broken, in what order do we fix it, and how do we make sure this never happens again?" You have 45 minutes to build the report. The clues are real — each maps to an incident in this module.
THE INCIDENT (the evidence file)
- Clue A:
System.LimitException: Too many SOQL queries: 101in a transaction that "only runs one flow." The flow has a Loop with a per-iteration Get Records. - Clue B: A field
Priority_Score__cis written by a before-save flow AND an Apex before-trigger AND a legacy workflow rule. The value is "wrong" per the business — and different every time the record is touched. - Clue C: The nightly "cleanup" flow deletes records where
LastModifiedDate < TODAY()— "it's been running every night for a month." - Clue D: An after-save flow "syncs" Contacts from Accounts, and another after-save flow "syncs" Accounts from Contacts. "The platform prevents infinite loops," says the vendor.
- Clue E: 3,000 Accounts have
VIP_Score__cthat "should have been updated" by a flow whose Last Run shows green. The flow has no fault path. - Clue F: A sales rep clicks a launch button for a screen flow — nothing happens. The admin can run it fine in Debug.
THE PROBLEM (the transfer test — the real interview scenario round)
Produce, in writing, a complete incident report:
- For each clue: name the mechanism (1 line), the root cause (2–3 lines), and the fix (pointer to the pattern — no full code).
- Prioritize: what do you fix tonight vs next week vs next quarter?
- Identify the shared root cause that connects at least 4 clues (there is one — find it).
- Write the 3 regression tests / verification steps you'd add before Monday's release.
- Role-play the interview: leadership asks "why is the org like this, and how do you guarantee automation quality going forward?" — answer in 2 minutes, closed notes.
This is deliberately hard. Produce your best report even if incomplete — the comparison with the model answer below is where the learning lives. (40–45 min cap.)
THE MODEL REPORT (reveal after your attempt)
- Clue-by-clue:
- A → Incident 7: per-iteration Get Records in a Loop inside a flow — declarative twin of the per-record SOQL loop; shares the 100-SOQL transaction wall. Fix: batched Get once per batch, or move the logic to Apex (matrix: complex/CPU → Apex).
- B → Incidents 1 + 8: three writers on one field — order-of-execution race (before-save flow first, before-trigger second, workflow last-ish) → flapping value. Fix: one owner (Apex trigger — needs transaction data + recursion control), bypass flags (Custom Metadata) + entry conditions, sunset the flow and the legacy workflow in waves with reconciliation.
- C → Incident 4:
LastModifiedDate < TODAY()= "not modified today," not "older than N days" — a month of nightly deletions. Fix:TODAY() - 90+ business guards (status exclusions, retain flag) + dry-run + summary email/audit + anomaly threshold. - D → Incident 2: cross-object ping-pong — the built-in recursion guard is per-flow, per-record; it does NOT stop Flow A→Contacts→Flow B→Accounts. Fix: one owner, one direction, only-differs conditions (
Contact.OwnerId != $Record.OwnerId) → converges in one pass. - E → Incident 6: green run ≠ success — element-level failures invisible without fault paths. Fix: fault paths on Get/Update → Error_Log + alerts, queryable convergence field (
VIP_Score_Last_Synced__c), nightly reconciliation (stale-count > 0 → alert). - F → Incident 5: screen-flow access — 4 layers (run access, launch surface, data/FLS per element, component/Apex access); "nothing happens" = launch denied or element FLS halt; Debug-as-user reproduces it.
- Priorities: Tonight — stop the bleeding: disable the cleanup flow (Clue C, active deletion), deactivate ONE of the owner-sync flows (Clue D, active ping-pong), and kill the per-iteration-Get flow's volume (Clue A) or route around it. Next week — Clue B consolidation (one owner + bypass flags + sunset waves with reconciliation), Clue E fault paths + convergence field + reconciliation job, Clue F access matrix audit. Next quarter — automation governance program: ownership registry (one field = one owner), mandatory fault paths + observability on all 47 automations, sunset of legacy workflow rules, release checklist (bulk test at 200+, access matrix per profile, reconciliation proof).
- The shared root cause (find it): "The org has 47 automations, zero governance, zero observability, and no ownership discipline — every automation was shipped by a vendor as a deliverable, with no fault path, no owner, and no test of what it does to the rest of the org." The specific incidents are all symptoms of one disease: automation without governance and without observability. Say this first — it's also the interview answer to "why is the org like this?"
- The 3 verification steps (flow-version of regression tests): (a) Bulk test at 200+ records — run the corrected flows with 200 records in a sandbox, assert zero SOQL/DML limit hits and expected convergence (the flow-testing framework + debug); (b) Reconciliation proof — after each sunset wave, the nightly stale-count = 0 for two weeks (evidence gate, Incident 6); (c) Access matrix regression — a checklist run per profile (run access, surface, FLS per element, component access) executed by a sandbox user cloned from a rep profile (Incident 5's Debug-as-user as a test).
- The 2-minute answer (say out loud): "The org's automation problem is governance and observability, not any single flow. Every clue is the same disease: no fault paths, no ownership, no reconciliation, no access verification — 47 deliverables, none with a guardian. The fix is a program: one owner per field with bypass flags and sunset waves; fault paths and an error log on every automation; a reconciliation job as the evidence gate; a bulk test and an access-matrix check as part of every release. Tonight I stop the bleeding — the cleanup deletion and the sync ping-pong — and from Monday, every automation has a guardian."
THE KNOWLEDGE SPINE (the allowed 5% — memorize after the incidents)
The order of execution (reproduce cold — the #1 probe)
- Before-save Record-Triggered Flow (and formulas) — FIRST automation
- Before triggers (oldest first)
- Validation rules
- After triggers (oldest first)
- Assignment & escalation rules
- After-save Record-Triggered Flow (+ scheduled paths)
- Workflow rules (legacy) · Process Builder (deprecated)
The flow-vs-apex decision matrix (reproduce cold — the non-negotiable 2026 answer)
| Flow | Apex | |
|---|---|---|
| Logic complexity | Simple–moderate | Complex loops/aggregation/CPU |
| Maintainer | Admin (declarative) | Developer |
| Recursion control | Entry conditions, change detection, bypass flags | Static flags + change detection |
| Bulk/volume | 200-record chunks, shared limits | Batch/Queueable with full control |
| Error handling | Fault paths + error log | try/catch + tests |
| Testability | Flow testing framework, Debug | Unit tests + mocking |
| Callouts | Async-only (callout element) | Queueable + AllowsCallouts |
| Verdict | "Simple-moderate, admin-maintainable, standard CRUD/FLS" | "Complex, CPU, bulk, recursion, mocking" |
Flow types (one-liners)
- Record-Triggered (before-save): modify the triggering record, no DML, runs before validation; before triggers run AFTER it.
- Record-Triggered (after-save): related records, emails, Apex, Platform Events — Id exists; runs after after-triggers.
- Screen Flow: user-facing wizard (inputs, data tables, custom LWCs) — needs 4-layer access.
- Schedule-Triggered: time-based automation (digests, cleanup) — observability mandatory.
- Platform Event-Triggered: event-driven automation without Apex (external system publishes → flow acts).
- Autolaunched: invoked by other flows/Apex (
Flow.Interview) — function-call automation. - Legacy: Workflow Rules (legacy), Process Builder (deprecated).
Limits to know cold (flow-relevant)
| Limit | Value |
|---|---|
| Max elements per flow | 2,500 |
| Max versions per flow | 2,000 |
| Flow shares transaction governors | 100 SOQL / 150 DML / 10,000 rows / CPU |
| Record-triggered batch chunk | 200 |
| Get Records max rows | 10,000 (per Get element) |
sendEmail invocations per transaction | 10 |
| Recursion guard | per-flow, per-record (NOT cross-object) |
| Flow → Apex | @InvocableMethod / @InvocableVariable |
| Apex → Flow | Flow.Interview.createInterview(...).start() |
| Flow execution context | System mode by default (bypasses sharing/FLS unless configured) |
The 5 automation disciplines (fix 90% of incidents)
- Know the order of execution — and that ownership, not order, decides correctness.
- One owner per field — entry conditions + change detection + skip flags (Custom Metadata bypass for ops).
- Bulk-aware — never per-iteration Get inside a Loop; 200-chunk reality; shared limits.
- Fault paths + reconciliation — every data element logs; green run ≠ success; stale-count > 0 → alert.
- Access verification — 4 layers (run access, surface, data/FLS, component) + Debug-as-user.
Quick one-liners
- "$Record = the triggering record; assignable in before-save, needs DML in after-save."
- "IsChanged = entry-condition change detection; value-differs = convergence."
- "Fault path = the error branch on Get/Update/Delete/Send — interviewers love it."
- "Flow runs in system context by default — the security-aware candidate says this unprompted."
- "Bypass: entry conditions · guard fields · Custom Metadata flags (ops-flippable, no deploy)."
- "Screen flow 'nothing happens' = launch denied or element FLS halt — Debug-as-user finds it."
Rapid-fire trick questions (module 4 scope)
| Question | Answer |
|---|---|
| Before-save Flow vs before-trigger — who runs first? | Before-save Flow |
| After-save Flow vs after-trigger — who runs first? | After-trigger |
| Max elements per flow? | 2,500 |
| Max flow versions? | 2,000 |
| Record-triggered chunk size? | 200 |
| Does the recursion guard stop cross-object loops? | No — per-flow, per-record |
| "$Record" assignable where? | Before-save (after-save needs DML) |
| Flow execution context by default? | System mode (bypasses sharing/FLS) |
| Flow → Apex? | @InvocableMethod |
| Apex → Flow? | Flow.Interview.createInterview().start() |
| Fault path purpose? | Error handling on elements — the maturity signal |
| Get Records max rows? | 10,000 |
| sendEmail invocations per transaction? | 10 |
| Workflow Rules status? | Legacy; Process Builder — deprecated |
| Platform Event-Triggered Flow use case? | Event-driven automation without Apex |
| Schedule-Triggered vs Scheduled Apex? | Simple/declarative vs complex/retries/volume |
| Per-iteration Get in a Loop = ? | The #1 flow bulk killer |
| Green flow run proves? | No unhandled fault — NOT data convergence |
| Screen flow errors go where? | Failure email / View Details — not the user |
| "Works for admin only" first tool? | Debug mode — run as another user |
INTERLEAVED PRACTICE SET (do 1–2 per session, closed-book)
Pick the technique before solving — the choice is the training. Mixes Modules 1–4.
- Limit hunt (which wallet, which module): (a) a flow with a Loop calling Get Records per iteration on 400 records; (b) a screen flow whose Get element reads a field the rep's profile hides; (c) a before-save flow + before-trigger setting the same field; (d) a scheduled flow deleting on
LastModifiedDate < TODAY(); (e) an after-save flow sending 200 emails. - Design (2 minutes, closed notes): "notify the account team when a high-value opportunity closes." Which automation (flow/apex/scheduled/platform event)? Before- or after-save? Fault path on which elements? Idempotency guard? Bulk story for a 500-close Friday?
- Module-1 bridge: a record-triggered flow updates a related object, and an Apex after-trigger does the same — the value is wrong. Reconstruct the order of execution and name the two governance fixes (Incident 1 × Module 1's trigger ordering).
- Module-3 bridge: a flow needs to call an external system after a record is saved. Why can't it call out synchronously, and which two patterns (flow-side and Apex-side) solve it? (Module 3 rules #1 + async boundary, declarative form.)
- The one-card answer: write the complete "Flow or trigger, and why?" answer in 5 bullet lines — then say which incident each bullet maps to.
SPACED REPETITION SCHEDULE (log it in the canvas)
- Today: after each incident — retrieval drill + redo.
- Tomorrow: re-answer the 5-question drills from Incidents 1–4 (closed-book).
- +1 week: the Interleaved Practice Set + rapid-fire bank (all four modules).
- +1 month: the Capstone (re-do from memory) + Modules 1–3 capstones back to back.
Incident sources (real, for your curiosity): agentforcelens.com (order of execution, flow-vs-trigger walkthrough); flowinator.com (flow types, before/after, limits, debugging, fault paths, recursion); salesforceben.com (flow vs trigger analysis, platform event flows, sharing); thesalesforcemonk.com (automation decision framework); trailhead.salesforce.com (Flow Builder modules); developer.salesforce.com/docs (Flow Developer Guide, Invocable Apex); research agent 05 (flow + security question bank, 2026 interview weightage). Full URL list in _research/round1_master_report/agent_05_flows_security/sources.md + links_master.md.