Module 4 — ANSWER SHEET (SEALED)
Companion to 04_Topic04_Flows_Declarative.md — open ONLY after you have written your own attempt.
Protocol (file 00, M1→M4): write ≥2 hypotheses + 2 solution attempts before reading. Then compare your attempt against this sheet like an investigator compares a suspect's story to the evidence. The contrast IS the learning. Then REDO from memory, then do the drill.
INCIDENT 1 — THE FLOW THAT FOUGHT THE TRIGGER
The problem restated
Two automations write one field; the value is "wrong on some records." Reconstruct the order of execution, explain the variance, and give the governance fix.
Model answer (2-min interview version)
- Order of execution: Before-save Record-Triggered Flow runs FIRST — before before-triggers. Sequence: before-save Flow → before-triggers → validation → after-triggers → assignment/escalation rules → after-save Flow → workflow rules (legacy). The consultant's "mine runs after" is backwards; the trigger's "it's overriding me" is only half-right.
- Who wins: the last writer whose condition fires for that record's update pattern. On create and on shipping-field updates, the trigger (second) overwrites the flow → correct. The wrong records are where the trigger's condition didn't fire (e.g.,
ShippingCountryset to null, or updates not touching the mapped fields) while the flow's loose entry condition did — leaving the flow's hardcoded"USA"surviving. "Some records right, some wrong" = the firing-window overlap. - The fix (governance, not reordering): (1) one owner per field — delete the flow (the trigger is the proven real-data mapping) or move the logic to a flow with the same real mapping, never a hardcoded value; (2) if both must coexist during a merge period, gate the loser with entry conditions (
IsChanged(ShippingCountry)) and/or a guard (BillingCountry == null); (3) the skip-flag pattern for Apex-initiated changes (Custom Metadata-based bypass). - Bonus security point: flows run in system context by default — a hardcoded value in an unreviewed Flow is the most dangerous automation in the org.
Self-grade — you "got it" if you named:
- Before-save Flow runs before before-triggers
- Last writer whose condition fires wins
- "Some records" = the trigger's condition didn't fire (flow's value survives)
- One owner per field (not reordering)
- Flow runs in system context by default
THE REDO — model answer
1. Order: before-save Flow → before-trigger → validation → after-trigger → after-save Flow → workflow (legacy)
2. Winner: last writer with a firing condition for that record's update pattern
3. Wrong records: trigger condition false (null shipping, non-mapped update) + flow condition true
4. Fix: one owner per field; gate the loser (IsChanged / guard field / bypass flag); never hardcode in a flowRETRIEVAL DRILL — model answers
- Before-save Flow vs before-trigger? → Before-save Flow runs FIRST (the first automation to touch the record).
- After-save Flow vs after-trigger? → After-trigger runs first; after-save Flow runs after after-triggers (before assignment rules).
- Who wins with two writers on one field? → The last writer whose condition fires; the real answer is governance — one owner per field, entry conditions, skip flags.
- Two gating techniques? → Entry conditions /
IsChanged(condition-based) and skip/guard fields or Custom Metadata bypass (flag-based). - Default context of record-triggered flows? → System mode — bypasses sharing and CRUD/FLS unless configured otherwise.
INCIDENT 2 — THE LOOP THAT NEVER STOPPED
The problem restated
Two after-save flows sync ownership across objects and ping-pong forever. Why didn't the recursion guard stop it, and what's the corrected convergent design?
Model answer (2-min interview version)
- What the guard does: the built-in recursion guard stops a flow re-triggering on its own update of the same record. It does NOT stop cross-object loops — Flow A's writes are new legitimate changes for Flow B, and vice versa. Cross-object ping-pong is invisible to the guard.
- The three defenses (in order): (1) one owner, one direction — owner-sync is one-directional (Account→Contacts); delete Flow B; (2) echo-proof conditions — update only where
Contact.OwnerId != $Record.OwnerId(value-differs), so the second pass matches zero records → converges in one pass; (3) runaway dead-letter — Custom Metadata bypass flag (ops-killable) + a "last sync" timestamp to halt/log a re-run within seconds. - The one-liner:
IsChangedalone is not enough — the condition must be value difference (X != $Record.Y), not just "was written."
Self-grade — you "got it" if you named:
- Guard = per-flow, per-record, same-object only
- Cross-object ping-pong not stopped
- One owner, one direction
- Value-differs condition (
!= $Record.X) → converges in one pass - Custom Metadata bypass / last-sync dead-letter
THE REDO — model answer
Flow A (after-save, Account, IsChanged OwnerId):
Get Contacts WHERE OwnerId != $Record.OwnerId
Update Contacts.OwnerId = $Record.OwnerId
Flow B: DELETED (one owner, one direction)
Echo pass: Flow A re-runs on its own Contact updates → matches 0 contacts → stops.RETRIEVAL DRILL — model answers
- What does the recursion guard stop? → A flow re-triggering on its own update of the same record.
- Why is cross-object ping-pong invisible? → Each flow sees the other's writes as fresh legitimate changes on a different object.
- Three defenses? → One owner/direction; value-differs conditions; bypass flag + last-sync dead-letter.
- Why isn't
IsChangedenough? → It fires on any write; write-identical values still count as changes → the echo re-runs. Value-difference (!= $Record.X) makes the second pass a no-op. - Get Records ceiling + escape hatches? → 10,000 rows; chunk via Loop + subflows or move to Apex.
INCIDENT 3 — THE 500-CASE EMAIL STORM
The problem restated
Mass-close 500 cases → email storm, SOQL wall, silent partial failure, duplicates. Is "Flow handles bulk" true, and what's the corrected design?
Model answer (2-min interview version)
- The bulk truth: record-triggered flows process in chunks of 200 with entry conditions per record — but they share the transaction's governor limits (100 SOQL / 150 DML / 10,000 rows / CPU) and the 10
sendEmailinvocations cap. A per-recordGet Recordsinside a Loop = 500 SOQL →Too many SOQL queries: 101; per-record email elements blow the send cap. Batching is automatic; limits are not. Flow is not unlimited. - The three fixes: (1) batched data access — Get Records once per batch (filter over the collection), never per-record; (2) email discipline + idempotency — one send step guarded by
Notification_Sent__c = false(checked + set in the same transaction) — re-runs/retries can't double-send; (3) fault path + error log on every element →Error_Log__c(Case Id, element, error, timestamp). - The meta-rule: the "second email step" fix created the duplicates; the guard + fault path is the fix.
Self-grade — you "got it" if you named:
- 200-record chunks + shared governor limits
- 10 sendEmail invocations per transaction
- Per-record Get in a Loop = #1 bulk killer
- Guard field idempotency (
Notification_Sent__c = false) - Fault path + error-log object
THE REDO — model answer
Flow (after-save, Case, entry: Status IsChanged = 'Closed'):
Get Contacts (filter: Id IN batch, joined to cases) ← once per batch
Loop → Send Email (guarded by Notification_Sent__c = false)
Update Case.Notification_Sent__c = true
Fault paths on Get + Send → insert Error_Log__cRETRIEVAL DRILL — model answers
- Chunk size? → 200 records per chunk for record-triggered flows.
- Shared limits (name 4)? → 100 SOQL, 150 DML, 10,000 rows, CPU time (and 10 sendEmail invocations).
- sendEmail cap? → 10 invocations per transaction.
- #1 flow bulk killer? → Per-iteration Get Records inside a Loop (the declarative per-record SOQL loop).
- Email idempotency guard? → A checkbox field (
Notification_Sent__c) checked and set in the same transaction; entry condition prevents re-sends.
INCIDENT 4 — THE FLOW THAT WENT ROGUE AT 3:00 AM
The problem restated
A scheduled "quarterly cleanup" ran daily and deleted this quarter's quotes. Name the failures, the tool decision, and the safe-delete design.
Model answer (2-min interview version)
- Three decision failures: (1) schedule semantics — "quarterly" configured as "every day at 2 AM"; the schedule is the source of truth and nobody read it; (2) filter logic —
LastModifiedDate < TODAY()= "not modified today" (matches everything), not "older than 90 days" (< TODAY() - 90); (3) observability — no fault path, no alert, no audit → a month of silent deletions. - Tool matrix: Schedule-Triggered Flow = declarative scheduled automation (digests, cleanup, status updates), admin-maintainable; Scheduled Apex = callouts with retries, complex logic, heavy volume, testability. Pick per job; for a billing sync with retry → Scheduled Apex (Module 3's design).
- Safe-delete design (5 points): explicit retention filter (
< TODAY() - 90); business guards (status exclusions, retain flag); dry-run first month; fault path + summary email + audit record; anomaly threshold (candidate count > 10× historical → halt + alert). - One-liner: never delete in automation without a guard field, a dry run, and a human-readable schedule that matches the intent.
Self-grade — you "got it" if you named:
- Schedule semantics (quarterly became daily)
-
TODAY()date-relative filter bug - No observability (fault path/alert/audit missing)
- Flow vs Scheduled Apex matrix
- Safe-delete: retention filter + guards + dry-run + audit + threshold
THE REDO — model answer
Schedule: Quarterly, named "Quarterly Quote Retention (90 days)"
Get Records: Quote WHERE LastModifiedDate < TODAY() - 90
AND Status NOT IN ('Approved','Invoiced')
Decision: if count > threshold → fault + alert (halt)
Delete Records
Summary email + audit record insert (examined/deleted counts)
Fault path → Error_Log + alert emailRETRIEVAL DRILL — model answers
- Flow vs Apex for scheduled jobs? → Flow: declarative digests/cleanup/status updates. Apex: retries, callouts, complex logic, heavy volume, testability.
- Why is
LastModifiedDate < TODAY()wrong? → It matches every record not modified today — it's "not modified today," not "older than N days." Correct:< TODAY() - 90. - 3 mandatory observability elements? → Fault path + alert email, completion summary (email + audit), anomaly threshold halt.
- Delete safeguards (4)? → Retention filter, business guards/retain flag, dry-run first month, audit + summary, anomaly threshold.
- Scheduled path? → A record-triggered flow's time-based wait (e.g., "2 hours after the trigger, then act") — distinct from a standalone schedule-triggered flow.
INCIDENT 5 — THE WIZARD NOBODY COULD OPEN
The problem restated
Screen flow launches for the admin but "nothing happens" for reps. List the access layers, the invisible causes, and the debug procedure.
Model answer (2-min interview version)
- The four access layers: (1) run access — profile/permission set grants "Run Flows" + the flow's Run access; (2) launch surface — button/action visibility on the record page per profile; (3) data access — object + FLS on every element (Get/Update fields, screen fields); (4) component access — the LWC's Apex class enabled ("Apex Classes" in the profile).
- The two invisible causes of "nothing happens": (1) launch-access denial — the rep lacks run access → the action silently no-ops; (2) element-level FLS halt — the flow starts, an element can't read a field → the run halts, and screen-flow errors go to the failure email / View Details / Flow Runs, NOT to the user.
- Debug procedure: (1) Debug mode → "Run as another user" — reproduces the rep's context exactly (the admin's Debug works because the admin has full access); (2) check View Details / Flow Runs for the failed interview (failing element + error); (3) enable the failure email; (4) audit the access matrix per profile (run, surface, data/FLS, component); (5) test the LWC standalone.
- One-liner: visible ≠ runnable; the button being on the page only proves layer 2, not the launch.
Self-grade — you "got it" if you named:
- The 4 access layers
- Silent no-op on launch denial
- Element FLS halt + errors go to failure email/View Details, not the user
- Debug-as-user as the #1 tool
- LWC/Apex class access (layer 4)
THE REDO — model answer
1. Layers: run access → surface → data/FLS → component (LWC/Apex)
2. Invisible causes: launch denial (no-op) + element FLS halt (error in View Details/failure email)
3. Debug: Debug-as-user → View Details/Flow Runs → failure email → access-matrix audit → LWC standaloneRETRIEVAL DRILL — model answers
- Four layers? → Run access (profile/permission set), launch surface (button/page), data access (object + FLS per element), component access (LWC/Apex enabled).
- Where do screen-flow errors go? → Failure email (if configured), View Details / Flow Runs, fault paths. NOT to the user's UI.
- Two invisible causes? → Launch-access denial (silent no-op) and element-level FLS halt (flow starts then stops, error only in logs).
- #1 debug tool for "admin only"? → Debug mode → "Run as another user" (reproduces the rep's context).
- Why doesn't the visible button prove access? → The button only proves the surface layer; launch still needs run access + data access + component access.
INCIDENT 6 — THE SILENT GRAVEYARD
The problem restated
A sync flow shows green but data is stale. What does green actually prove, and what's the fault-aware + reconciliation design?
Model answer (2-min interview version)
- What green proves: the interview completed without an unhandled fault. Element-level failures (an Update inside a Loop failing for one iteration — validation, lock, FLS edge) can be invisible; the run record still reads success. Partial failure is invisible by default; the fault path is the only built-in instrument.
- The fault-aware design: (1) fault paths on every data element (Get/Update/Delete/Send) →
Error_Log__c(element, record Id, error, timestamp) + threshold alerts; (2) queryable convergence field —Account_Status_Last_Synced__c→ gaps areWHERE ... < TODAY()(or null); (3) reconciliation job — nightly scheduled flow counts stale records (expected ≠ actual) → > 0 = alert + email list. - Why both instruments: fault paths catch what the platform reports; reconciliation catches what it doesn't. Two instruments because each misses a class.
- The meta-rule: success = "every record converged, and we can prove it," not "no unhandled fault." The proof is a convergence field + a reconciliation job.
Self-grade — you "got it" if you named:
- Green = no unhandled fault, NOT convergence
- Element failures inside a Loop can be invisible
- Fault paths on data elements → error log + alerts
- Queryable convergence field
- Nightly reconciliation (stale-count > 0 → alert)
THE REDO — model answer
Fault path on Get Opportunities → Error_Log + alert
Fault path on Update Opportunities → Error_Log per failing record + threshold alert
Add Account_Status_Last_Synced__c (set on success) → queryable gaps
Nightly: count Opportunities WHERE Account_Status__c != Account.Account_Status__c
(or stale last-synced) → > 0 → alert + email listRETRIEVAL DRILL — model answers
- What does a green flow run prove? → No unhandled fault — NOT data convergence.
- Where do element failures inside a Loop go without a fault path? → Nowhere visible — possibly aborted silently; the run can still show success.
- Three components of the fault-aware design? → Fault paths + error-log object, queryable convergence field, reconciliation job.
- Why both fault paths AND reconciliation? → Fault paths catch reported failures; reconciliation catches silent element failures. Each misses a class.
- What does the convergence field power? → The dashboard query and the reconciliation job's WHERE clause.
INCIDENT 7 — THE FLOW THAT ATE THE GOVERNOR
The problem restated
A grown autolaunched flow hits the element/version walls, SOQL walls from nested per-iteration Gets, and a sync callout. Apply the matrix and design the migration.
Model answer (2-min interview version)
- The three walls: (1) 2,500 max elements — the flow is 2,800 → it cannot save a new version; (2) 2,000 max versions — history is capped; (3) shared transaction governors — per-iteration Get Records in a nested loop = the declarative per-record SOQL loop → 101st SOQL crashes on a 400-line quote; plus no sync callout in a transaction (the HTTP callout element hits the same uncommitted-work wall as Module 3).
- The matrix verdict: Apex-wins criteria hit 4/5: complex loops/aggregation ✓, CPU-intensive ✓, bulk volume ✓, recursion control ✓, mocking/testability ✓. Flow-wins criteria (simple-moderate, admin-maintainable, standard CRUD/FLS) no longer hold. Verdict: the pricing engine moves to Apex.
- The migration path: (1) Invocable Apex bridge —
@InvocableMethodQuotePricingService(input: Quote Id; output: totals) — the flow stays the orchestrator (entry conditions, fault path, admin-tweakable), calls the action in ONE element → element count drops; (2) nested loops → one bulkified Apex call (proper SOQL); (3) the callout → Queueable withAllowsCallouts+ retry/backoff (Module 3 Incident 8), enqueued by the flow/Apex — never sync; (4) tests — Apex engine gets test classes with mocks. - One-liner: Flow is not unlimited; when the toolset can't express the logic safely, Invocable Apex is the bridge that keeps declarative orchestration while Apex owns the heavy math.
Self-grade — you "got it" if you named:
- 2,500 elements / 2,000 versions (exact numbers)
- Shared governors + no sync callout in transaction
- Matrix verdict (complex/CPU/volume/recursion/mocking → Apex)
- Invocable Apex bridge (@InvocableMethod) keeps flow as orchestrator
- Callout → Queueable with AllowsCallouts
THE REDO — model answer
@InvocableMethod
public static List<QuoteTotal> compute(List<QuoteRequest> reqs) {
// bulkified SOQL, pricing math, no per-record gets
}
Flow: entry conditions → Invocable action (1 element) → fault path → next decision
Callout: Queueable + AllowsCallouts + retry/backoff (enqueued, never sync)RETRIEVAL DRILL — model answers
- Max elements/versions? → 2,500 elements; 2,000 versions per flow.
- Three shared limits? → 100 SOQL, 150 DML, 10,000 rows (plus CPU).
- Apex-wins criteria? → Complex loops/aggregation, CPU-intensive work, bulk volume, recursion control, mocking/testability.
- Flow → Apex? →
@InvocableMethod+@InvocableVariable— the flow calls the action as one element. - Apex → Flow? →
Flow.Interview.createInterview(flowName, inputs).start().
INCIDENT 8 — THE TWO AUTOMATION WAR
The problem restated
Three automations write one field; value flaps; nobody owns it; removal is "breaking a deliverable." Consolidate safely.
Model answer (2-min interview version)
- The 4 symptoms: (1) plural ownership of one field; (2) value flapping per update pattern (order-of-execution race: before-save flow first, before-trigger second, workflow last-ish); (3) no source of truth; (4) deliverable politics (each automation is a vendor invoice).
- The consolidation verdict: the recompute needs transaction data, recursion control, and testability → Apex trigger survives (matrix). The before-save flow is redundant (approximates the exact computation) → dies. The workflow rule is legacy → dies. One writer remains. The fix isn't reordering — it's removing until one remains.
- The migration + sunset procedure: (1) bypass patterns first — entry conditions +
IsChanged; a skip/guard field (VIP_Score_Computed_By__cset by the trigger, checked by the flow); Custom Metadata-based bypass (Automation_Bypass__mdt.Disable_VIP_Score_Flow__c) ops can flip with zero deploy; (2) sunset in waves — deactivate workflow → 2-week watch (nightly reconciliation: expected vs actual) → deactivate flow → 2-week watch → delete both; (3) each wave reversible; reconciliation is the evidence gate. - The governance rule to state: "One field, one owner, one path. Every other automation is gated by bypass or deleted — deliverables are not an excuse for dual ownership."
Self-grade — you "got it" if you named:
- The 4 symptoms (incl. deliverable politics)
- Order-of-execution race → flapping
- Survivor = Apex trigger (matrix: transaction data/recursion/tests)
- 3 bypass mechanisms (conditions, guard field, Custom Metadata)
- Sunset in waves + reconciliation evidence gate
THE REDO — model answer
Symptoms: plural ownership, flapping value, no source of truth, deliverable politics
Survivor: Apex before-trigger (recompute from transactions) — ONE writer
Bypass: entry conditions + IsChanged; guard field; Automation_Bypass__mdt flags
Sunset: deactivate workflow → 2-week reconcile → deactivate flow → 2-week reconcile → delete
Rule: one field, one owner, one pathRETRIEVAL DRILL — model answers
- Four symptoms? → Plural ownership, value flapping, no source of truth, deliverable politics.
- Who survives and why? → The Apex trigger — it needs transaction data, recursion control, and testability (matrix). Flow and workflow are removed.
- Three bypass mechanisms? → Entry conditions + IsChanged; skip/guard fields; Custom Metadata-based bypass flags.
- Sunset sequence? → Deactivate one automation at a time → 2-week watch with nightly reconciliation → delete; reversible waves with an evidence gate.
- The one-line governance rule? → One field, one owner, one path — deliverables are not an excuse for dual ownership.
🏆 CAPSTONE — THE ORG THAT AUTOMATES ITSELF (model report)
- Clue-by-clue:
- A → Incident 7: per-iteration Get in a Loop (declarative per-record SOQL) → 100-SOQL wall. Fix: batched Get once, or move logic to Apex (matrix).
- B → Incidents 1+8: three writers, one field → order-of-execution race. Fix: one owner (Apex trigger), bypass flags + entry conditions, sunset flow + legacy workflow in waves with reconciliation.
- C → Incident 4:
LastModifiedDate < TODAY()= "not modified today" → a month of night deletions. Fix:TODAY() - 90+ business guards + dry-run + audit/summary + anomaly threshold. - D → Incident 2: cross-object ping-pong — recursion guard is per-flow, per-record. Fix: one owner, one direction, only-differs conditions → converges in one pass.
- E → Incident 6: green run ≠ success — no fault paths → silent partial failures. Fix: fault paths + error log, convergence field, nightly reconciliation.
- F → Incident 5: screen-flow access — 4 layers; "nothing happens" = launch denied or element FLS halt. Fix: access-matrix audit + Debug-as-user.
- Priorities: Tonight — stop the bleeding: disable the cleanup flow (C, active deletion), deactivate one owner-sync flow (D, active ping-pong), and stop the per-iteration-Get volume (A) or route around it. Next week — B consolidation (owner + bypass + sunset waves), E fault paths + convergence + reconciliation, F access audit. Next quarter — automation governance program: ownership registry, mandatory fault paths + observability on all 47 automations, sunset legacy workflow rules, release checklist (bulk test at 200+, access matrix per profile, reconciliation proof).
- The shared root cause: "The org has 47 automations, zero governance, zero observability, and no ownership discipline — every automation was shipped as a deliverable with no fault path, no owner, and no test of its effect on the org." One disease: automation without governance and observability.
- The 3 verification steps: (a) bulk test at 200+ records in sandbox — assert zero limit hits + expected convergence (flow testing framework + Debug); (b) reconciliation proof — stale-count = 0 for two weeks after each sunset wave (evidence gate); (c) access-matrix regression — checklist run per profile by a sandbox user cloned from a rep profile (run access, surface, FLS per element, component access).
- 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 in every release. Tonight I stop the bleeding — the cleanup deletion and the sync ping-pong — and from Monday, every automation has a guardian."
KNOWLEDGE SPINE — rapid-fire (model answers)
- 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.
$Recordassignable where? → Before-save (after-save needs DML).- Flow execution context by default? → System mode (bypasses sharing/FLS).
- Flow → Apex? →
@InvocableMethod/@InvocableVariable. - Apex → Flow? →
Flow.Interview.createInterview().start(). - Fault path purpose? → Error handling on Get/Update/Delete/Send — 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 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 — model answers
- Limit hunt: (a) Module 4 — per-iteration Get in a Loop → 100-SOQL wall (Incident 7); (b) Module 4 — screen flow FLS halt (Incident 5); (c) Module 4 — dual ownership / order-of-execution race (Incident 1); (d) Module 4 — date-relative filter + no observability (Incident 4); (e) Module 4 — 10 sendEmail cap (Incident 3).
- Design (2 min): after-save record-triggered flow on Opportunity (entry: Stage = Closed Won AND IsChanged(Stage)); Get Account Team members (batched); Send Email guarded by
Notification_Sent__c = false; fault paths on Get/Send → Error_Log + alert. Bulk story: batched Get (not per-record), guard field prevents double-send on re-runs; if > 10 sends needed per transaction, switch to digest emails or Platform Event–driven async send (Module 3 async boundary). - Module-1 bridge: order-of-execution: before-save flow → before-trigger → validation → after-trigger → after-save flow. Two governance fixes: (1) one owner per field (delete/relegate one writer); (2) gate the loser (entry conditions / skip flag) — plus Apex static-flag/change-detection equivalent from Module 1 Incident 2.
- Module-3 bridge: no sync callouts in a transaction (uncommitted-work rule — same wall as Apex triggers). Flow-side: the callout element is async-only / restricted; Apex-side: Queueable +
Database.AllowsCallouts(with retry/backoff, Module 3 Incident 8). Enqueue from the flow via an Invocable action or from the trigger — the async boundary is the design. - One-card answer (5 bullets + incident map): (1) Decision matrix first — simple-moderate + admin-maintainable → Flow; complex/CPU/bulk/recursion/mocking → Apex (I7/I8); (2) Order of execution — before-save flow first, after-save flow last (I1); (3) One owner per field + entry conditions + bypass flags (I1/I8); (4) Bulk-aware — batched Gets, shared limits, 200-chunk reality (I3); (5) Fault paths + reconciliation — green ≠ converged (I6).
THE ONE-CARD ANSWER KEY (carry this)
"Flow or trigger, and why?" — 5 lines:
- Decision matrix, not preference: simple-moderate, admin-maintainable, standard CRUD/FLS → Flow; complex loops/CPU, bulk volume, recursion control, testability → Apex.
- Order of execution: before-save Flow runs FIRST (before before-triggers); after-save Flow runs AFTER after-triggers — last writer whose condition fires wins.
- One owner per field: entry conditions +
IsChanged+ value-differs conditions + Custom Metadata bypass flags — dual ownership is the disease, not the order. - Bulk-aware: batched Gets (never per-iteration), 200-record chunks, shared 100 SOQL / 150 DML / 10,000-row / 10-sendEmail limits — Flow is not unlimited.
- Fault paths + reconciliation: every data element logs; green run ≠ convergence; stale-count > 0 → alert. And flows run in system context by default — security-aware design applies everywhere.
Numbers to say cold: before-save flow < before-trigger < validation < after-trigger < after-save flow < workflow · 2,500 elements · 2,000 versions · 200-chunk · 10,000 Get rows · 10 sendEmail · @InvocableMethod (Flow→Apex) · Flow.Interview (Apex→Flow).