From fe6ab0f3495e046d049456b050451f743cf1d7f2 Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sun, 21 Jun 2026 09:51:49 +0000 Subject: [PATCH] docs: add search, standard-vs-async guide, API class examples Address issue #31: - enable VitePress local search - add Standard Apex vs Async Lib parity page (queueable, batchable, schedulable), covering Database.Stateful, QueryLocator and where job logic lives - open each API page with a full job-class example - cross-link the new page from getting-started and the home hero Closes #31 --- website/.vitepress/config.mts | 8 +- website/api/batchable.md | 35 ++- website/api/queueable.md | 57 ++-- website/api/schedulable.md | 21 +- website/getting-started.md | 8 +- website/index.md | 2 +- .../standard-apex-vs-async-lib.md | 297 ++++++++++++++++++ 7 files changed, 399 insertions(+), 29 deletions(-) create mode 100644 website/introduction/standard-apex-vs-async-lib.md diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index 668414c..f99eb4b 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -27,6 +27,9 @@ export default defineConfig({ }, themeConfig: { logo: '/logo.png', + search: { + provider: 'local' + }, // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Home', link: '/' }, @@ -39,6 +42,10 @@ export default defineConfig({ collapsed: false, items: [ { text: 'Getting Started', link: '/getting-started' }, + { + text: 'Standard Apex vs Async Lib', + link: '/introduction/standard-apex-vs-async-lib' + }, { text: 'Installation', link: '/introduction/installation' } ] }, @@ -69,7 +76,6 @@ export default defineConfig({ ] } ], - footer: false, socialLinks: [ { icon: 'github', diff --git a/website/api/batchable.md b/website/api/batchable.md index 9e0051e..9fa43f4 100644 --- a/website/api/batchable.md +++ b/website/api/batchable.md @@ -2,16 +2,43 @@ Apex classes `BatchableBuilder.cls` and `BatchableManager.cls`. -**Common Batchable example:** +New to async jobs? See +[Standard Apex vs Async Lib](/introduction/standard-apex-vs-async-lib#batchable) +for how this maps to a plain `Database.executeBatch`. + +**Common BatchJob class example:** + +Your batch class is a normal `Database.Batchable`. Async Lib does not change it, +so `Database.Stateful`, `Database.QueryLocator`, and the `start` / `execute` / +`finish` methods all work as usual. ```apex -Database.Batchable job = new MyBatchJob(); +public class AccountCleanupBatch implements Database.Batchable, Database.Stateful { + public Integer deletedCount = 0; + + public Database.QueryLocator start(Database.BatchableContext bc) { + return Database.getQueryLocator('SELECT Id FROM Account WHERE Is_Active__c = false'); + } + + public void execute(Database.BatchableContext bc, List scope) { + delete scope; + deletedCount += scope.size(); + } + + public void finish(Database.BatchableContext bc) { + System.debug('Deleted ' + deletedCount + ' accounts'); + } +} +``` -Async.AsyncResult result = Async.batchable(job) +**Common Batchable example:** + +```apex +Async.Result result = Async.batchable(new AccountCleanupBatch()) .scopeSize(100) .execute(); -System.debug('Batch job enqueued: ' + result); +System.debug('Batch job enqueued: ' + result.salesforceJobId); ``` ## Methods diff --git a/website/api/queueable.md b/website/api/queueable.md index 90fab9e..8e7a1d5 100644 --- a/website/api/queueable.md +++ b/website/api/queueable.md @@ -3,40 +3,61 @@ Apex classes `QueueableBuilder.cls`, `QueueableManager.cls`, and `QueueableJob.cls`. -For testing patterns and best practices, see -[Testing Async Jobs](/explanations/testing-async-jobs). +New to async jobs? See +[Standard Apex vs Async Lib](/introduction/standard-apex-vs-async-lib#queueable) +for how this maps to a plain `Queueable`. For testing patterns and best +practices, see [Testing Async Jobs](/explanations/testing-async-jobs). + +**Common QueueableJob class example:** + +Extend `QueueableJob` and put your logic in `work()` instead of implementing +`Queueable.execute()`. + +```apex +public class AccountProcessorJob extends QueueableJob { + private List accountIds; + + public AccountProcessorJob(List accountIds) { + this.accountIds = accountIds; + } + + public override void work() { + List accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds]; + for (Account acc : accounts) { + acc.Description = 'Processed'; + } + update accounts; + } +} +``` **Common Queueable example:** ```apex -QueueableJob job = new MyQueueableJob(); -Async.Result result = Async.queueable(job) +Async.Result result = Async.queueable(new AccountProcessorJob(accountIds)) .priority(5) .delay(2) .continueOnJobExecuteFail() .enqueue(); ``` -Returns `result.customJobId` containing MyQueueableJob's unique Custom Job Id. - -**Common QueueableJob class example:** - -```apex -public class AccountProcessorJob extends QueueableJob { - public override void work() { - // Get job context - Async.QueueableJobContext ctx = Async.getQueueableJobContext(); - } -} -``` +Returns `result.customJobId` containing AccountProcessorJob's unique Custom Job +Id. **Common Finalizer class example:** +A finalizer runs after the job completes, whether it succeeded or threw. Extend +`QueueableJob.Finalizer` and read the outcome from the `FinalizerContext`. + ```apex -private class ProcessorFinalizer extends QueueableJob.Finalizer { +public class ProcessorFinalizer extends QueueableJob.Finalizer { public override void work() { - // Get finalizer context FinalizerContext finalizerCtx = Async.getQueueableJobContext().finalizerCtx; + if (finalizerCtx.getResult() == ParentJobResult.SUCCESS) { + System.debug('Job succeeded'); + } else { + System.debug('Job failed: ' + finalizerCtx.getException().getMessage()); + } } } ``` diff --git a/website/api/schedulable.md b/website/api/schedulable.md index f3392a0..bca9caa 100644 --- a/website/api/schedulable.md +++ b/website/api/schedulable.md @@ -2,11 +2,28 @@ Apex classes `SchedulableBuilder.cls`, `SchedulableManager.cls`, and `CronBuilder.cls`. +New to async jobs? See +[Standard Apex vs Async Lib](/introduction/standard-apex-vs-async-lib#schedulable) +for how this maps to a plain `System.schedule`. + +**Common SchedulableJob class example:** + +Your class is a normal `Schedulable`. Async Lib only replaces the +`System.schedule(...)` call, building the cron expression and skipping the job +when one with the same name is already scheduled. + +```apex +public class AccountReportJob implements Schedulable { + public void execute(SchedulableContext context) { + // ... work ... + } +} +``` + **Common Schedulable example:** ```apex -Schedulable job = new MySchedulableJob(); -List results = Async.schedulable(job) +List results = Async.schedulable(new AccountReportJob()) .name('Daily Processing Job') .cronExpression('0 0 2 * * ? *') .skipWhenAlreadyScheduled() diff --git a/website/getting-started.md b/website/getting-started.md index 9222047..b598931 100644 --- a/website/getting-started.md +++ b/website/getting-started.md @@ -257,16 +257,18 @@ why. Query by **ChainId\_\_c** to see a whole chain run as a timeline. Now that you understand the basics: -1. **Explore the API** - Learn about all available methods and options: +1. **[Standard Apex vs Async Lib](/introduction/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](/api/queueable.md)** - Detailed information on using Queueable jobs 2. **[Batchable API](/api/batchable.md)** - Detailed information on using Batchable jobs 3. **[Schedulable API](/api/schedulable.md)** - Detailed information on using Schedulable jobs -2. **Read the Blog Post** - Check out the detailed explanation: +3. **Read the Blog Post** - Check out the detailed explanation: [Apex Queueable Processing Framework](https://blog.beyondthecloud.dev/blog/apex-queueable-processing-framework) -3. **[Initial Queueable Chain Schedulable Explanation](/explanations/initial-scheduled-queuable-batch-job.md)** - +4. **[Initial Queueable Chain Schedulable Explanation](/explanations/initial-scheduled-queuable-batch-job.md)** - Learn why this job is important for framework to function properly. ## Quick Tips diff --git a/website/index.md b/website/index.md index 503b1ae..86a9d77 100644 --- a/website/index.md +++ b/website/index.md @@ -12,7 +12,7 @@ hero: link: /getting-started - theme: alt text: View Examples - link: /getting-started + link: /introduction/standard-apex-vs-async-lib features: - title: Smart Queueable Jobs diff --git a/website/introduction/standard-apex-vs-async-lib.md b/website/introduction/standard-apex-vs-async-lib.md new file mode 100644 index 0000000..5e5e5df --- /dev/null +++ b/website/introduction/standard-apex-vs-async-lib.md @@ -0,0 +1,297 @@ +--- +outline: deep +--- + +# Standard Apex vs Async Lib + +If you already know how to write a `Queueable`, a `Database.Batchable`, or a +`Schedulable` in plain Apex, this page maps each of those to its Async Lib +equivalent. Same concepts, less boilerplate, no "Too many queueable jobs" +errors. + +Async Lib only wraps the **enqueue, chain, and schedule** parts. Your business +logic stays where it always was, so things like `Database.Stateful`, +`QueryLocator`, and finalizer context work exactly the same. + +## At a glance + +| Standard Apex | Async Lib | +| ---------------------------------------------- | --------------------------------------------------------- | +| `implements Queueable` + `execute(context)` | `extends QueueableJob` + `work()` | +| `System.enqueueJob(job)` | `Async.queueable(job).enqueue()` | +| Enqueue next job inside `execute()` | `.chain(new NextJob())` | +| 1 child queueable per transaction (hard limit) | Automatic overflow to a scheduled batch, no limit | +| `System.attachFinalizer` + `implements Finalizer` | `extends QueueableJob.Finalizer` + `attachFinalizer()` | +| `Database.executeBatch(job, scope)` | `Async.batchable(job).scopeSize(scope).execute()` | +| `implements Schedulable` + `System.schedule` | `Async.schedulable(job).cronExpression(...).schedule()` | +| Hand-written cron string | `CronBuilder` fluent helpers | + +## Queueable + +### Defining a job + +In standard Apex you `implements Queueable` and put your logic in +`execute(QueueableContext)`. With Async Lib you `extends QueueableJob` and +override `work()`. The Salesforce `QueueableContext` is still available through +the job context. + +**Standard Apex** + +```apex +public class AccountProcessorJob implements Queueable { + private List accountIds; + + public AccountProcessorJob(List accountIds) { + this.accountIds = accountIds; + } + + public void execute(QueueableContext context) { + List accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds]; + // ... process accounts ... + update accounts; + } +} +``` + +**Async Lib** + +```apex +public class AccountProcessorJob extends QueueableJob { + private List accountIds; + + public AccountProcessorJob(List accountIds) { + this.accountIds = accountIds; + } + + public override void work() { + QueueableContext context = Async.getQueueableJobContext().queueableCtx; + List accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds]; + // ... process accounts ... + update accounts; + } +} +``` + +### Enqueuing + +**Standard Apex** + +```apex +System.enqueueJob(new AccountProcessorJob(accountIds)); +``` + +**Async Lib** + +```apex +Async.queueable(new AccountProcessorJob(accountIds)) + .priority(5) + .enqueue(); +``` + +The builder adds options you'd otherwise hand-roll: `priority`, `delay`, +`retry`, rollback/continue-on-failure, and more. See the +[Queueable API](/api/queueable) for the full list. + +### Chaining jobs + +Standard Apex lets you enqueue **one** child queueable from inside a running +queueable. Go past that and you hit `System.AsyncException: Too many queueable +jobs added to the queue: 2`. Async Lib chains as many jobs as you want and +automatically overflows to a scheduled batch when the platform limit is reached. + +**Standard Apex** + +```apex +public class FirstJob implements Queueable { + public void execute(QueueableContext context) { + // ... work ... + System.enqueueJob(new SecondJob()); // only one allowed per transaction + } +} +``` + +**Async Lib** + +```apex +Async.queueable(new FirstJob()) + .chain(new SecondJob()) + .chain(new ThirdJob()) + .enqueue(); +``` + +Async Lib also adds [`dependsOn(...)`](/api/queueable#dependson) so a chained job +can run only when an earlier one succeeded, failed, or finished. There is no +standard-Apex equivalent. + +### Finalizers + +A finalizer runs after the job completes, whether it succeeded or threw. The +shape is the same in both worlds; Async Lib just attaches it through the builder +and exposes the `FinalizerContext` through the job context. + +**Standard Apex** + +```apex +public class CleanupFinalizer implements Finalizer { + public void execute(FinalizerContext context) { + if (context.getResult() == ParentJobResult.SUCCESS) { + System.debug('Job succeeded'); + } else { + System.debug('Job failed: ' + context.getException().getMessage()); + } + } +} + +public class MainJob implements Queueable { + public void execute(QueueableContext context) { + System.attachFinalizer(new CleanupFinalizer()); + // ... work ... + } +} +``` + +**Async Lib** + +```apex +public class CleanupFinalizer extends QueueableJob.Finalizer { + public override void work() { + FinalizerContext context = Async.getQueueableJobContext().finalizerCtx; + if (context.getResult() == ParentJobResult.SUCCESS) { + System.debug('Job succeeded'); + } else { + System.debug('Job failed: ' + context.getException().getMessage()); + } + } +} + +public class MainJob extends QueueableJob { + public override void work() { + Async.queueable(new CleanupFinalizer()).attachFinalizer(); + // ... work ... + } +} +``` + +## Batchable + +Your batch class does **not** change. It is a normal +`Database.Batchable` with `start()`, `execute()`, and `finish()`. Async +Lib only replaces the `Database.executeBatch(...)` call, adding scheduling and +result tracking on top. + +**Standard Apex** + +```apex +Database.executeBatch(new AccountCleanupBatch(), 200); +``` + +**Async Lib** + +```apex +Async.batchable(new AccountCleanupBatch()) + .scopeSize(200) + .execute(); +``` + +### Does `Database.Stateful` work the same? + +Yes. State is kept on **your** batch class, which Async Lib never touches. +Implement `Database.Stateful` exactly as you do today. + +```apex +public class AccountCleanupBatch implements Database.Batchable, Database.Stateful { + public Integer deletedCount = 0; + + public Database.QueryLocator start(Database.BatchableContext bc) { + return Database.getQueryLocator('SELECT Id FROM Account WHERE Is_Active__c = false'); + } + + public void execute(Database.BatchableContext bc, List scope) { + delete scope; + deletedCount += scope.size(); // preserved across batches by Database.Stateful + } + + public void finish(Database.BatchableContext bc) { + System.debug('Deleted ' + deletedCount + ' accounts'); + } +} +``` + +### How do I use `QueryLocator`? + +The same way. Return it from `start()` as usual (see the example above). Async +Lib hands your job straight to `Database.executeBatch`, so the query locator, +chunking, and 50-million-row limit all behave identically. + +### Should I move my logic into `work()`? + +No. `work()` belongs to **queueable** jobs (`QueueableJob`). A batch keeps its +`start()` / `execute()` / `finish()` methods. The only thing that moves is how +you kick it off: `Async.batchable(job).execute()` instead of +`Database.executeBatch(job)`. + +## Schedulable + +Your `Schedulable` class is unchanged. Async Lib wraps `System.schedule`, builds +the cron expression for you, and can skip scheduling when a job of the same name +already exists (so you don't have to catch the "already scheduled" exception). + +**Standard Apex** + +```apex +public class NightlyJob implements Schedulable { + public void execute(SchedulableContext context) { + // ... work ... + } +} + +// daily at 02:00 — cron string written by hand +System.schedule('Nightly Job', '0 0 2 * * ? *', new NightlyJob()); +``` + +**Async Lib** + +```apex +Async.schedulable(new NightlyJob()) + .name('Nightly Job') + .cronExpression('0 0 2 * * ? *') + .skipWhenAlreadyScheduled() + .schedule(); +``` + +### Building the cron expression + +Instead of remembering cron field order, use `CronBuilder`. + +**Standard Apex** + +```apex +// every day at 02:00 +System.schedule('Nightly Job', '0 0 2 * * ? *', new NightlyJob()); +``` + +**Async Lib** + +```apex +Async.schedulable(new NightlyJob()) + .name('Nightly Job') + .cronExpression(new CronBuilder().everyDay(2, 0)) + .schedule(); +``` + +See the [Schedulable API](/api/schedulable#build---cron-expression) for the full +set of `CronBuilder` helpers (`everyHour`, `everyXHours`, `everyMonth`, and so +on). + +### Scheduling a queueable or batch + +Standard Apex has no direct way to schedule a queueable. With Async Lib, any +queueable or batch builder converts to a schedulable with `asSchedulable()`. + +```apex +Async.queueable(new AccountProcessorJob(accountIds)) + .asSchedulable() + .name('Hourly Account Processing') + .cronExpression(new CronBuilder().everyHour(0)) + .schedule(); +```