Loading...

Apex Queueable Chaining: State Across Async Invocations

A multi-step async workflow involves processing 100K records across five distinct stages. Each stage is too much for one transaction. Queueable chaining keeps the work going across invocations without losing state, governor limits, or the ability to retry.

Apex Queueable Chaining: State Across Async Invocations

A typical multi-step async workflow: process 100,000 records through five stages. Validate. Enrich from an external API. Apply business rules. Update related records. Send notification. Each stage by itself is too much for one transaction. The whole thing in one Apex method blows past every governor limit Salesforce has.

Queueable chaining is the standard pattern for stitching such workflows together. Each stage runs as its own Queueable. State passes between stages via constructor parameters. The framework handles the async transitions. Done right, the workflow is observable, retryable, and bounded by the platform's job queue limits rather than transaction limits.

Done wrong, the workflow loses state, retries the wrong work, or quietly drops records when a chain link fails. The difference is design discipline.

What chaining actually does

A Queueable can enqueue another Queueable from within its execute method:

public class StageOneQueueable implements Queueable {
  private List<Id> recordIds;

  public StageOneQueueable(List<Id> ids) {
    this.recordIds = ids;
  }

  public void execute(QueueableContext qc) {
    // ... do stage one work ...

    // Hand off to stage two.
    System.enqueueJob(new StageTwoQueueable(this.recordIds, otherState));
  }
}

The framework guarantees that the second Queueable runs in a fresh transaction with fresh governor limits. The chain can continue indefinitely (subject to the per-org async job concurrency limits).

State that needs to persist across stages travels via the constructor of the next Queueable. The state must be Serializable (primitives, sObjects, lists/maps/sets of those, or classes that implement Serializable).

When chaining is the right tool

Three patterns where chaining fits cleanly:

1. Multi-stage workflows with logical separation. Stage 1 validates, Stage 2 enriches, Stage 3 commits. Each stage has its own concerns; chaining keeps them in separate classes.

2. Work that exceeds one transaction's governor limits. A single stage is fine; the total work is not. Split into chunks, chain through them.

3. Workflows that involve callouts plus DML. Apex restricts the mix of callouts and DML in a single transaction in specific ways. Chaining lets each step be cleanly one or the other.

When chaining is the wrong tool:

  • Volumes above 50,000 records per stage. Use Batch Apex.
  • Genuinely parallel work that does not need ordering. Use Queueables enqueued concurrently, not chained.
  • Single-stage work. Just use one Queueable; chaining adds complexity without benefit.

Passing state safely

The constructor parameter is the only way state flows from one Queueable to the next. Three patterns work:

Pass the data directly. For small state (a few IDs, a few flags), put it in the next Queueable's constructor. Simple, no external dependency.

System.enqueueJob(new StageTwoQueueable(
  recordIds,           // the records still being processed
  enrichmentResults,   // results from stage one to pass forward
  retryCount           // attempt counter for backoff logic
));

Pass an ID to a record that holds state. For larger state (a 50,000-record list of mappings), store the state in a custom object or platform cache and pass only the record ID forward.

// Persist state record.
ProcessingState__c state = new ProcessingState__c(
  RecordIds__c = JSON.serialize(recordIds),
  Stage__c = 'EnrichmentComplete'
);
insert state;

// Pass only the state record's ID.
System.enqueueJob(new StageTwoQueueable(state.Id));

Pass a reference to platform cache. For state that should not persist on the database, use Cache.Org.put(...) with a session-scoped key. The next Queueable retrieves via Cache.Org.get(key).

For most cases, the first pattern (direct constructor parameters) is the right starting point. Move to a persisted-state pattern when the constructor data grows large or when state needs to survive a job failure.

Error handling and retry

Async jobs fail. Network blips during callouts, transient governor limit issues, downstream record locks. The chain needs a strategy for what happens when a stage errors out.

Three retry strategies:

Exponential backoff in the same chain. The failing stage catches the exception, enqueues itself with a retry count, and the next attempt uses a longer delay (via System.enqueueJob after a System.now() + delay wait, or via a Scheduled job).

public void execute(QueueableContext qc) {
  try {
    // ... stage logic ...
  } catch (CalloutException e) {
    if (this.retryCount < 3) {
      System.enqueueJob(new ThisStage(this.recordIds, this.retryCount + 1));
    } else {
      logError(e, this.recordIds);
    }
  }
}

Dead-letter queue for unrecoverable failures. Records that fail all retries land in a custom DLQ object for manual review.

Job status tracking. A custom object captures the chain's state at each stage: which records succeeded, which failed, where the chain stopped. Used for operational visibility and retry-on-demand.

The right strategy depends on the business cost of a dropped record vs the engineering cost of retry infrastructure. Most production workflows use some combination of in-chain retries plus a DLQ for genuinely failed cases.

Observability

Async chains run invisibly. Operations teams need a way to see what is happening.

Two patterns:

AsyncApexJob queries. The platform tracks every async job in the AsyncApexJob table. Query for status, completion time, errors:

SELECT Id, JobType, Status, ApexClass.Name, ExtendedStatus,
       CompletedDate, NumberOfErrors
FROM AsyncApexJob
WHERE CreatedDate = TODAY
  AND ApexClass.Name LIKE '%Queueable%'
ORDER BY CreatedDate DESC

A dashboard against this query shows the chain's pulse. Good for dev tooling and operational monitoring.

Custom progress records. A persistent record per chain run tracks progress through stages. Fields like RecordsProcessed__c, RecordsFailed__c, LastStage__c. Updated by each Queueable. Queryable by operations dashboards.

