apex-batch-chaining
Use this skill when you need to run one Batch Apex job immediately after another completes — chaining via finish(), managing Flex Queue capacity, or choosing between batch-to-batch chaining and a Queueable bridge. NOT for async job technology selection — use the async-selection decision tree. NOT for single-job batch patterns, scope sizing, or Database.Stateful design — use batch-apex-patterns.
Best use case
apex-batch-chaining is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Use this skill when you need to run one Batch Apex job immediately after another completes — chaining via finish(), managing Flex Queue capacity, or choosing between batch-to-batch chaining and a Queueable bridge. NOT for async job technology selection — use the async-selection decision tree. NOT for single-job batch patterns, scope sizing, or Database.Stateful design — use batch-apex-patterns.
Teams using apex-batch-chaining should expect a more consistent output, faster repeated execution, less prompt rewriting, better workflow continuity with your supporting tools.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
- You already have the supporting tools or dependencies needed by this skill.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/apex-batch-chaining/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How apex-batch-chaining Compares
| Feature / Agent | apex-batch-chaining | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Use this skill when you need to run one Batch Apex job immediately after another completes — chaining via finish(), managing Flex Queue capacity, or choosing between batch-to-batch chaining and a Queueable bridge. NOT for async job technology selection — use the async-selection decision tree. NOT for single-job batch patterns, scope sizing, or Database.Stateful design — use batch-apex-patterns.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Apex Batch Chaining
This skill activates when a practitioner needs to trigger one or more Batch Apex jobs in a controlled sequence — using `finish()` callbacks, Flex Queue guards, or a Queueable bridge — and must avoid silent job-queue saturation or loss of intermediate state.
---
## Before Starting
Gather this context before working on anything in this domain:
- Confirm you actually need chaining: if you only have one large job, use `batch-apex-patterns` instead.
- Know how many concurrent and queued batch jobs already exist in the org — the Flex Queue holds a maximum of 100 jobs and only 5 can execute concurrently. A chain that submits blindly can silently fail or delay.
- Identify whether intermediate state must pass between jobs. `Database.Stateful` keeps state inside a single job; you need a different mechanism (Custom Settings, Custom Metadata, a temporary SObject, or a constructor parameter) to pass state between chained jobs.
- Confirm the API version is 26.0 or later — `Database.executeBatch` from `finish()` was introduced at API v26.0.
---
## Core Concepts
### finish() as the Chain Trigger
Every Batch Apex class implements three interface methods: `start()`, `execute()`, and `finish()`. The `finish(Database.BatchableContext bc)` method is called exactly once after all `execute()` scope chunks complete. Calling `Database.executeBatch(new NextBatch())` inside `finish()` is the standard, platform-supported mechanism for chaining. The returned `Id` is the `AsyncApexJob` Id of the newly enqueued job — capture it if you need to monitor downstream status.
Chaining from `finish()` is synchronous from the perspective of your code but fully asynchronous from the platform's perspective. The new job enters the **Flex Queue** and waits for an execution slot.
### The Flex Queue and the 5-Concurrent-Job Ceiling
Before the Flex Queue was introduced, Salesforce enforced a hard 5-concurrent-batch limit that caused `Database.executeBatch` to throw a `LimitException` when the ceiling was hit. The Flex Queue removed that hard throw: jobs now queue silently behind the 5 active slots. The Flex Queue can hold up to **100 jobs** (holding + active combined in a single org).
The risk is that silent queuing makes it easy to saturate the queue in high-volume orgs. A chain that checks `System.FlexQueue.getJobIds().size()` before each `Database.executeBatch` call catches saturation before it becomes a production incident.
### Queueable as an Unlimited-Depth Alternative
A Queueable class can enqueue a new Queueable from inside its own `execute()` method — this is the standard recursive Queueable pattern. The depth limit per transaction is **1 child Queueable** per `execute()` call, but there is no enforced total chain depth at the platform level. Queueable chains are therefore preferred when:
- The number of chain steps is not known at design time.
- You need to pass complex typed state between steps (Queueable constructors accept any serializable type).
- Each step must conditionally decide whether to enqueue the next step.
Queueable chains have their own governor context per `execute()` invocation, just like batch. The trade-off is that Queueable does not chunk records the way Batch does — if a step processes large data sets you still need a batch class for that step, with a Queueable acting only as the coordinator.
### Test Limitations
`Test.startTest()` / `Test.stopTest()` forces **one synchronous chain level**: the first batch job runs synchronously at `stopTest()`, but any job enqueued from within `finish()` does not run synchronously in the same test. This means full multi-step chains **cannot be unit-tested end-to-end** in a single test method. The correct approach is to test each link in isolation with its own test method, verifying that `finish()` calls `Database.executeBatch` (or `System.enqueueJob`) with the expected arguments. Use `Test.getStandardPricebookId()` / `Test.isRunningTest()` guards where needed.
---
## Common Patterns
### Pattern 1: Two-Step Chain with Flex Queue Guard
**When to use:** You have exactly two batch jobs that must run in sequence and you want the simplest possible implementation.
**How it works:**
```apex
public class StepOneBatch implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator([SELECT Id FROM Account WHERE ...]);
}
public void execute(Database.BatchableContext bc, List<SObject> scope) {
// process scope
}
public void finish(Database.BatchableContext bc) {
// Guard: Flex Queue must have room before chaining
Integer queuedJobs = [
SELECT COUNT() FROM AsyncApexJob
WHERE JobType = 'BatchApex'
AND Status IN ('Holding', 'Queued', 'Processing', 'Preparing')
];
if (queuedJobs >= 95) {
// Log and alert — do not chain into a saturated queue
System.debug(LoggingLevel.ERROR,
'StepOneBatch: Flex Queue near capacity (' + queuedJobs +
'). StepTwoBatch NOT enqueued.');
return;
}
Database.executeBatch(new StepTwoBatch(), 200);
}
}
```
**Why not blindly call executeBatch:** Without the guard, a saturated queue accepts the job silently but the job sits in `Holding` status indefinitely. Monitoring alerts will not fire until a human reviews the queue.
### Pattern 2: Queueable Coordinator for Multi-Step Chains
**When to use:** Three or more steps, or when each step must decide conditionally whether to proceed.
**How it works:**
```apex
public class BatchChainCoordinator implements Queueable {
private Integer step;
private Id contextId; // pass state between steps
public BatchChainCoordinator(Integer step, Id contextId) {
this.step = step;
this.contextId = contextId;
}
public void execute(QueueableContext ctx) {
if (step == 1) {
Database.executeBatch(new StepOneBatch(contextId), 200);
} else if (step == 2) {
Database.executeBatch(new StepTwoBatch(contextId), 200);
} else if (step == 3) {
Database.executeBatch(new StepThreeBatch(contextId), 200);
}
// Queueable does NOT chain itself here — the batch finish() calls:
// System.enqueueJob(new BatchChainCoordinator(step + 1, contextId));
}
}
```
Each batch's `finish()` method calls:
```apex
public void finish(Database.BatchableContext bc) {
System.enqueueJob(new BatchChainCoordinator(2, this.contextId));
}
```
**Why this works better than pure batch-to-batch chaining:** The coordinator owns all routing logic in one place. Adding a step means editing one class, not modifying every batch's `finish()`. Conditional skipping (e.g., skip step 3 if no records were processed) is easy to add.
---
## Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| Two sequential batch jobs, simple state passing via constructor | Direct `finish()` chain with Flex Queue guard | Simplest; no extra class needed |
| Three or more sequential batch jobs | Queueable coordinator + batch `finish()` → `enqueueJob()` | Centralizes routing; avoids modifying every finish() when chain grows |
| Chain steps unknown at design time (dynamic depth) | Queueable chain — each step decides whether to enqueue next | Only Queueable supports fully open-ended depth without design-time limit |
| Need to pass complex typed objects between steps | Queueable constructor parameters | Batch constructor accepts typed args but Queueable makes this the primary state-passing mechanism |
| Chain must survive test coverage requirements with full path coverage | Separate unit tests per batch class | Test.stopTest() only runs one synchronous level — end-to-end integration testing requires a sandbox run |
---
## Recommended Workflow
Step-by-step instructions for an AI agent or practitioner implementing batch chaining:
1. **Confirm chaining is the right choice** — check `standards/decision-trees/async-selection.md`. If the job is a single large dataset, `batch-apex-patterns` is sufficient. If real-time triggering is needed, consider Platform Events.
2. **Assess Flex Queue headroom** — query `AsyncApexJob` for `Status IN ('Holding','Queued','Processing','Preparing')` in the target org. If count is near 90, implement a hard guard before any `Database.executeBatch` call in `finish()`.
3. **Choose chain architecture** — two steps and simple state: direct `finish()` chain. Three or more steps, conditional logic, or unknown depth: Queueable coordinator pattern.
4. **Implement state transfer** — do NOT rely on `Database.Stateful` across jobs. Pass state via constructor parameters (primitive types or serializable classes). For large state, persist to a staging SObject or Custom Setting and query it in the next job's `start()`.
5. **Write unit tests per batch class** — test each class independently. Assert that `finish()` calls `Database.executeBatch` (or `System.enqueueJob`) with correct arguments using a test flag or mock. Do not attempt to run the full chain in a single test method.
6. **Add AsyncApexJob monitoring** — query `AsyncApexJob` by the returned `Id` from `Database.executeBatch` to confirm each job reaches `Completed` status. Log `NumberOfErrors` and `ExtendedStatus` fields to your custom logging framework.
7. **Review with the checklist below** before deploying to production.
---
## Review Checklist
Run through these before marking work in this area complete:
- [ ] Every `Database.executeBatch` call in `finish()` is guarded by a Flex Queue size check
- [ ] State passed between chained jobs uses constructor parameters or a staging SObject — NOT `Database.Stateful` across job boundaries
- [ ] Each batch class in the chain has its own unit test; no test attempts to assert the full multi-step chain in one `Test.startTest()/stopTest()` block
- [ ] The `Id` returned by `Database.executeBatch` is captured and logged so downstream job status can be monitored
- [ ] There is an alerting/abort path when the Flex Queue guard fires (not a silent no-op)
- [ ] Chain does not have the potential to recurse infinitely — a step counter or terminal condition is present
---
## Salesforce-Specific Gotchas
Non-obvious platform behaviors that cause real production problems:
1. **Silent Flex Queue saturation** — `Database.executeBatch` no longer throws a `LimitException` when the org has many queued jobs. If the Flex Queue already holds 100 jobs, the call silently returns null (or throws `AsyncException` depending on context). Without a guard, the chain step is lost with no error surfaced to the calling code.
2. **Test.stopTest() only runs one synchronous batch level** — calling `Test.stopTest()` inside a test method forces the first batch job to run synchronously, but any `Database.executeBatch` or `System.enqueueJob` call made from within that job's `finish()` is NOT executed synchronously. Tests that assert on a downstream job's effects will always fail.
3. **5-concurrent-job limit still governs execution slots** — even with the Flex Queue, only 5 batch jobs can run concurrently per org. A chain that submits many small jobs rapidly fills the execution slots and leaves later jobs in `Holding` status. Size scope appropriately to keep each job's wall-clock time reasonable.
---
## Output Artifacts
| Artifact | Description |
|---|---|
| `finish()` implementation | Guarded chain trigger using `Database.executeBatch` with Flex Queue size check |
| `BatchChainCoordinator` Queueable | Optional coordinator class for multi-step or conditional chains |
| Unit test per batch class | Isolated test asserting correct chaining behavior without full end-to-end execution |
---
## Related Skills
- `batch-apex-patterns` — scope sizing, `Database.Stateful`, `QueryLocator` vs `Iterable`, and single-job batch design; read this first before chaining
- `apex-queueable-patterns` — Queueable interface, `System.enqueueJob`, and child-job limits; used when building the Queueable coordinator
- `apex-transaction-finalizers` — for cleanup logic after Queueable step failures inside a chain
- `async-apex` — high-level comparison of all async mechanisms; useful for initial technology selectionRelated Skills
batch-apex-patterns
Use when designing, reviewing, or debugging Batch Apex contracts, scope sizing, stateful behavior, chaining, and AsyncApexJob monitoring. Triggers: 'Database.Batchable', 'Database.Stateful', 'executeBatch', 'batch scope', 'AsyncApexJob'. NOT for generic async choice discussions where Queueable or Future might still be the better tool.
apex-queueable-patterns
Use when designing, implementing, reviewing, or debugging Queueable Apex jobs that chain, use the Finalizer interface, pass state across transactions, or need controlled async depth. Trigger keywords: 'Queueable', 'System.enqueueJob', 'Finalizer', 'QueueableContext', 'AsyncOptions', 'stack depth', 'chained queueable'. NOT for basic async Apex mechanism selection (use async-apex), NOT for large-volume record processing where Batch Apex is the right tool (use batch-apex-patterns).
apex-managed-sharing-patterns
Grant row-level access programmatically via __Share records when declarative sharing rules cannot express the policy. NOT for OWD, role hierarchy, or criteria-based sharing rule design.
lwc-imperative-apex
Call Apex methods imperatively from LWC — on button click, lifecycle hooks, or conditional logic. Covers import syntax, cacheable vs non-cacheable, async/await patterns, error handling, loading states, and Promise.all. NOT for wire service (use wire-service-patterns) and NOT for testing Apex mocks (use lwc-testing).
real-time-vs-batch-integration
When to use this skill: choosing between real-time (synchronous callouts, Platform Events, CDC, Pub/Sub API) and batch (Bulk API 2.0, scheduled ETL) integration patterns. Trigger keywords: should I use real-time or batch, how to sync high-volume data, when to use Platform Events vs Bulk API, integration latency vs volume tradeoff. NOT for Batch Apex internals (use batch-apex-patterns), NOT for MuleSoft middleware design (use middleware-integration-patterns), NOT for CDC field tracking configuration.
dataweave-for-apex
Use when transforming structured data inside Apex — CSV → JSON, XML → SObject list, JSON → flattened CSV, or schema-mapping a third-party payload to a Salesforce model — and the existing options (`JSON.deserialize`, `Dom.Document`, hand-written loops) are getting unwieldy. Triggers: 'apex transform csv json xml without external library', 'system.dataweave script', 'salesforce native dataweave apex execute', 'transform xml to sobject apex no mulesoft', 'json reshape salesforce apex script'. NOT for MuleSoft Anypoint DataWeave running off-platform (use mulesoft-anypoint-architecture), NOT for Apex JSON serialization basics (use apex-json-serialization), NOT for Bulk API CSV ingest (use bulk-api-2-patterns).
flow-invocable-from-apex
Author @InvocableMethod Apex classes that Flow can call as Actions. Design the input / output variable contract, bulk semantics (one list in, one list out), null handling, and error surfacing. Also covers the inverse direction: calling a flow from Apex via Flow.Interview. NOT for general Apex authoring (use apex-service-selector-domain). NOT for REST-exposed Apex (use apex-rest-resource-patterns).
flow-batch-processing-alternatives
Use when a Scheduled Flow or Record-Triggered Flow needs to process more records than Flow can safely handle in a single run. Covers Flow limit realities, scheduled-path chunking, Data Cloud batch transforms, and Apex Queueable/Batch escalation. Does NOT cover choosing async across a general workflow (see async-selection decision tree).
flow-apex-defined-types
Design and use Apex-Defined Types as Flow variables for structured non-sObject data (HTTP callout payloads, External Service responses, complex configuration). Trigger keywords: apex-defined type, flow variable, @AuraEnabled class, flow http callout response. Does NOT cover building HTTP Callout Actions themselves, External Services schema, or raw Apex invocable methods.
data-loader-batch-window-sizing
Choose the right batch size, parallel/serial mode, and load window for Data Loader, Bulk API V1/V2, and custom Database.executeBatch jobs against a given object volume and complexity profile. Covers tradeoffs between batch size, trigger CPU cost, sharing recalculation cost, and row-skew lock contention. NOT for CSV column mapping (see data/data-loader-csv-column-mapping). NOT for picklist validation pre-load (see data/data-loader-picklist-validation-pre-load). NOT for sharing-recalc tuning after the load lands (see data/sharing-recalculation-performance).
batch-data-cleanup-patterns
Use when scheduling automated deletion of temporary records, enforcing data retention policies, running nightly cleanup jobs, reclaiming org storage, managing recycle bin, or performing async bulk deletion of aged records. Trigger keywords: batch delete, retention policy, purge records, cleanup job, recycle bin, emptyRecycleBin, hard delete, nightly purge, storage optimization. NOT for data archival to external storage (use data-archival-strategies).
scheduled-apex-failure-detection-and-monitoring
Use when nightly batch / scheduled Apex jobs are failing without anyone noticing — covers why uncaught exceptions in `execute()` go to the debug log instead of email, how to query `AsyncApexJob` for `Status`, `NumberOfErrors`, and `ExtendedStatus`, when to implement `Database.RaisesPlatformEvents` so the platform publishes `BatchApexErrorEvent` on uncaught failures, how to subscribe to that event with an Apex trigger and notify operators, and how to layer a custom watcher schedule on top so silent-failure modes (job that never started, scheduled class deleted, queue stuck on `Queued`) still surface. Triggers: 'nightly batch failed at 2am with no notification', 'how do we know if a scheduled apex job is failing', 'BatchApexErrorEvent vs custom retry logic', 'Setup Apex Jobs only shows last 7 days, where else can I look', 'job is stuck in queued status nobody noticed for a week'. NOT for general Apex exception handling patterns (use apex/apex-exception-handling-and-logging), NOT for Batch Apex authoring or chunking strategy (use apex/batch-apex-design), NOT for Setup → Apex Jobs UI walkthrough as an admin task (use admin/batch-job-scheduling-and-monitoring), NOT for retry logic itself (use apex/scheduled-apex-retry-patterns once authored).