> For the complete documentation index, see [llms.txt](https://triton.pharos.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://triton.pharos.ai/common-apex-usage-patters/batch-logging.md).

# Batch Logging

Logging in Batch Apex — where to flush, why each execute() is its own transaction, and how to group every iteration under a single parent log.

Batch jobs are the classic logging blind spot. They run in the background, they chew through thousands of records, and when something fails Salesforce often hands you a one-line error with no stack trace. Good logging turns that black box into something you can actually debug.

There's one thing about batch that changes how you log, so let's lead with it.

## Each `execute()` is its own transaction

Triton's buffer and transaction id are **static state that lives for one Apex transaction** (see [The Static Logging API](/core-concepts/static-logging-api.md)). In a batch, every `execute()` chunk — plus `start()` and `finish()` — runs as a *separate* transaction. So the statics reset between chunks.

The practical consequence: **flush at the end of each `execute()`, not in `finish()`.** Anything you buffer in a chunk is gone once that chunk's transaction ends.

## Basic error handling

For a batch that just needs to not swallow failures, this is all you need — no `Database.Stateful`, no bookkeeping:

{% code lineNumbers="true" %}

```apex
public class LeadsBatch implements Database.Batchable<SObject> {
    private final String query;

    public LeadsBatch(String leadQuery) {
        this.query = leadQuery;
    }

    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(query);
    }

    public void execute(Database.BatchableContext bc, List<Lead> leads) {
        for (Lead l : leads) {
            try {
                LeadProcessingResult result = LeadService.getQualificationScore(l);
                if (result.isSuccess) {
                    l.Score__c = result.qualificationScore;
                }
            } catch (Exception e) {
                Triton.error(Triton.t.area(TritonTypes.Area.LeadConversion).exception(e));
            }
        }

        try {
            update leads;
        } catch (Exception e) {
            Triton.error(Triton.t.area(TritonTypes.Area.LeadConversion).exception(e));
        }

        Triton.flush();   // publish this chunk's logs before the transaction ends
    }

    public void finish(Database.BatchableContext bc) {}
}
```

{% endcode %}

`Triton.error(...)` **buffers** the exception at `ERROR` level (and flags it for issue creation); the single `Triton.flush()` at the bottom publishes the whole chunk's worth of logs in one platform event. That's the win over logging inline — a batch with hundreds of iterations still costs one event per chunk instead of one per record.

{% hint style="info" %}
**Why buffer at all?** Every published log consumes a platform-event allocation. Buffering lets a chunk's logs go out together. See [Buffering, Flushing & Deferred Publishing](/core-concepts/buffering-flushing.md).
{% endhint %}

## Grouping every iteration under one transaction

The basic version publishes each chunk independently, so the logs aren't tied together. To see the *whole job* as one navigable tree, you need every chunk to share a transaction id — and that means preserving the id across chunks with `Database.Stateful`.

The pattern: start the transaction once (in the constructor), stash the id in a stateful field, and `resumeTransaction(id)` at the top of every entry point so that chunk's logs adopt the shared id.

{% code title="Batch + shared transaction" lineNumbers="true" %}

```apex
public class LeadsBatch implements Database.Batchable<SObject>, Database.Stateful {
    private final String query;
    private String transactionId;   // preserved across chunks by Database.Stateful
    private Integer iteration = 0;

    public LeadsBatch(String leadQuery) {
        this.query = leadQuery;
        this.transactionId = Triton.startTransaction();
        Triton.setTemplate(Triton.makeBuilder()
            .type(TritonTypes.Type.Backend)
            .area(TritonTypes.Area.LeadConversion));
    }

    public Database.QueryLocator start(Database.BatchableContext bc) {
        Triton.resumeTransaction(transactionId);
        Triton.info(Triton.t.summary('LeadsBatch starting').details('Query: ' + query));
        Triton.flush();
        return Database.getQueryLocator(query);
    }

    public void execute(Database.BatchableContext bc, List<Lead> leads) {
        Triton.resumeTransaction(transactionId);
        iteration++;
        Triton.fine(Triton.t
            .summary('Executing iteration ' + iteration)
            .details('Processing ' + leads.size() + ' leads'));

        for (Lead l : leads) {
            try {
                LeadProcessingResult result = LeadService.getQualificationScore(l);
                if (result.isSuccess) {
                    l.Score__c = result.qualificationScore;
                } else {
                    Triton.fine(Triton.t
                        .summary('No qualification score for ' + l.Id)
                        .details(String.valueOf(result)));
                }
            } catch (Exception e) {
                Triton.error(Triton.t.exception(e));
            }
        }

        try {
            update leads;
        } catch (Exception e) {
            Triton.error(Triton.t.exception(e));
        }

        Triton.flush();
    }

    public void finish(Database.BatchableContext bc) {
        Triton.resumeTransaction(transactionId);
        Triton.info(Triton.t
            .summary('LeadsBatch finished')
            .details('Iterations processed: ' + iteration));
        Triton.flush();
    }
}
```

{% endcode %}

What's doing the work here:

* **`Database.Stateful`** preserves `transactionId` between chunks — the static `Triton.TRANSACTION_ID` doesn't survive on its own.
* **`Triton.resumeTransaction(transactionId)`** at the top of `start`, `execute`, and `finish` re-adopts the shared id so every log lands under the same parent.
* **A template** (`Triton.setTemplate(...)`) stamps the common `type`/`area` onto every `Triton.t`, so each log only spells out what's unique. Templates get the full treatment in [Full Control with TritonBuilder](/common-apex-usage-patters/full-control-with-tritonbuilder.md).
* **Log levels earn their keep** — routine tracing sits at `fine`, milestones at `info`, failures at `error`. Turn the noise up in a sandbox and down in production without touching this code. See [Log Levels](/common-apex-usage-patters/log-levels.md).

## Timing each chunk

Once the transaction id is threaded through, wrapping a chunk in a [performance mark](/common-apex-usage-patters/performance-marks.md) costs two lines and gives you a timed span per chunk — plus automatic parenting for every log inside it:

{% code lineNumbers="true" %}

```apex
public void execute(Database.BatchableContext bc, List<Lead> leads) {
    Triton.resumeTransaction(transactionId);
    Triton.startMark();                    // → 'LeadsBatch.execute'
    try {
        // ... the loop from above ...
    } finally {
        Triton.endMark();
        Triton.flush();
    }
}
```

{% endcode %}

`Triton.startMark()` takes no arguments — the phase *is* the method name, so it derives `LeadsBatch.execute` from the stack trace. There's no batch-specific mark method to learn, and no `clearMarks()` needed at the top of the chunk: a fresh execution context already starts with an empty mark stack, so each chunk becomes its own subtree under the shared transaction.

{% hint style="warning" %}
**Do** flush at the end of every `execute()`. **Don't** buffer across chunks expecting `finish()` to flush them — by then those chunk transactions are over and their buffers are gone.
{% endhint %}

Log in every method that has real logic, not just `execute()`. A one-line `start()` doesn't need it; a `start()` that builds a dynamic query absolutely does.
