> 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/triton-best-practices/implementation-patterns.md).

# Implementation Patterns

Copy-ready logging patterns for the contexts you actually work in — triggers, queueables, batches, REST, schedulers, LWC, and Flow — all on the 2.0 static API.

The principles from the earlier pages are the same everywhere; what changes is where the "unit of work" begins and ends and where you can safely flush. This page walks the contexts you'll actually meet, each as a small, correct, copy-ready pattern on the 2.0 static, builder-first API.

## Where logs go, regardless of context

Before the specifics, the general skeleton. In any unit of work, you want logs at:

* **Entry** — start/resume the transaction, set the template, log intent at `INFO`.
* **Milestones** — key steps with counts and durations (`INFO`/`DEBUG`).
* **Decision points** — which branch you took (`DEBUG`/`FINE`).
* **External calls** — before/after callouts, with a sanitized payload.
* **Errors** — `.exception(e)`, published immediately with `logNow`.
* **Exit** — a result summary, then `flush()` (ideally in `finally`).

## Trigger handler

Set the template in the handler entry point. You don't need to hand-capture the trigger operation or timestamp — for Apex logs, Triton stamps the execution context (operation type, sObject, batch size) automatically.

{% code lineNumbers="true" %}

```apex
public with sharing class AccountTriggerHandler {
    public static void beforeUpdate(List<Account> newList, Map<Id, Account> oldMap) {
        Triton.setTemplate(Triton.makeBuilder()
            .category(TritonTypes.Category.Apex)
            .type(TritonTypes.Type.AccountTrigger)
            .area(TritonTypes.Area.Accounts));

        Set<Id> scopeIds = new Map<Id, Account>(newList).keySet();

        Triton.info(Triton.t
            .summary('Account before update')
            .details('count=' + scopeIds.size())
            .relatedObjects(scopeIds));

        // ... handler logic ...

        Triton.flush();   // triggers are their own transaction — flush before you exit
    }
}
```

{% endcode %}

## Queueable

An async job is a fresh transaction. If it's part of a larger trace, resume the id you were constructed with; otherwise start one. Flush in `finally`.

{% code lineNumbers="true" %}

```apex
public class PriceSyncQueueable implements Queueable, Database.AllowsCallouts {
    private final Set<Id> productIds;
    private final String txId;

    public PriceSyncQueueable(Set<Id> productIds, String txId) {
        this.productIds = productIds;
        this.txId = txId;
    }

    public void execute(QueueableContext qc) {
        if (String.isNotBlank(txId)) Triton.resumeTransaction(txId);
        else Triton.startTransaction();

        Triton.setTemplate(Triton.makeBuilder()
            .category(TritonTypes.Category.Apex)
            .area(TritonTypes.Area.OpportunityManagement)
            .relatedObjects(productIds));

        Long started = System.now().getTime();
        try {
            // ... callouts and DML ...
            Triton.info(Triton.t
                .summary('Price sync complete')
                .details('synced=' + productIds.size())
                .duration(System.now().getTime() - started));
        } catch (Exception e) {
            Triton.logNow(Triton.t.exception(e));
            throw e;
        } finally {
            Triton.flush();
            Triton.stopTransaction();
        }
    }
}
```

{% endcode %}

## Batchable

The key subtlety: **each `execute` is its own transaction**, so flush inside every chunk, not once at the end. Use `Database.Stateful` if you need to carry a counter or the transaction id across chunks.

{% code lineNumbers="true" %}

```apex
global class LeadQualifyBatch implements Database.Batchable<SObject>, Database.Stateful {

    global Database.QueryLocator start(Database.BatchableContext bc) {
        Triton.startTransaction();
        Triton.setTemplate(Triton.makeBuilder()
            .category(TritonTypes.Category.Apex)
            .area(TritonTypes.Area.LeadConversion));
        Triton.info(Triton.t.summary('Lead qualify started'));
        Triton.flush();
        return Database.getQueryLocator([SELECT Id, Status FROM Lead WHERE IsConverted = FALSE]);
    }

    global void execute(Database.BatchableContext bc, List<Lead> scope) {
        Long t0 = System.now().getTime();
        try {
            // ... work ...
            Triton.info(Triton.t
                .summary('Executed batch chunk')
                .details('size=' + scope.size())
                .duration(System.now().getTime() - t0)
                .relatedObjects(new Map<Id, Lead>(scope).keySet()));
        } catch (Exception e) {
            Triton.error(Triton.t
                .exception(e)
                .relatedObjects(new Map<Id, Lead>(scope).keySet()));
            // continue or rethrow per your design
        } finally {
            Triton.flush();   // publish this chunk's logs before the next transaction
        }
    }

    global void finish(Database.BatchableContext bc) {
        Triton.info(Triton.t.summary('Lead qualify finished'));
        Triton.flush();
        Triton.stopTransaction();
    }
}
```

{% endcode %}

For a deeper treatment (stateful buffering, log grouping across chunks), see [Common Apex Usage → Batch Logging](/common-apex-usage-patters/batch-logging.md).

## REST resource

Callout-style entry point. It's still an ordinary `Apex` log — what makes it an integration log is the request/response payload you attach with `.integrationPayload(...)`, which accepts `RestRequest`/`RestResponse` directly.

{% code lineNumbers="true" %}

