---
schema_version: "1"
id: 8f245b49d5ab873f097320f7d3391ec75aa63769a58a13eb2ee48776e5edc4bf
title: Prompt Template Batch Processing in Apex
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/genai/agentforce/guides/models/prompt-template-batch-job.md
canonical_url: https://developer.salesforce.com/docs/ai/agentforce/guide/prompt-template-batch-job.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: b933f897b57602b69a71e659d391e04bf2e71a41e8110dd0df1bd7db47beb776
products:
  - Agentforce Developer Guide
  - Agentforce and Einstein Generative AI
---
# Prompt Template Batch Processing in Apex

To asynchronously generate large quantities of responses for prompt templates you can use Prompt Template Batch Processing. You can batch and execute jobs in Flow and Apex. For Flow, see [Flow Core Action: Prompt Template Actions](https://help.salesforce.com/s/articleView?id=platform.flow_ref_elements_actions_generate_prompt_response.htm).

See the object reference guide for [AiJobRun](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_aijobrun.htm) and [AiJobRunItem](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_aijobrunitem.htm).

## Considerations

There are several immutability and status constraints for batch processing standard objects.

### Immutability

Once an `AiJobRun` begins processing and the status updates to `InProgress`, some `AiJobRunItem` fields become immutable.

- You can't change the `AiJobRunId` and `Input` field values.
- `AiJobRunItem` schema fields set to a non-null value (except status) can't be changed.
- You can't delete the `AiJobRun` or any of its `AiJobRunItems`.

### Status Changes

These statuses for both AiJobRun and AiJobRunItem can't be changed by the user:

- `InProgress`
- `Completed`
- `Failed`

To initiate an `AiJobRun` for batch execution, update the status to `ReadyToStart`.

### Rate Limits

Standard models have a limit of 1,000 `AiJobRunItems` for each `AiJobRun`. Models that support native batch processing have a limit of 10,000 `AiJobRunItems` for each `AiJobRun`.

In Apex, the recommended daily volume is 5,000 items for standard models and 50,000 items for native-batch models (approximately five `AiJobRun` objects at the maximum items per job). You can create and start as many `AiJobRun` objects as you need. If more jobs are ready than the system can process at once, the extra jobs stay in `Queued` status and are picked up automatically as capacity becomes available.

Exceeding the recommended daily volume may cause some jobs to take longer than 24 hours to complete, as extra items are only processed as capacity allows. For native-batch models, the expected time to completion is determined by the model provider (typically 24 hours).

For a list of models that support native batch processing, see [Large Language Model Support](https://help.salesforce.com/s/articleView?id=ai.generative_ai_large_language_model_support.htm).

### Processing Order

When an `AiJobRun` object is created, it's assigned a `CreatedDate` field with a dateTime timestamp. Multiple `AiJobRun` objects with a `ReadyToStart` status begin processing in chronological order of the `CreatedDate` value. However, if several `AiJobRun` objects are updated to `ReadyToStart` within a few seconds of one another, the start time may not strictly follow the `CreatedDate` order. This processing order behavior applies to all `AiJobRun` objects, regardless of how their status is updated.

## Apex Example

Use this example when you need to generate LLM output for a large set of records without blocking a user-facing transaction. For instance, a nightly summarization of a Service Cloud case backlog.

The code below summarizes the `Subject` and `Description` of a set of `Case` records using a prompt template named `Summarize_Case`, and writes each summary back as an internal `CaseComment` (see [Monitor Job Completion](#monitor-job-completion) below). The same three-step pattern applies to any prompt template and any SObject input.

### Prerequisites

- A prompt template with DeveloperName `Summarize_Case` exists in the org and declares a `Case` SObject input variable.
- The user running the job is assigned the standard `Prompt Template User` permission set.

**note:** All three steps below run in the same Apex transaction. `jobRun.Id` is only available after the insert in Step 1, and every item must be inserted before the status flip in Step 3.

### Step 1: Create the AiJobRun

The prompt template reference lives on `AiJobRun.Target`, which is a string that accepts either the template's DeveloperName or its record Id. The DeveloperName is recommended since it's stable across orgs. `Status` is required on insert. Use `New` so Apex can flip it to `ReadyToStart` after the items are in place.

```apex
AiJobRun jobRun = new AiJobRun(
    JobType = 'PromptTemplate',
    Target  = 'Summarize_Case', // prompt template DeveloperName (Id also accepted)
    Status  = 'New'
);
insert as user jobRun;
```

### Step 2: Create AiJobRunItem records

There’s one `AiJobRunItem` per input record. The `Input` field is a JSON string whose keys use the documented `Input`: prefix, and the value under `Input:Case` is a record pointer with an `id`. Insert `Status = 'Ready'` on every item.

```apex
List<Case> cases = [SELECT Id FROM Case LIMIT 200];

List<AiJobRunItem> items = new List<AiJobRunItem>();
for (Case c : cases) {
    Map<String, Object> payload = new Map<String, Object>{
        'Input:Case' => new Map<String, Object>{ 'id' => c.Id }
    };
    items.add(new AiJobRunItem(
        AiJobRunId = jobRun.Id,
        Status     = 'Ready',
        Input      = JSON.serialize(payload)
    ));
}
insert as user items;
```

**note:** For runs approaching the 10,000-item cap, wrap this step in a `Database.Batchable` so each execute scope gets fresh governor limits.

### Step 3: Flip the AiJobRun to ReadyToStart

Once every item is inserted, updating the parent to `ReadyToStart` hands the batch off to the platform orchestrator. Nothing should change on the job or its items after this point. See [Considerations: Immutability](#immutability) for more details.

```apex
jobRun.Status = 'ReadyToStart';
update as user jobRun;
```

## Monitor Job Completion

The platform publishes an `AiJobRunStatusEvent` on each transition into `InProgress`, `Completed`, and `Failed`. Subscribe to [`AiJobRunStatusEvent`](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/sforce_api_objects_aijobrunstatusevent.htm) with a [platform-event trigger](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers.htm) and filter for `Completed`. See [Subscribe to Platform Event Notifications with Apex Triggers](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_subscribe_apex.htm) for more information.

When the job completes, you can query the related `AiJobRunItem` records to access both the original input data and the generated responses. The `Input` field contains the input data for each individual item within the job run, and the Response field contains the corresponding LLM response as JSON in the form `{"promptResponse":"<LLM text>"}`. Unwrap the JSON envelope before using the text.

Event schema: `AiJobRunIdentifier` (The parent `AiJobRun` Id as a string), `Status`, `JobType`, `Target`.

### Trigger

```apex
trigger AiJobRunStatusEventTrigger on AiJobRunStatusEvent (after insert) {
    Set<Id> completedJobRunIds = new Set<Id>();
    for (AiJobRunStatusEvent e : Trigger.new) {
        if (e.Status == 'Completed' && String.isNotBlank(e.AiJobRunIdentifier)) {
            completedJobRunIds.add((Id) e.AiJobRunIdentifier);
        }
    }
    if (!completedJobRunIds.isEmpty()) {
        try {
            AiJobRunStatusEventHandler.handleCompletedJobs(completedJobRunIds);
        } catch (Exception ex) {
            // Swallow and log. An uncaught exception fails the batch of events, and after
            // 9 retries the platform disables the subscription — killing all future delivery.
            System.debug(LoggingLevel.ERROR,
                'AiJobRunStatusEventTrigger failed for ' + completedJobRunIds +
                ': ' + ex.getTypeName() + ': ' + ex.getMessage());
        }
    }
}
```

### Handler

The handler queries the finished `AiJobRunItem` rows, parses each `Input` JSON (`{"Input:Case":{"id":"<CaseId>"}}`) back to the source Case Id, unwraps the `Response` envelope (`{"promptResponse":"<LLM text>"}`) to the LLM text, and writes that text as an internal `CaseComment`.

```apex
public without sharing class AiJobRunStatusEventHandler {
    public static void handleCompletedJobs(Set<Id> completedJobRunIds) {
        List<AiJobRunItem> items = [
            SELECT Id, AiJobRunId, Input, Response
            FROM AiJobRunItem
            WHERE AiJobRunId IN :completedJobRunIds
              AND Status = 'Completed'
            WITH SYSTEM_MODE
        ];


        List<CaseComment> comments = new List<CaseComment>();
        for (AiJobRunItem item : items) {
            String promptText = extractPromptResponse(item.Response);
            if (String.isBlank(promptText)) continue;
            Id caseId = extractCaseId(item.Input);
            if (caseId == null) continue;
            if (promptText.length() > 3500) {
                promptText = promptText.substring(0, 3500);
            }
            comments.add(new CaseComment(
                ParentId    = caseId,
                IsPublished = false,
                CommentBody = promptText
            ));
        }
        if (!comments.isEmpty()) {
            insert as system comments;
        }
    }


    // Input envelope: {"Input:Case":{"id":"<CaseId>"}}
    private static Id extractCaseId(String inputJson) {
        if (String.isBlank(inputJson)) return null;
        try {
            Map<String, Object> parsed =
                (Map<String, Object>) JSON.deserializeUntyped(inputJson);
            Map<String, Object> ref = (Map<String, Object>) parsed.get('Input:Case');
            return ref == null ? null : (Id) String.valueOf(ref.get('id'));
        } catch (Exception ignore) {
            return null;
        }
    }


    // Response envelope: {"promptResponse":"<LLM text>"}
    private static String extractPromptResponse(String responseJson) {
        if (String.isBlank(responseJson)) return null;
        try {
            Map<String, Object> parsed =
                (Map<String, Object>) JSON.deserializeUntyped(responseJson);
            Object text = parsed.get('promptResponse');
            return text == null ? null : String.valueOf(text);
        } catch (Exception ignore) {
            return null;
        }
    }
}
```

**note:** Platform events can be redelivered, so a production handler should remove duplicates (for example, by keying off `AiJobRunId + ParentId`). Individual items can also finish with `Status = 'Failed'`. Query and surface those alongside the successes rather than silently skipping them. For runs approaching the 10,000-item cap, offload the write-back to a `Database.Batchable` (or enqueue a Queueable per scope) so each execute gets fresh governor limits.

## Job Type Values

[`AiJobRun.JobType`](https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_aijobrun.htm) can differ based on how the batch is initiated.

- Apex-created batch runs: `PromptTemplate`
- Flow (Prompt Template Batch Generation action): `GeneratePromptAsyncIA`

## See Also

- _Agentforce Developer Guide_: [Get Started with Prompt Builder](https://developer.salesforce.com/docs/ai/agentforce/guide/prompt-builder/get-started-prompt-builder.md)
- _Salesforce Help_: [Flow Core Action: Prompt Template Actions](https://help.salesforce.com/s/articleView?id=platform.flow_ref_elements_actions_generate_prompt_response.htm)
