Queueable Apex
🌀 Queueable Apex in Salesforce
Queueable Apex is an asynchronous execution method in Salesforce that enables you to run long-running operations—such as external web service callouts or heavy database logic—outside the standard synchronous request. It’s an enhancement over the @future method, offering more control and flexibility.
Jobs submitted via the System.enqueueJob() method are added to the Apex Job Queue and return a job ID, which allows you to monitor job progress using the Apex Jobs page in the UI or by querying the AsyncApexJob object.
📘 Key Concepts
| Concept | Description |
| 🧠 Interface | Classes must implement the Queueable interface |
| 🛠 Method | Requires an execute(QueueableContext context) method |
| 🔄 Execution | Jobs are queued using System.enqueueJob() |
| 🆔 Monitoring | The method returns a Job ID used to track execution status |
💡 Basic Syntax
public class MyQueueableClass implements Queueable {
public void execute(QueueableContext context) {
System.debug('Queueable Job ID: ' + context.getJobId());
}
}
🚀 How to Enqueue a Queueable Job
You can enqueue a job using either of these approaches:
System.enqueueJob(new MyQueueableClass());
Or store the Job ID for tracking:
Id jobId = System.enqueueJob(new MyQueueableClass());
System.debug('Job ID: ' + jobId);
✅ Why Use Queueable Apex Instead of @future?
| Feature | @future | Queueable |
| ✅ Supports Complex Data Types | ❌ No | ✅ Yes |
| 🔁 Job Chaining | ❌ Not Possible | ✅ Supported |
| 🆔 Track Job ID | ❌ No | ✅ Yes |
| 🧠 Better Code Flexibility | ❌ Limited | ✅ High |
| 📦 Ideal for Large/Chained Jobs | ❌ Not ideal | ✅ Recommended |
🛑 Limitations of Queueable Apex
| Limitation | Description |
| ⛓ Chaining Limit | Maximum of 50 jobs can be chained in a single transaction |
| 🔄 No Nested Queueables | Cannot enqueue another Queueable job after reaching chaining limit |
| 📊 Governor Limits Apply | Still subject to asynchronous governor limits (CPU time, SOQL, etc.) |
| ⚠️ From Triggers | Use with caution inside triggers with bulk DML to avoid hitting limits |
🧪 Example: Queueable Apex with sObject
public class AccountUpdater implements Queueable {
private Account acc;
public AccountUpdater(Account acc) {
this.acc = acc;
}
public void execute(QueueableContext context) {
acc.Name += ' - Updated';
update acc;
}
}
// Example Usage
Account a = [SELECT Id, Name FROM Account LIMIT 1];
System.enqueueJob(new AccountUpdater(a));
🆔 How to Retrieve Queueable Job ID
You can get the Job ID in two ways:
- Inside the
execute()method usingQueueableContext:
public void execute(QueueableContext context) {
System.debug('Job ID: ' + context.getJobId()); // 18-character ID
}
- When enqueuing the job using
System.enqueueJob():
Id jobId = System.enqueueJob(new MyQueueableClass()); // 15-character ID
📌 Note: The only difference is the length of the returned Job ID (15 vs. 18 characters).
⚙️ Queueable Apex – Real-world Example, Testing, and Chaining
This guide covers a practical example of a Queueable Apex class in Salesforce, including a test class and chaining of multiple Queueable jobs. It also highlights key limitations and best practices to follow while working with asynchronous Apex logic.
🧩 Example: Queueable Apex Class
The following Queueable class searches for an Opportunity named "Test Account Prospect", creates a new parent Account, and links the Opportunity to it.
public class QueueableClassForCreation implements Queueable {
public void execute(QueueableContext qc) {
Opportunity opp = [SELECT Id, AccountId FROM Opportunity WHERE Name = 'Test Account Prospect' LIMIT 1];
Account acc = new Account();
acc.Name = 'Async Apex Account';
insert acc;
opp.AccountId = acc.Id;
update opp;
}
}
🧪 Test Class for Queueable Apex
When writing tests for Queueable classes:
Always wrap asynchronous calls in
Test.startTest()andTest.stopTest().For chained Queueables, use
if (!Test.isRunningTest)to prevent recursive enqueuing during test execution.
✅ Test Class Example
@isTest
public class QueueableClassForCreation_Test {
@testSetup
public static void setupTestData() {
Opportunity opp = new Opportunity();
opp.Name = 'Test Account Prospect';
opp.CloseDate = System.today();
opp.StageName = 'Closed Won';
insert opp;
}
@isTest
public static void testQueueableExecution() {
Test.startTest();
System.enqueueJob(new QueueableClassForCreation());
Test.stopTest();
}
}
🔗 Chaining Queueable Jobs
Chaining is the practice of enqueuing one Queueable class from another. This is helpful when you want sequential processing across multiple asynchronous jobs.
🔄 How Many Can You Chain?
✅ Maximum 5 total jobs: 1 initial + 4 chained
❌ Only 1 job can be enqueued from another Queueable
📛 Errors:
Too many enqueued:
System.LimitException: Too many queueable jobs added to the queue: 2Exceeded stack depth:
System.AsyncException: Maximum stack depth has been reached
🧱 Chaining Example: 5 Queueable Classes
1️⃣ QueueableClass1
public class QueueableClass1 implements Queueable {
public void execute(QueueableContext qc) {
System.debug('Before calling Queueable 2');
if (!Test.isRunningTest()) System.enqueueJob(new QueueableClass2());
System.debug('After calling Queueable 2');
}
}
2️⃣ QueueableClass2
public class QueueableClass2 implements Queueable {
public void execute(QueueableContext qc) {
System.debug('Before calling Queueable 3');
if (!Test.isRunningTest()) System.enqueueJob(new QueueableClass3());
System.debug('After calling Queueable 3');
}
}
3️⃣ QueueableClass3
public class QueueableClass3 implements Queueable {
public void execute(QueueableContext qc) {
System.debug('Before calling Queueable 4');
if (!Test.isRunningTest()) System.enqueueJob(new QueueableClass4());
System.debug('After calling Queueable 4');
}
}
4️⃣ QueueableClass4
public class QueueableClass4 implements Queueable {
public void execute(QueueableContext qc) {
System.debug('Before calling Queueable 5');
if (!Test.isRunningTest()) System.enqueueJob(new QueueableClass5());
System.debug('After calling Queueable 5');
}
}
5️⃣ QueueableClass5
public class QueueableClass5 implements Queueable {
public void execute(QueueableContext qc) {
System.debug('Inside 5th Queueable');
}
}
🛡️ Best Practices for Chaining Queueables
| Best Practice | Reason |
✅ Use !Test.isRunningTest | Prevents infinite loops or async call issues during test execution |
| ✅ Check chaining limit (max 5) | Avoids AsyncException for exceeding stack depth |
| ❌ Don’t enqueue multiple Queueables at once | Only one System.enqueueJob() allowed per execute() context |
✅ Use Test.startTest()/stopTest() | Ensures async execution is triggered during unit testing |
🧪 Testing Chained Queueable Jobs in Salesforce
Queueable job chaining allows sequential execution of asynchronous jobs. However, testing them requires special care—especially when one job enqueues another.
🧩 Problem: Test Class Fails for Chained Jobs
@isTest
public class QueueableClass1_Test {
@isTest
public static void QueueableClass1() {
Test.startTest();
System.enqueueJob(new QueueableClass1());
Test.stopTest(); // ⚠️ Test may fail if QueueableClass1 enqueues another job
}
}
🔥 Why it fails?
If QueueableClass1 tries to enqueue another job like QueueableClass2, the test context will throw an error such as:
System.AsyncException: Maximum stack depth has been reached
✅ Solution: Use !Test.isRunningTest()
To prevent enqueueing during tests:
public class QueueableClass1 implements Queueable {
public void execute(QueueableContext qc) {
System.debug('Inside Queueable 1');
if (!Test.isRunningTest()) {
System.enqueueJob(new QueueableClass2());
}
}
}
📊 Using Limits.getQueueableJobs()
This method returns the number of Queueable jobs enqueued in the current transaction. Useful for tracking and controlling logic dynamically.
public class SynchronousApexClass {
public static void callQueueable() {
System.enqueueJob(new QueueableClass1());
System.enqueueJob(new QueueableClass2());
System.debug('Total queueable called are: ' + Limits.getQueueableJobs());
}
}
🔍 Using System.isQueueable()
This method checks whether the current context is inside a Queueable job.
Example:
public class QueueableClass1 implements Queueable {
public void execute(QueueableContext qc) {
SynchronousApexClass.callQueueable();
}
}
public class SynchronousApexClass {
public static void callQueueable() {
if (System.isQueueable()) {
System.debug('Inside Queueable context');
} else {
System.debug('Outside Queueable context');
}
}
}
🧪 Key Tips for Queueable Testing
| Tip | Purpose |
✅ Use Test.startTest() and Test.stopTest() | Ensures async job runs during test |
✅ Guard with !Test.isRunningTest() | Prevents infinite chaining or async limits during test |
✅ Use Limits.getQueueableJobs() | Monitor how many jobs are enqueued in the same transaction |
🔍 Use System.isQueueable() | Control behavior based on async vs sync execution context |
🔁 Calling Queueable Apex from a Trigger
Scenario: On Account insert, use a Queueable class to create a child Contact asynchronously.
📦 Trigger + Queueable Design
🧩 AccountTrigger.apxt
trigger AccountTrigger on Account (after insert) {
if (Trigger.isAfter && Trigger.isInsert) {
AccountTriggerHandler.createChildContact(Trigger.new);
}
}
🧠 AccountTriggerHandler.apxc
public class AccountTriggerHandler {
public static void createChildContact(List<Account> accNewList) {
System.enqueueJob(new AccountTriggerQueueable(accNewList));
}
}
🚀 AccountTriggerQueueable.apxc
public class AccountTriggerQueueable implements Queueable {
public List<Account> accNewList;
public AccountTriggerQueueable(List<Account> accNewList) {
this.accNewList = accNewList;
}
public void execute(QueueableContext qc) {
List<Contact> conToCreate = new List<Contact>();
for (Account acc : accNewList) {
Contact con = new Contact();
con.LastName = 'New Con Created';
con.AccountId = acc.Id;
conToCreate.add(con);
}
if (!conToCreate.isEmpty()) {
insert conToCreate;
}
}
}
💡 Why use Queueable here?
Offloads post-insert processing from the trigger, helping with governor limits, bulk processing, and scalability.
🌐 Callouts with Queueable Apex
To perform a HTTP callout, implement both Queueable and Database.AllowsCallouts.
🛠️ CalloutQueueable.apxc
public class CalloutQueueable implements Queueable, Database.AllowsCallouts {
public void execute(QueueableContext qc) {
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint('https://gorest.co.in/public/v2/posts');
req.setMethod('GET');
HttpResponse res = h.send(req);
System.debug('Status Code: ' + res.getStatusCode());
System.debug('Body: ' + res.getBody());
}
}
⚠️ Don’t forget to whitelist the endpoint in Remote Site Settings!
🧪 Test Class for Queueable Callout
🧱 CalloutMock.apxc
@isTest
public class CalloutMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('Test JSON Body');
return res;
}
}
🚦 CalloutQueueable_Test.apxc
@isTest
public class CalloutQueueable_Test {
@isTest
public static void calloutQueueable() {
Test.startTest();
Test.setMock(HttpCalloutMock.class, new CalloutMock());
System.enqueueJob(new CalloutQueueable());
Test.stopTest();
}
}
📏 Limitations of Queueable Apex
| ⚠️ Limitation | 🚫 Description |
| 🔢 Max Jobs per Transaction | Up to 50 Queueable jobs per transaction (System.enqueueJob) |
| 🔗 Max Chaining Depth | Only 5 levels allowed (1 parent + 4 chained child jobs) |
| 🧬 Single Child from Parent | Only 1 child Queueable can be enqueued from within another Queueable |
| 🔁 No Nested Queueables | Enqueuing inside another after limit reached results in AsyncException |

