Bulk Case Closure That Doubles Up Tasks
Halcyon Field Service closes cases in bulk. An after-save record-triggered flow on Case stamps Asset.Last_Service_Date__c and creates a follow-up Task. A before-save flow sets Priority when a custom Severity__c field changes. A legacy workflow rule with a field update on Case is still active, along with two Process Builder processes that update Case.Status__c. Last month a Data Loader job closed 8,000 cases in the nightly window. It failed with 'Apex CPU time limit exceeded' after 40 minutes; when it was rerun the next night, agents found about 300 cases with two identical follow-up tasks and roughly 60 assets whose Last_Service_Date__c was stale despite a task being created.
1. Explain the duplicate tasks and the stale asset dates, naming the mechanics rather than the symptoms.
Read the worked answer
Duplicate tasks: recursion through field-update re-saves. When a workflow rule (or Process Builder) performs a field update, the record is saved again. That second save re-fires before-save logic and the after-save flow, so the flow that creates a Task executes twice for the same case. Nothing in the flow is idempotent - there is no guard field and no entry criteria that would block the second pass. The reopen-then-close scenario is the same disease: the flow has no memory of having run.
Stale asset dates: partial success across batches. Each Data Loader batch of 200 is its own transaction. The 8,000-record run hit the CPU limit mid-flight; batches committed before the failure kept their tasks, and a batch that faulted without a fault path rolled back its asset stamp while an earlier pass had already created a task. Rerunning the whole file then produced a second task for every case that already had one, and left the asset untouched wherever the flow faulted.
Why it is slow in the first place: the after-save flow crosses objects (Case โ Asset, Case โ Task) one record at a time, and it is stacked on top of a workflow field update plus two Process Builders that each cause their own save cycle. Four automations firing per record, thousands of times, is how you reach a CPU limit.
2. Rewrite the automation so it is bulk-safe and idempotent. Be specific about which flow type does what.
Read the worked answer
Consolidate to two flows and delete the rest.
Before-save flow (same record only, no DML):
- Set Priority from Severity__c.
- Set the guard field when the case reaches Closed:
Follow_Up_Created__c = trueonly after the task exists - which means it cannot live here, so instead use a 'closing trigger' field set here (e.g.,Closure_Stamp__c), and let the after-save flow use it.
After-save flow (does the DML):
- Entry criteria:
IsClosed = true AND Follow_Up_Created__c = false. This is what makes it idempotent - the workflow field update re-save and any manual re-save hit the criteria and stop. - Create Task.
- Update the case:
Follow_Up_Created__c = true. That update re-enters the flow, the entry criteria fail, and the loop terminates after one extra evaluation. - For assets: Get Records on the related Assets into a collection, then Update Records using that collection, so the flow performs one DML for many records rather than one per case. Where the asset update is heavy, move it to a scheduled path so it runs outside the closing transaction.
Retire the workflow rule and both Process Builders - migrate their field updates into the before-save flow, then deactivate and delete. Before-save flows on the same object are the correct home for field updates and cost far less than the old engines.
Still to check: whether any remaining automation writes back to Case after save; every such write is a fresh recursion risk.
3. What is your proof that the fix works before it reaches production?
Read the worked answer
Prove it with volume, not with a single test record.
- Sandbox with production-like data. Load 8,000 cases in batches of 200, in the same window, with the same integration user.
- Assert the invariants, not the vibe:
count(Task where CaseId in closed set) == count(closed cases), and every closed case has a non-nullAsset.Last_Service_Date__cwhere an asset exists. Write these as SOQL counts before and after. - Read the limits block in a debug log from the largest batch. CPU time should be an order of magnitude below the limit; if it is not, the flow is still doing per-record DML.
- Exercise the re-entry paths explicitly: reopen then re-close a case, run a second Data Loader pass over the same file (it should create zero new tasks), and close a case from the UI and from the API.
- Test the fault path - point one row at a missing asset and confirm the batch survives and the error is visible, rather than a silent rollback.
- Only then deploy, and keep the old flow inactive-but-present for one release so reverting is a status change rather than a rebuild.