Skip to content

Getting Started โ€‹

Async Lib is a powerful Salesforce Apex framework that provides an elegant solution for managing asynchronous processes. It eliminates common limitations like "Too many queueable jobs" errors and offers a unified API for queueable, batchable, and schedulable jobs.

Why Async Lib? โ€‹

Salesforce Limits โ€‹

Apex ContextQueueableFutureBatchSchedule
Synchronous or Scheduled* process5050100 in Holding status
5 in Queued or Active status
100
Queueable job150
@future method call10
Batch job10As above in finish() batch method. For start() and execute() methods, the limit is 0.

Key Benefits โ€‹

  • ๐Ÿš€ Eliminates Queueable Limits: Automatically handles "Too many queueable jobs" by intelligent chaining and batch overflow
  • ๐ŸŽฏ Unified API: Single, consistent interface for all async job types (Queueable, Batchable, Schedulable)
  • โšก Smart Prioritization: Jobs execute based on priority with automatic sorting
  • ๐Ÿ›ก๏ธ Advanced Error Handling: Built-in error recovery, rollback options, and continuation strategies
  • ๐Ÿ“Š Job Tracking: Comprehensive tracking with custom job IDs and result records
  • โš™๏ธ Configuration-Driven: Control job behavior through custom metadata without code changes
  • ๐Ÿ”— Support Finalizers: Execute cleanup logic after job completion with full context

Core Concepts โ€‹

1. QueueableJob Base Class โ€‹

All queueable jobs extend the QueueableJob abstract class:

apex
public class MyQueueableJob extends QueueableJob {
  public override void work() {
    // Your business logic here
    System.debug(
      'Processing job: ' + Async.getQueueableJobContext().currentJob.customJobId
    );
  }
}

2. Builder Pattern API โ€‹

All job types use a fluent builder pattern:

apex
// Queueable Job
Async.queueable(new MyQueueableJob())
    .priority(10)
    .delay(5)
    .enqueue();

// Batch Job
Async.batchable(new MyBatchJob())
    .scopeSize(100)
    .execute();

// Schedulable Job
Async.schedulable(new MySchedulableJob())
    .name('Daily Cleanup')
    .cronExpression('0 0 2 * * ? *')
    .skipWhenAlreadyScheduled()
    .schedule();

3. Automatic Job Chaining โ€‹

When queueable limits are reached, Async Lib automatically switches to scheduled-batch-based execution, ensuring your jobs always run without hitting Queueable platform limits.

Your First Queueable Job โ€‹

Let's create a simple job that processes accounts:

Step 1: Create Your Job Class โ€‹

apex
public class AccountProcessorJob extends QueueableJob {
  private List<Id> accountIds;

  public AccountProcessorJob(List<Id> accountIds) {
    this.accountIds = accountIds;
  }

  public override void work() {
    // Get job context
    Async.QueueableJobContext ctx = Async.getQueueableJobContext();
    System.debug('Processing job: ' + ctx.currentJob.customJobId);

    // Process accounts
    List<Account> accounts = [
      SELECT Id, Name
      FROM Account
      WHERE Id IN :accountIds
    ];
    for (Account acc : accounts) {
      acc.Description = 'Processed by ' + ctx.currentJob.className;
    }
    update accounts;

    System.debug('Processed ' + accounts.size() + ' accounts');
  }
}

Step 2: Enqueue the Job โ€‹

apex
// Get some account IDs
List<Id> accountIds = new List<Id>{
    '0013000000abcdef',
    '0013000000ghijkl'
};

// Enqueue the job
Async.Result result = Async.queueable(new AccountProcessorJob(accountIds))
    .priority(5)
    .enqueue();

System.debug('Job enqueued with ID: ' + result.customJobId);

Processing a Large Data Set โ€‹

One job per record blows the limits, and a Database.Batchable is a separate lifecycle with none of the chain tracking. Async.chunk(...) sits in between: it pages through a source and runs one job per page, with the same retry, backoff and AsyncResult__c tracking as any other job.

apex
public class AccountRecalcJob extends ChunkJob implements Async.ChunkResettable, Async.Retryable {
    public override void work(List<SObject> chunk) {
        List<Account> accounts = (List<Account>) chunk;
        for (Account acc : accounts) {
            acc.Description = 'Recalculated';
        }
        update accounts;
    }

    // This job keeps nothing between pages or attempts, and says so.
    public void resetBeforeNextChunk(Integer pageNumber) {
    }

    public void resetBeforeRetry(Integer attempt) {
    }
}

The same job object runs once per page, and again on every retry, so Async Lib makes you state what happens to its state. Empty bodies are a valid answer when there is nothing to clear. See Job State Between Runs.

apex
// In-memory records or ids
Async.chunk(new AccountRecalcJob(), ChunkSource.of(accounts))
    .chunkSize(200)
    .retry(2)
    .enqueue();

