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:

  • @future can’t call @future
  • Async can’t start async
  • @future accepts 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:

  1. Update asset and service history
  2. Create billing or service entitlement records
  3. Apply penalties or exceptions (missed pickups, contamination)
  4. Push updates to external council systems (reporting, GIS, integrations)
  5. 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 @future method to handle post‑collection processing
  • Inside that future method, fan out into other async processes

That approach failed immediately.

@future
public 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 @future job 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:

@future
public static void processWorkOrder(WorkOrder wo) { }
Salesforce forces us to pass IDs only, ensuring fresh data is queried at execution time.

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, ServiceTerritoryId
        FROM WorkOrder
        WHERE 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);
@future
static 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

TriggerQueueable OrchestratorChained QueueablesRe‑query data inside each executionOptional @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.