apex-transaction-finalizers
Use this skill when you need guaranteed post-Queueable cleanup, retry, or failure-logging logic that must run even when the parent Queueable throws an unhandled exception. Trigger keywords: FinalizerContext, System.attachFinalizer, Queueable cleanup on failure, post-job compensation, guaranteed async cleanup. NOT for batch job completion callbacks — use apex-batch-chaining. NOT for platform event publishing on failure — use platform-events-apex.
Best use case
apex-transaction-finalizers is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Use this skill when you need guaranteed post-Queueable cleanup, retry, or failure-logging logic that must run even when the parent Queueable throws an unhandled exception. Trigger keywords: FinalizerContext, System.attachFinalizer, Queueable cleanup on failure, post-job compensation, guaranteed async cleanup. NOT for batch job completion callbacks — use apex-batch-chaining. NOT for platform event publishing on failure — use platform-events-apex.
Teams using apex-transaction-finalizers 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-transaction-finalizers/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How apex-transaction-finalizers Compares
| Feature / Agent | apex-transaction-finalizers | 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 guaranteed post-Queueable cleanup, retry, or failure-logging logic that must run even when the parent Queueable throws an unhandled exception. Trigger keywords: FinalizerContext, System.attachFinalizer, Queueable cleanup on failure, post-job compensation, guaranteed async cleanup. NOT for batch job completion callbacks — use apex-batch-chaining. NOT for platform event publishing on failure — use platform-events-apex.
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 Transaction Finalizers This skill activates when a Queueable job needs guaranteed post-execution behavior — cleanup, retry, or failure logging — that must run even if the parent Queueable throws an unhandled exception. Use `System.attachFinalizer()` to bind a `System.Finalizer` implementation to a Queueable; the Finalizer runs in a **separate Apex transaction** with **full governor limits** after the parent job finishes. --- ## Before Starting Gather this context before working on anything in this domain: - Confirm the parent job is a `Queueable` (not Batch, Scheduled, or `@future`). Transaction Finalizers are only supported on Queueable jobs. - Identify the failure scenario: Is this a retry (re-enqueue the same job), a compensation (write a failure record / publish a PE), or silent logging? - Determine a retry ceiling. Finalizers can enqueue exactly one new Queueable — if that Queueable also has a Finalizer, the chain continues. Without a retry limit you risk infinite loops. - Check the API version of the Queueable class — `System.attachFinalizer()` requires API v53.0+ (Summer '21). - The Finalizer does **not** run if the parent job was aborted via `System.abortJob()`. Plan for that case separately. --- ## Core Concepts ### Finalizer Lifecycle When `System.attachFinalizer(myFinalizer)` is called inside a Queueable's `execute()` method, the platform registers the Finalizer to execute after the parent job's transaction closes — whether it committed successfully or was rolled back due to an unhandled exception. The Finalizer runs in a **completely separate Apex transaction** with fresh governor-limit counters (100 SOQL queries, 150 DML statements, etc.). The parent transaction's state (variable values, uncommitted DML) is not visible to the Finalizer. ### FinalizerContext API The Finalizer's `execute(FinalizerContext ctx)` method receives a `FinalizerContext` object with three key members: | Member | Returns | Notes | |---|---|---| | `ctx.getJobId()` | `Id` | The `AsyncApexJob` ID of the **parent** Queueable | | `ctx.getResult()` | `System.ParentJobResult` | `SUCCESS` or `UNHANDLED_EXCEPTION` | | `ctx.getException()` | `Exception` | Non-null only when `getResult() == UNHANDLED_EXCEPTION` | Always gate retry / compensation logic on `ctx.getResult()` to avoid double-processing on success. ### Enqueue Constraint A Finalizer may enqueue **exactly one** new Queueable job via `System.enqueueJob()`. Attempting to enqueue more than one throws a `System.AsyncException`. A Finalizer cannot attach another Finalizer to itself — `System.attachFinalizer()` called from within a Finalizer context throws a `System.AsyncException`. ### Abort Gap If the parent Queueable is terminated via `System.abortJob()`, the Finalizer is **not invoked**. This is a hard platform constraint with no workaround at the Finalizer layer. If abort-path cleanup is required, model it as a separate Schedulable or monitoring job that polls `AsyncApexJob` for `ABORTED` status. --- ## Common Patterns ### Retry-on-Failure with Backoff Counter **When to use:** A Queueable makes an external callout or complex DML that can fail transiently. You want automatic retry up to N times without manual re-queuing. **How it works:** 1. Pass a `retryCount` integer into the Queueable constructor. 2. Inside `execute()`, call `System.attachFinalizer(new MyFinalizer(jobPayload, retryCount))`. 3. In the Finalizer's `execute()`, check `ctx.getResult()`. On `UNHANDLED_EXCEPTION` and `retryCount < MAX_RETRIES`, enqueue a new instance of the Queueable with `retryCount + 1`. 4. On `retryCount >= MAX_RETRIES`, write a failure record instead of re-enqueuing. **Why not try/catch inside execute():** A `try/catch` inside `execute()` only catches exceptions thrown by code in that block — governor-limit violations and some system exceptions escape it. A Finalizer provides an out-of-band, guaranteed callback even for unhandled exceptions that bypass catch blocks. ### Failure Logging to Custom Object **When to use:** You need an auditable record of every Queueable failure for operations monitoring, SLA reporting, or manual reprocessing. **How it works:** 1. Attach a Finalizer that receives the job context (record IDs, batch key, etc.) from the parent Queueable constructor. 2. In `execute(ctx)`, if `ctx.getResult() == UNHANDLED_EXCEPTION`, insert an `Async_Job_Error__c` (or equivalent) record with the job ID, exception message, stack trace, and payload snapshot. 3. Use the single Queueable enqueue slot only if retry is also needed; otherwise leave it unused. **Why not System.debug:** Debug logs are transient and unavailable to non-admin users. A custom object record survives platform restarts and is queryable by monitoring tools. --- ## Decision Guidance | Situation | Recommended Approach | Reason | |---|---|---| | Parent Queueable fails transiently (callout timeout, lock contention) | Retry Finalizer with counter | Full governor limits in separate transaction; single enqueue slot used for the retry job | | Failure needs permanent audit record | Logging Finalizer (DML in separate transaction) | Parent transaction is rolled back; Finalizer gets fresh DML budget | | Both retry AND logging needed | Single Finalizer handles both; log first, then conditionally enqueue retry | Enqueue limit is 1 — combine both behaviors in one Finalizer | | Parent job was aborted by admin | Schedulable monitor polling `AsyncApexJob` for ABORTED status | Finalizer does not fire on abort; no workaround | | Batch job completion callback | `Database.Batchable` `finish()` method or apex-batch-chaining skill | Transaction Finalizers are Queueable-only | | Publishing a Platform Event on failure | PE publish inside Finalizer OR dedicated PE skill | PE publish counts against Finalizer's DML budget; prefer dedicated skill for complex routing | --- ## Recommended Workflow 1. **Confirm Queueable context** — verify the failing async job is a `Queueable`, API v53+, and that abort-path behavior does not need to be covered by this Finalizer. 2. **Choose Finalizer behavior** — decide between retry, compensation DML, or both. If both, plan the single Finalizer class that handles them sequentially. 3. **Design the retry ceiling** — pick `MAX_RETRIES` (typically 3–5) and pass `retryCount` through the Queueable constructor so the Finalizer can increment and re-enqueue safely. 4. **Implement `System.Finalizer`** — create a class that `implements System.Finalizer`, receives the job payload via constructor, implements `execute(FinalizerContext ctx)`, gates on `ctx.getResult()`, and enqueues at most one retry job. 5. **Attach in `execute()`** — call `System.attachFinalizer(new MyFinalizer(...))` near the top of the parent Queueable's `execute()` method so it is registered before any code that might throw. 6. **Test both SUCCESS and UNHANDLED_EXCEPTION paths** — use `Test.startTest()` / `Test.stopTest()` to flush the queue; mock the failure by having the Queueable throw in test context, and assert the Finalizer's DML/enqueue behavior. 7. **Review checklist** — confirm no second `attachFinalizer` call, retry counter bounded, no `attachFinalizer` inside the Finalizer itself. --- ## Review Checklist - [ ] `System.attachFinalizer()` is called exactly once per Queueable `execute()` invocation - [ ] Finalizer gates all compensation logic on `ctx.getResult() == System.ParentJobResult.UNHANDLED_EXCEPTION` - [ ] Retry counter is passed via constructor and incremented before re-enqueuing; `MAX_RETRIES` ceiling is enforced - [ ] The Finalizer enqueues at most one new Queueable job (throws `AsyncException` if you try more) - [ ] No call to `System.attachFinalizer()` inside the Finalizer's own `execute()` method - [ ] Tests cover both SUCCESS and UNHANDLED_EXCEPTION result paths - [ ] Abort-path (if required) is handled by a separate mechanism — Finalizer does not fire on `System.abortJob()` --- ## Salesforce-Specific Gotchas 1. **Finalizer does not fire on `System.abortJob()`** — If an admin or another job calls `System.abortJob(parentJobId)`, the Finalizer is silently skipped. This is undocumented in some sources but confirmed in the official Apex Developer Guide. Any cleanup that must happen on abort needs a separate polling mechanism. 2. **Parent transaction rollback is total** — When the Queueable throws an unhandled exception, every DML operation in that transaction is rolled back. The Finalizer starts with a clean slate — it cannot read variables set in the parent, and it cannot "see" records that the parent tried but failed to commit. 3. **One enqueue, no exceptions** — Calling `System.enqueueJob()` more than once in a single Finalizer `execute()` call throws `System.AsyncException` immediately. Wrap the retry call in a conditional so it is only reached when retry is actually needed. 4. **Finalizer exception is swallowed** — If the Finalizer itself throws an unhandled exception, the platform logs it to `ApexLog` but does not propagate it anywhere visible. There is no secondary Finalizer. Build explicit logging inside the Finalizer's own `execute()` using a `try/catch` wrapper. --- ## Output Artifacts | Artifact | Description | |---|---| | `System.Finalizer` implementation class | Apex class implementing `System.Finalizer` with retry and/or logging logic | | Updated Queueable class | Parent Queueable with `System.attachFinalizer()` call and `retryCount` constructor param | | `Async_Job_Error__c` insert (optional) | Custom object record capturing job ID, exception type, message, and stack trace | --- ## Related Skills - apex-queueable-patterns — foundational Queueable design; use alongside this skill for the parent job structure - apex-batch-chaining — for batch-to-batch chaining; Finalizers do not apply to Batch jobs - apex-limits-monitoring — for monitoring Apex governor limits that might cause the Queueable to fail in the first place
Related Skills
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).
transaction-security-policies
Transaction Security policy creation and configuration: condition builder, enhanced policies, enforcement actions (block, MFA, notification, end session), real-time monitoring mode, and policy troubleshooting. NOT for Event Monitoring log analysis or Shield Event Monitoring setup (use event-monitoring). NOT for Apex testing or debug-log analysis.
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).
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-transactional-boundaries
Reason about when a Flow is inside the caller's transaction vs starts its own. Pick Before-Save vs After-Save vs Async Path vs Pause + Resume when transaction boundaries matter. Covers governor-limit sharing, DML sequencing, recoverability, and the exact semantics of each Flow entry point. NOT for choosing Flow vs Apex (use automation-selection.md). NOT for Flow-to-Flow invocation contracts (use subflows-and-reusability).
flow-transaction-finalizer-patterns
Use when a Flow needs to do work that must survive the triggering transaction — post-commit notifications, callouts, audit rows, or compensating actions. Covers Flow Transaction Control element, scheduled paths, Platform Event + finalizer escalation, and Apex Queueable finalizer bridging. Does NOT cover general Flow async decisions (see async-selection).
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-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.
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).
platform-events-apex
Use when publishing or subscribing to Salesforce Platform Events from Apex, comparing Platform Events with Change Data Capture, or designing event-triggered error handling and monitoring. Triggers: 'EventBus.publish', 'platform event trigger', 'CDC vs Platform Events', 'replay ID', 'high-volume event'. NOT for Flow-only publish/subscribe automation.
health-cloud-apex-extensions
Use this skill when extending Health Cloud via Apex: implementing HealthCloudGA managed-package interfaces, automating care plan lifecycle hooks, processing referrals using Industries Common Components invocable actions, or enforcing HIPAA-compliant logging governance for clinical Apex code. Trigger keywords: CarePlanProcessorCallback, HealthCloudGA namespace, ReferralRequest, ReferralResponse, care plan invocable actions, clinical Apex extension, Health Cloud Apex API. NOT for standard Apex triggers or generic Apex development unrelated to Health Cloud managed-package extension points.