// Too large to hold in memory? Page a SOQL cursor instead
Async.chunk(new AccountRecalcJob(), ChunkSource.query('SELECT Id FROM Account'))
    .chunkSize(200)
    .enqueue();

The run takes one slot in the chain, so everything chained after it waits for the last page:

apex
Async.queueable(new PrepareJob())
    .chunk(new AccountRecalcJob(), ChunkSource.of(accounts))
    .chunkSize(200)
    .chain(new NotifyJob())
    .enqueue();

See the Chunk API for ordering, priority, failure handling and the cursor limits.

Error Handling โ€‹

Async Lib provides sophisticated error handling options:

apex
Async.queueable(new MyJob())
    .continueOnJobEnqueueFail()    // Don't fail if enqueue fails
    .continueOnJobExecuteFail()    // Continue processing other jobs if this fails
    .rollbackOnJobExecuteFail()    // Rollback any DML if job fails
    .enqueue();

Using Finalizers โ€‹

Finalizers run after job completion and have access to success/failure context:

apex
public class MyJobFinalizer extends QueueableJob.Finalizer {
    public override void work() {
        Async.QueueableJobContext ctx = Async.getQueueableJobContext();
        FinalizerContext finalizerCtx = ctx.finalizerCtx;

        if (finalizerCtx.getResult() == ParentJobResult.SUCCESS) {
            System.debug('Job completed successfully!');
        } else {
            System.debug('Job failed: ' + finalizerCtx.getException().getMessage());
        }
    }
}

// Attach finalizer within a job
public class MyMainJob extends QueueableJob {
    public override void work() {
        // Do main work...

        // Attach finalizer
        Async.queueable(new MyJobFinalizer())
            .attachFinalizer();
    }
}

Configuration โ€‹

Control job behavior using Custom Metadata (QueueableJobSetting__mdt):

  1. Go to Setup โ†’ Custom Metadata Types โ†’ QueueableJobSetting โ†’ Manage Records
  2. Create or edit settings:
    • All: Global settings for all jobs
    • Specific Class Name: Settings for specific job classes

Available settings:

  • QueueableJobName__c: The name of the QueueableJob.
  • IsDisabled__c: Disable job execution
  • CreateResult__c: Create AsyncResult__c records for tracking

Async Result Records โ€‹

When enabled in QueueableJobSetting__mdt (CreateResult__c = true), Async Lib automatically creates an AsyncResult__c record for every job in a chain โ€” including jobs that were skipped โ€” so you can see exactly what happened, and why. Query by ChainId__c to see a whole chain run as a timeline.

Key Fields โ€‹

  • ChainId__c: Correlation id shared by every job in the same chain run
  • Status__c: Lifecycle outcome โ€” COMPLETED, FAILED, SKIPPED_DEPENDENCY, SKIPPED_CHAIN_STOPPED, SKIPPED_CHUNK_STOPPED, SKIPPED_EXPLICIT, SKIPPED_DISABLED, FRAMEWORK_ERROR
  • CustomJobId__c: Unique job ID generated by Async Lib
  • SalesforceJobId__c: Underlying Salesforce job ID (Queueable, Batch, or Scheduled)
  • ClassName__c: Apex class name of the job
  • Result__c: Platform job result for jobs that ran (SUCCESS, UNHANDLED_EXCEPTION)
  • ExceptionType__c / ExceptionMessage__c: Failure detail when Status__c is FAILED
  • SkipReason__c: Why a skipped job was skipped (e.g. "Dependency 'extract' required SUCCESS but was FAILURE")
  • DependsOnResult__c: Self-lookup to the result of the dependency that decided this job's fate, with RequiredOutcome__c / ActualOutcome__c
  • RetryAttempts__c: How many retries the job went through
  • RetryHistory__c: Per-attempt retry log

These records are never deleted automatically. See AsyncResult Cleanup for the scheduled cleanup batch with separate retention for failed results and the rest.

What's Next? โ€‹

Now that you understand the basics:

  1. Standard Apex vs Async Lib - See each plain-Apex async pattern next to its Async Lib equivalent.
  2. Explore the API - Learn about all available methods and options:
    1. Queueable API - Detailed information on using Queueable jobs
    2. Chunk API - Paging a large data set across chained Queueables
    3. Batchable API - Detailed information on using Batchable jobs
    4. Schedulable API - Detailed information on using Schedulable jobs
  3. Read the Blog Post - Check out the detailed explanation: Apex Queueable Processing Framework
  4. Initial Queueable Chain Schedulable Explanation - Learn why this job is important for framework to function properly.

Quick Tips โ€‹

  • Job Naming: Jobs get unique names with timestamps: MyJob::2024-01-15T10:30:45.123Z::1
  • Custom Job IDs: Every job gets a UUID for tracking independent of Salesforce Job IDs
  • Priority Matters: Lower numbers = higher priority. Finalizers always run first.
  • Test Friendly: Framework handles test context automatically
  • Callouts Supported: add implements Database.AllowsCallouts to any job