When building Salesforce Field Service for a local council’s waste‑collection operations, asynchronous Apex stops being an academic topic and starts becoming a daily architectural concern.
On paper, async rules are easy to remember:
@futurecan’t call@future- Async can’t start async
@futureaccepts only primitives- Queueable Apex is more powerful
But once we implemented a real‑world, event‑driven workflow—triggered the moment a waste collection Work Order is completed—we quickly discovered that the nuance behind these rules matters more than the rules themselves.
This post walks through three async Apex nuances we encountered while processing waste‑collection data at scale—and how understanding Salesforce’s intent behind the rules helped us build a robust, governor‑friendly solution.
The Use Case: Waste Collection in Salesforce Field Service
In our implementation, Salesforce Field Service was used to manage:
- Work Orders – One per waste pickup (general waste, recycling, green waste)
- Service Appointments – Assigned to collection vehicles and drivers
- Work Order Line Items – Individual collection tasks
- Post‑Completion Processing – Once a bin is collected, multiple downstream actions must occur
When a collection is marked Completed by the driver in the Field Service mobile app, Salesforce must:
- Update asset and service history
- Create billing or service entitlement records
- Apply penalties or exceptions (missed pickups, contamination)
- Push updates to external council systems (reporting, GIS, integrations)
- Trigger resident notifications asynchronously
This is where async Apex becomes unavoidable.
Nuance #1: “Async Can’t Call Async”… Except When It Can
The Initial Design (and Failure)
Our first instinct was simple:
- Use an after update trigger on Work Order
- Call an
@futuremethod to handle post‑collection processing - Inside that future method, fan out into other async processes
That approach failed immediately.
@futurepublic static void postCollection(Id workOrderId) {notifyExternalSystems(workOrderId); // another @future}
Salesforce rightly blocked this:
Future method cannot be called from a future method.
Why Salesforce Enforces This
In waste collection, a single truck can complete hundreds of Work Orders per day. If each async job could spawn more async jobs without restriction:
- The async queue could explode
- Execution ordering would be unpredictable
- Platform resources would be exhausted
Salesforce’s rule here is about fan‑out control.
Why Queueable Apex Solved It
We refactored the design using Queueable Apex as the orchestration layer.
System.enqueueJob(new PostCollectionQueueable(workOrderId));
Inside the Queueable:
public void execute(QueueableContext context) {updateServiceHistory();System.enqueueJob(new IntegrationQueueable(workOrderId));}
Why This Works
Even though the job is started asynchronously:
Queueable.execute()runs with synchronous governor limits- Salesforce allows one child Queueable per job
- The platform can track and govern the entire chain
Salesforce treats this as controlled async orchestration, not free‑form async recursion.
Real‑World Benefit
For waste collection:
- Each completed Work Order becomes one predictable chain
- Complex processing is serialized intentionally
- Failures are isolated and debuggable
Controlled scalability, not chaos
Nuance #2: Stale Data and Why @future Rejects sObjects
The Stale Data Risk in Field Service
Imagine this scenario:
- Driver completes a Work Order at 7:45 AM
- An
@futurejob starts minutes later - In between:
- The Work Order is reassigned
- The Service Appointment is corrected
- Council admin updates contamination notes
If we passed a full WorkOrder record into a future method, Salesforce would be forced to operate on potentially invalid state.
That’s why this is illegal:
@futurepublic static void processWorkOrder(WorkOrder wo) { }
Why Queueable Allows sObjects (and Why That’s Dangerous)
Queueable Apex allows this:
public class CollectionQueueable implements Queueable {private WorkOrder wo;
public CollectionQueueable(WorkOrder wo) {this.wo = wo;}
public void execute(QueueableContext context) {System.debug(wo.Status);}}
Why?
- Queueable jobs serialize their entire state
- Salesforce restores the object exactly as it was when enqueued
- The platform shifts responsibility to the developer
What We Did in Practice
Even though we could pass Work Orders directly, we chose not to.
Best practice we followed:
public void execute(QueueableContext context) {WorkOrder freshWO = [SELECT Status, AssetId, ServiceTerritoryIdFROM WorkOrderWHERE Id = :workOrderId];}
Why This Matters for Councils
- Data correctness is legally and financially important
- Reports and external systems must reflect current truth
- Queueable gives flexibility—but not safety guarantees
Trust, not guardrails
Nuance #3: “Can I Pass sObjects to @future If I Serialize Them?”
Yes. Technically.
String payload = JSON.serialize(workOrder);@futurestatic void process(String json) {WorkOrder wo = (WorkOrder) JSON.deserialize(json, WorkOrder.class);}
But we deliberately avoided doing this.
Why This Is a Bad Fit for Field Service
- Serialized state doesn’t respect later updates
- Deletes aren’t detected
- Permission and sharing changes are ignored
- Debugging becomes harder under load
Salesforce doesn’t block this approach—but it clearly discourages it by design.
In regulated, high‑volume public‑sector implementations, fresh queries always win.
Final Architecture Pattern We Landed On
Trigger → Queueable Orchestrator → Chained Queueables → Re‑query data inside each execution → Optional @future callouts for external notifications
This gave us:
- Controlled async execution
- Fresh and accurate Work Order data
- Scalable processing for thousands of collections per day
- Clear separation of responsibilities
Key Takeaway
Salesforce’s async “rules” are not arbitrary restrictions—they are deliberate platform protections. Once you understand what Salesforce is protecting, the exceptions stop feeling inconsistent and start feeling intentional.
In large‑scale Field Service implementations—especially public‑sector systems like waste collection—async Apex isn’t about clever code. It’s about respecting transaction boundaries, governor limits, and data integrity.
And Queueable Apex, when used thoughtfully, becomes the backbone that makes this possible.