Repository navigation
Best Practices
This guide outlines best practices for building robust, efficient async action processors. Following these patterns will help you create processors that are maintainable, performant, and resilient.
Every action must end the transaction with either a completed status or be handled by the failure mechanism. Actions left without status updates will remain in 'Pending' status indefinitely.
public void process(AsyncActionProcessor__mdt settings, List<AsyncAction__c> actions) {
// ❌ Bad - Action status never updated
for (AsyncAction__c action : actions) {
processBusinessLogic(action);
// Missing status update - action stays pending forever!
}
// ✅ Good - Every action gets a final status
for (AsyncAction__c action : actions) {
try {
processBusinessLogic(action);
action.Status__c = AsyncActions.Status.COMPLETED.name();
} catch (Exception error) {
new AsyncActions.Failure(settings).fail(action, error);
}
}
}Avoid modifying the original actions List parameter, as this may interfere with the framework's error detection. Instead, work with a copy or Map. For actions that each name a record, AsyncActions.groupByRecord does this for you.
public void process(AsyncActionProcessor__mdt settings, List<AsyncAction__c> actions) {
// ❌ Bad - Modifying the original list
actions.remove(0);
actions.add(someNewAction);
// ✅ Good - Work with a Map for processing
Map<Id, AsyncAction__c> actionMap = new Map<Id, AsyncAction__c>(actions);
for (AsyncAction__c action : actionMap.values()) {
// If an action isn't relevant, mark as failed and remove from map:
if (action?.RelatedRecordId__c == null) {
actionMap?.remove(action?.Id);
new AsyncActions.Failure(settings, AsyncActions.RetryBehavior.SUDDEN_DEATH)
.fail(action, 'Missing RelatedRecordId__c');
}
}
// Now process any remaining valid actions:
this.processValidActions(actionMap);
}Validate input data early and fail gracefully for invalid data. AsyncActions.groupByRecord fails any action whose RelatedRecordId__c is not a valid Id with SUDDEN_DEATH, and groups the rest by record. See AsyncActions.RecordGroup for a full example.
public void process(AsyncActionProcessor__mdt settings, List<AsyncAction__c> actions) {
// Actions with an invalid RelatedRecordId__c have already failed; blank ones are under the null key
Map<Id, AsyncActions.RecordGroup> groups = AsyncActions.groupByRecord(settings, actions);
// ...query groups.keySet() once, then complete, cancel or fail each group
}When using the Data__c field, validate and parse JSON data safely.
private Map<String, Object> parseActionData(AsyncAction__c action) {
if (String.isBlank(action.Data__c)) {
return new Map<String, Object>();
}
try {
return (Map<String, Object>) JSON.deserializeUntyped(action.Data__c);
} catch (Exception error) {
// Handle invalid JSON gracefully
return new Map<String, Object>();
}
}The framework automatically handles Async Action record updates at the end of each batch. For this reason, you do not need to perform DML on these records.
public void process(AsyncActionProcessor__mdt settings, List<AsyncAction__c> actions) {
// ❌ Bad - Manual DML on AsyncAction records
update actions;
// ✅ Good - Only DML on business records
List<Task> tasksToInsert = this.generateTasksFromActions(actions);
insert tasksToInsert;
}Use the framework's built-in failure handling for consistent retry behavior.
public void process(AsyncActionProcessor__mdt settings, List<AsyncAction__c> actions) {
try {
// Your business logic here
processActions(actions);
// Mark successful actions as completed
for (AsyncAction__c action : actions) {
action.Status__c = 'Completed';
}
} catch (Exception error) {
// Let the framework handle retries
new AsyncActions.Failure(settings, AsyncActions.RetryBehavior.ALLOW_RETRY)
.fail(actions, error);
}
}Handle scenarios where some actions succeed and others fail within the same batch using allOrNone=false DML operations.
public void process(AsyncActionProcessor__mdt settings, List<AsyncAction__c> actions) {
List<SObject> recordsToInsert = new List<SObject>();
for (AsyncAction__c action : actions) {
SObject record = this.createRecordForAction(action);
recordsToInsert.add(record);
}
List<Database.SaveResult> results = Database.insert(recordsToInsert, false);
for (Integer i = 0; i < results.size(); i++) {
AsyncAction__c action = actions[i];
Database.SaveResult result = results[i];
if (result?.isSuccess() == true) {
action.Status__c = 'Completed';
} else {
List<Database.Error> errors = result.getErrors();
new AsyncActions.Failure(settings).fail(action, errors);
}
}
}- AsyncAction__c
- AsyncActionProcessor__mdt
- AsyncActionParameter__mdt
- AsyncActionScheduledJob__mdt
- AsyncActionScheduledJobItem__mdt