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 Context | Queueable | Future | Batch | Schedule |
|---|---|---|---|---|
| Synchronous or Scheduled* process | 50 | 50 | 100 in Holding status 5 in Queued or Active status | 100 |
| Queueable job | 1 | 50 | ||
| @future method call | 1 | 0 | ||
| Batch job | 1 | 0 | As 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:
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:
// 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 โ
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 โ
// 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.
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.
// 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:
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:
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:
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):
- Go to Setup โ Custom Metadata Types โ QueueableJobSetting โ Manage Records
- 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__crecords 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__cisFAILED - 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:
- Standard Apex vs Async Lib - See each plain-Apex async pattern next to its Async Lib equivalent.
- Explore the API - Learn about all available methods and options:
- Queueable API - Detailed information on using Queueable jobs
- Chunk API - Paging a large data set across chained Queueables
- Batchable API - Detailed information on using Batchable jobs
- Schedulable API - Detailed information on using Schedulable jobs
- Read the Blog Post - Check out the detailed explanation: Apex Queueable Processing Framework
- 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.AllowsCalloutsto any job