```apex
@RestResource(urlMapping='/api/accounts/*')
global with sharing class AccountRestAPI {
    @HttpGet
    global static void getAccount() {
        Triton.startTransaction();
        Triton.setTemplate(Triton.makeBuilder()
            .category(TritonTypes.Category.Apex)
            .area(TritonTypes.Area.RestAPI));

        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;
        try {
            String accountId = req.requestURI.substring(req.requestURI.lastIndexOf('/') + 1);
            // ... process ...
            Triton.info(Triton.t
                .summary('REST GET handled')
                .relatedObject(accountId)
                .integrationPayload(req, res));
        } catch (Exception e) {
            Triton.logNow(Triton.t.exception(e).integrationPayload(req, res));
            throw e;
        } finally {
            Triton.flush();
            Triton.stopTransaction();
        }
    }
}
```

{% endcode %}

More on integration logging: [Common Apex Usage → Apex Rest Logging](/common-apex-usage-patters/apex-rest-logging.md) and [Integration Logs](/common-apex-usage-patters/integration-logs.md).

## Scheduled job

{% code lineNumbers="true" %}

```apex
global class DataSyncScheduler implements Schedulable {
    global void execute(SchedulableContext sc) {
        Triton.startTransaction();
        Triton.setTemplate(Triton.makeBuilder()
            .category(TritonTypes.Category.Apex)
            .area(TritonTypes.Area.OpportunityManagement));
        try {
            Triton.info(Triton.t.summary('Data sync scheduled job started'));
            // ... enqueue / sync ...
            Triton.info(Triton.t.summary('Data sync scheduled job completed'));
        } catch (Exception e) {
            Triton.logNow(Triton.t.exception(e));
            throw e;
        } finally {
            Triton.flush();
            Triton.stopTransaction();
        }
    }
}
```

{% endcode %}

## LWC — with `@wire`

For components with `@wire`, initialize in the constructor so early wire callbacks are captured. Log both the success and error branches.

{% code lineNumbers="true" %}

```javascript
import Triton, { TYPE, AREA } from 'c/triton';
import getCases from '@salesforce/apex/CaseController.getCases';
import { wire } from 'lwc';

export default class CaseList extends LightningElement {
    triton;

    constructor() {
        super();
        this.triton = new Triton().bindToComponent('caseList');
        this.triton.setTemplate(
            this.triton.makeBuilder().type(TYPE.FRONTEND).area(AREA.COMMUNITY)
        );
    }

    @wire(getCases, { contactId: '$recordId' })
    wiredCases({ data, error }) {
        if (data) {
            this.triton.log(
                this.triton.fromTemplate()
                    .summary(`Wire loaded ${data.length} cases`)
                    .relatedObjects([this.recordId])
            );
        } else if (error) {
            this.triton.logNow(
                this.triton.fromTemplate()
                    .exception(error)
                    .summary('Wire error loading cases')
                    .relatedObjects([this.recordId])
            );
        }
    }
}
```

{% endcode %}

## LWC — imperative

{% code lineNumbers="true" %}

```javascript
async loadOrders() {
    const txId = this.triton.startTransaction();
    try {
        const data = await loadOrders({ accountId: this.recordId, txId });
        this.triton.log(this.triton.fromTemplate().summary(`Orders loaded: ${data.length}`));
    } catch (e) {
        this.triton.logNow(this.triton.fromTemplate().exception(e).summary('Load orders failed'));
    } finally {
        this.triton.stopTransaction();   // flushes remaining logs
    }
}
```

{% endcode %}

The LWC logger surface and lifecycle guidance live in [Beyond Apex → LWC](/beyond-apex/lwc.md).

## Flow

Flow logging is declarative: drop the Triton **`Add Log`** invocable action at the points that matter. The principles carry straight over — log at the start, at decisions, around DML/callouts, and in fault paths.

The two rules that make Flow logging pay off:

* **Always pass `$Flow.InterviewGuid`** to every `Add Log` action. It's what groups a Flow's logs together and lets Triton attach any unhandled Flow exception to the same trace automatically.
* **Use constants and text templates** for `Area`/`Category`/`Type` and for detail strings, so a taxonomy change is a one-place edit.

The complete Flow setup — configuring parameters, naming conventions, additional fields — is in [Beyond Apex → Flows](/flows.md).

## How logs reach Pharos

A couple of things worth knowing about the plumbing behind these patterns:

* **Platform events.** Buffered Apex logs publish as a batch of platform events on `flush()`, with automatic backoff when allocations run low. Nothing to configure.
* **Issue creation.** `.exception(e)` (and `.error()`) flag the log for **Pharos Issue** creation. That's exactly what you want for genuine failures — and exactly what you *don't* want on routine warnings, or you'll create alert fatigue. Reserve issue-flagging (i.e., error-level exception logs and explicit `.createIssue()`) for things a human should triage.
* **LWC bridge.** LWC logs batch to the server via an `@AuraEnabled` bridge; the same buffer-then-flush discipline applies, with `logNow` for the ones you can't lose.

## Related reading

* The buffer/flush discipline these patterns rely on: [Buffering & Flushing](/triton-best-practices/buffering-and-flushing.md).
* Correlating them across boundaries: [Transaction Management](/triton-best-practices/transaction-management.md).
* Making each log carry enough context: [Context & Related Objects](/triton-best-practices/context-and-related-objects.md).