Without one or both of these, async chain debugging becomes a black box. A failed nightly job produces no signal until users notice missing data.

Chain depth limits

Queueable chains have practical depth limits:

  • The recursion depth limit (the depth at which a chained Queueable cannot enqueue another) varies by org but is typically around 5 in synchronous test contexts and effectively unlimited in async contexts.
  • The per-org concurrent async job limit (50 by default, higher with adjustments).
  • The 24-hour async job queue limit (typically 250K, varies by edition).

For workflows that need to process millions of records across many stages, Batch Apex is usually the better fit. Queueable chaining works for tens of thousands of records across five to ten stages. Above that, hybrid patterns (Batch that triggers Queueable, or Queueable that triggers Batch) become useful.

Testing chains

Test classes can enqueue Queueables but the chain does not naturally propagate during the test. The pattern that works:

@isTest
static void testStageOneEnqueuesStageTwo() {
  Test.startTest();
  System.enqueueJob(new StageOneQueueable(testIds));
  Test.stopTest(); // Stage 1 executes here, enqueues Stage 2.

  // After Test.stopTest, only Stage 1 has run.
  // Stage 2 is enqueued but not executed.
  // Verify Stage 1's behavior.

  // Mock or simulate Stage 2 separately.
}

Each stage gets its own test. The chain transition (Stage N enqueuing Stage N+1) is verified by checking that the AsyncApexJob queue has the expected next job. The next stage's behavior is tested in isolation.

Common chaining mistakes

Five patterns Sapota has seen in audits:

  • State lost between stages. Stage 1 computes results, stage 2 re-computes them because the chain forgot to pass them. Wasted work or, worse, inconsistent results.
  • No retry logic. A transient failure in stage 3 stops the chain. The remaining records sit in limbo. Operations notices days later.
  • Chain depth abuse. A chain that runs 20 stages because each stage is a tiny operation. The framework overhead dominates. Combine logically related steps into fewer, larger stages.
  • Sync code mixed in. A "Queueable" that also does synchronous DML or callouts before enqueuing the next stage. Hits transaction limits.
  • No observability. Operations cannot see the chain's progress. Debugging requires reading raw AsyncApexJob queries.

What good chaining looks like

A Salesforce org running healthy Queueable chains:

  • Each stage is a separate class with a clear single responsibility.
  • State passed via constructor for small data, via persisted records for large data.
  • Every chain has retry logic with bounded attempts.
  • Operations dashboard surfaces chain progress and recent failures.
  • Tests cover each stage independently and verify the next stage gets enqueued.

Sapota's Salesforce team holds the Platform Developer I credential and designs Queueable chains as deliberate workflows with explicit state and retry contracts on production engagements. The investment pays back the first time a chain runs reliably for a week without manual intervention.


Designing multi-stage async workflows in Salesforce Apex? Sapota's Salesforce team handles Queueable chain design, state management, and operational observability on production engagements. Get in touch ->

See our full platform services for the stack we cover.

About this post

Posts on the Sapota engineering blog are written by the engineer who shipped the pattern, not by a marketing copywriter. The byline above maps to a verifiable LinkedIn profile on the team page; click through and you will see the same author with a link back to recent posts. Posts that reference internal projects describe the relevant architecture and the gotchas in enough detail to be useful while keeping client-confidential specifics out of the public version.

If a pattern in this post matches what your team is currently building, the engineers who wrote it are usually available to scope an engagement. Sapota's engagement model starts with a two-week no-commitment trial scoped to a concrete deliverable, with named senior engineers from day one. Direct pricing starts at $1,800 per engineer per month, no agency markup, monthly rolling. Reach out via the contact page with a short description of the project and the rough timeline.

For more on the platforms Sapota ships on, see the services hub, which links to dedicated pages for Salesforce, Power Platform, Retool, Shopify, AI and machine learning, custom software, web app development, and mobile app development. For more posts in this topic area, browse the relevant category linked at the top of this page or use the blog index for a full chronological view.

Sapota is a senior-only engineering bench based in Đà Nẵng, Vietnam, working directly with global businesses and as the engineering tier behind 30 plus Vietnamese IT service firms. Direct pricing starts at $1,800 per engineer per month, with a two-week no-commitment trial that opens every engagement. No agency markup, no minimum commitment, no bait-and-switch between proposal and kickoff. The same engineers who run the trial continue the project if the fit is right. The about pagecovers the company-level context, the engagement model, and the operating principles that shape every project. The FAQ page answers the practical questions that come up in the first conversation with a prospective client.

One more note on how to use the patterns described above. Most posts intentionally stop at the pattern itself: the decision framework, the failure mode, or the configuration walkthrough. The post does not try to map onto a specific client engagement because the same pattern applies across very different project contexts. If you read a post and think "yes, this is what we just hit," that is the intended reaction; the next step is usually a quick scope conversation about whether Sapota can help, what team composition would make sense, and how the two-week trial would be structured around your specific deliverable. That conversation lives at the contact page and replies typically come within one business day.

Contact Us Now

Share Your Story

We build trust by delivering what we promise – the first time and every time!

We'd love to hear your vision. Our IT experts will reach out to you during business hours to discuss making it happen.

WHY CHOOSE US

"Collaborate, Elevate, Celebrate where Associates - Create Project Excellence"

SapotaCorp beyond the IT industry standard, we are

  • Certificated
  • Assured quality
  • Extra maintenance

Tell us about your project