> 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/end-to-end.md).

# End-to-End Logging

How Triton stitches one user action — starting in an LWC, hopping into Apex, and fanning out into a Flow — into a single correlated trace.

A single click in the UI almost never stays in one context. The user presses a button in a Lightning Web Component, that calls an Apex controller, the controller commits a record, and the DML kicks off an autolaunched Flow. Four contexts, one intent — and, without help, four disconnected piles of logs.

This section is about making those four piles into **one story**. Triton correlates logs across LWC, Apex, and Flow using a shared **transaction id**, so when you open a log in Pharos you can follow the whole chain from the button press to the Flow that failed three hops later.

Three pages make up this section:

* **This page** — the full LWC → Apex → Flow walkthrough.
* [Transactions & Platform Cache](/end-to-end/transactions-and-cache.md) — how the transaction id is created, resumed, and shared across contexts (including `Triton.withCache()` and `TritonCacheManager`).
* [Spans & Distributed Tracing](/end-to-end/spans-and-tracing.md) — how spans (`spanId` / `parentSpanId`) add parent-child structure *within* a transaction, and how that maps to OpenTelemetry concepts.

If you haven't yet, skim [Logging in LWC](/beyond-apex/lwc.md) and [Logging in Flow](/flows.md) first — this page assumes you know the basics of each and focuses on wiring them together.

## The scenario

We'll trace one interaction all the way through:

1. A user clicks a button in an LWC.
2. The LWC calls an Apex controller.
3. The controller performs DML.
4. The DML fires an autolaunched Flow, which logs its own progress.

Our goal: **every log from all four steps shares one transaction id**, so they group together in Pharos.

{% code lineNumbers="true" %}

```
Transaction: 7fb8c320-e8f9-4f1b-9742-1a5768b9e0d2
├── LWC        "Flow Demo - Start"        (Frontend)
├── Apex       "Performing DML"           (Backend)
└── Flow       Add Log actions            (Flow)
    └── Unhandled Flow error              (auto-captured)
```

{% endcode %}

## What "correlation" actually means

The correlation id is the transaction id. In Apex it lives statically on `Triton.TRANSACTION_ID`; in LWC it lives on your logger instance as `this.triton.transactionId`; in Flow it rides along as the **Transaction ID** parameter of the Add Log action. When two logs carry the same transaction id, Pharos groups them.

The interesting question is how that one id makes it from the browser, into Apex, and on into the Flow. There are two ways to do it, and they're not mutually exclusive:

| Approach             | How the id travels                                                                                                                                       | Best for                                                                            |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| **Explicit passing** | You read `this.triton.transactionId` in the LWC and pass it to Apex as a parameter; Apex calls `Triton.resumeTransaction(id)`.                           | Simple, linear call paths where you control every hop.                              |
| **Platform cache**   | `Triton.withCache()` stashes the id in platform **session** cache; later Apex/Flow in the same session resume it automatically — no parameter threading. | Multi-component or multi-call scenarios where passing the id everywhere is painful. |

Both produce the exact same grouped result. The rest of this page walks each one. For the full mechanics of transactions and the cache, see [Transactions & Platform Cache](/end-to-end/transactions-and-cache.md).

{% hint style="info" %}
**Why does this even work across the DML → Flow boundary?** The autolaunched Flow runs **inside the same Apex transaction** as your controller's DML. Because Triton keeps `TRANSACTION_ID` as static state for the life of that Apex transaction, the Flow's Add Log action inherits the id automatically — you don't have to pass it. The two approaches below are really about getting the id from the *browser* into *Apex* in the first place.
{% endhint %}

## Approach 1 — Explicit parameter passing

The most transparent option: read the transaction id in the LWC and hand it to Apex directly.

### The LWC

{% code lineNumbers="true" %}

```javascript
// logDemoDirect.js
import apexActionThatTriggersFlow from '@salesforce/apex/LogDemoDirectController.apexActionThatTriggersFlow';
import Triton, { AREA, TYPE } from 'c/triton';

export default class LogDemoDirect extends LightningElement {
    triton;

    connectedCallback() {
        this.triton = new Triton().bindToComponent('logDemoDirect');
    }

    async executeScenario() {
        // Log the start of the interaction from the UI
        await this.triton.logNow(
            this.triton.debug(TYPE.FRONTEND, AREA.ACCOUNTS)
                .summary('Flow Demo - Start')
                .details('Starting execution with explicit id passing')
        );

        try {
            // Hand the current transaction id to Apex
            await apexActionThatTriggersFlow({
                transactionId: this.triton.transactionId
            });
        } catch (error) {
            await this.triton.logNow(this.triton.exception(error));
        }
    }
}
```

{% endcode %}

`this.triton.transactionId` is the id the LWC logger generated (or resumed) for this session. Passing it as a plain method argument is all it takes.

### The Apex controller

{% code lineNumbers="true" %}

```apex
public with sharing class LogDemoDirectController {
    @AuraEnabled
    public static void apexActionThatTriggersFlow(String transactionId) {
        // Adopt the id the LWC already established
        Triton.resumeTransaction(transactionId);

        Triton.debug(Triton.t
            .type(TritonTypes.Type.Backend)
            .area(TritonTypes.Area.Community)
            .summary('Flow Demo - Apex')
            .details('Performing DML to trigger flow'));

        try {
            List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 1];
            if (!accounts.isEmpty()) {
                accounts[0].Name = 'Flow Demo';
                update accounts;   // ← triggers the autolaunched Flow
            }
        } catch (Exception e) {
            Triton.error(Triton.t.area(TritonTypes.Area.Community).exception(e));
        }

        Triton.flush();   // publish the buffered Apex logs
    }
}
```

{% endcode %}

Two things to notice:

* `Triton.resumeTransaction(transactionId)` sets `TRANSACTION_ID` to the LWC's id. From here, every Apex log — and every log the Flow produces during the `update` — carries that id.
* We buffer with `Triton.debug(...)` / `Triton.error(...)` and publish once with `Triton.flush()`. See [Buffering, Flushing & Deferred Publishing](/core-concepts/buffering-flushing.md) for why that's the right default.

## Approach 2 — Platform cache

When threading a parameter through every layer gets tedious, let the platform cache carry the id for you.

### The LWC

The LWC barely changes — it doesn't need to pass anything:

{% code lineNumbers="true" %}

```javascript
// logDemoCache.js
import apexActionThatTriggersFlow from '@salesforce/apex/LogDemoCacheController.apexActionThatTriggersFlow';
import Triton, { AREA, TYPE } from 'c/triton';

export default class LogDemoCache extends LightningElement {
    triton;

    connectedCallback() {
        this.triton = new Triton().bindToComponent('logDemoCache');
    }

    async executeScenario() {
        await this.triton.logNow(
            this.triton.debug(TYPE.FRONTEND, AREA.ACCOUNTS)
                .summary('Flow Demo - Start')
                .details('Starting execution with platform cache')
        );

        try {
            // No transaction id argument — the cache handles it
            await apexActionThatTriggersFlow();
        } catch (error) {
            await this.triton.logNow(this.triton.exception(error));
        }
    }
}
```

{% endcode %}

{% hint style="info" %}
When the LWC's logs are saved, `TritonLwc.saveComponentLogs` resumes the LWC's transaction id **and** calls `Triton.withCache()` — which writes that id into platform Org cache. That's what makes it available to a later Apex call without any parameter passing. The full sequence is spelled out in [Transactions & Platform Cache](/end-to-end/transactions-and-cache.md#how-the-id-travels-lwc-apex-flow).
{% endhint %}

### The Apex controller

{% code lineNumbers="true" %}

```apex
public with sharing class LogDemoCacheController {
    @AuraEnabled
    public static void apexActionThatTriggersFlow() {
        // Resume the id from platform cache (or start one if none is cached)
        Triton.withCache();

        Triton.debug(Triton.t
            .type(TritonTypes.Type.Backend)
            .area(TritonTypes.Area.Community)
            .summary('Flow Demo - Apex')
            .details('Performing DML to trigger flow'));

        try {
            List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 1];
            if (!accounts.isEmpty()) {
                accounts[0].Name = 'Flow Demo';
                update accounts;
            }
        } catch (Exception e) {
            Triton.error(Triton.t.area(TritonTypes.Area.Community).exception(e));
        }

        Triton.flush();
    }
}
```

{% endcode %}

`Triton.withCache()` does the work: if a transaction id is sitting in Org cache, it resumes it; if not, it starts a fresh one and caches it. Either way, the Flow triggered by the `update` inherits the same id.

## The Flow leg

The Flow side is exactly as described in [Logging in Flow](/flows.md) — add the **Add Log** action, pass `$Flow.InterviewGuid` as the Interview GUID, and configure Area / Summary / Details. You do **not** need to set the **Transaction ID** parameter for this scenario: because the Flow runs inside the same Apex transaction that already called `resumeTransaction` or `withCache`, it picks up `TRANSACTION_ID` automatically.

{% hint style="info" %}
**Set the Transaction ID parameter only when the Flow starts the chain**, or when you're correlating a Flow that runs in a *separate* transaction from the Apex that set the id. In that case, pass the id into the Flow and map it to the Add Log action's **Transaction ID** — it will `resumeTransaction` for you.
{% endhint %}

### Flow logs publish a little later

There's one behavioral detail worth knowing for end-to-end debugging. Flow logs aren't published inline like Apex logs. The `Add Log` invocable **drains** Triton's buffer and hands the records to `TritonFlowFlushBatch`, a singleton batch that publishes them shortly after (buffered in Org cache, flushed via a short-delayed batch). This keeps high-volume Flows from burning platform-event allocations on the hot path.

The practical upshot: **the LWC and Apex logs may appear in Pharos a beat before the Flow logs.** They'll all land under the same transaction id — the Flow leg just arrives fashionably late. This is the `drain()` path described in [Buffering, Flushing & Deferred Publishing](/core-concepts/buffering-flushing.md).

## Unhandled Flow errors come along for free

If the Flow throws an **unhandled** error, you don't need a fault path to capture it. Because you passed `$Flow.InterviewGuid` to your Add Log action, Pharos correlates the automatically-captured Flow error with your transaction. Let's force one by clearing a required field:

{% code lineNumbers="true" %}

```apex
public with sharing class LogDemoCacheController {
    @AuraEnabled
    public static void apexActionThatTriggersFlow() {
        Triton.withCache();

        Triton.debug(Triton.t
            .type(TritonTypes.Type.Backend)
            .area(TritonTypes.Area.Community)
            .summary('Flow Demo - Apex')
            .details('Triggering a flow that will fail'));

        try {
            List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 1];
            if (!accounts.isEmpty()) {
                accounts[0].Name = null;   // Flow validation will reject this
                update accounts;
            }
        } catch (Exception e) {
            Triton.error(Triton.t.area(TritonTypes.Area.Community).exception(e));
        }

        Triton.flush();
    }
}
```

{% endcode %}

The resulting trace in Pharos:

{% code lineNumbers="true" %}

```
Transaction: 7fb8c320-e8f9-4f1b-9742-1a5768b9e0d2
├── LWC   "Flow Demo - Start"
├── Apex  "Flow Demo - Apex"
└── Flow
    ├── Add Log      (start)
    └── Unhandled Flow error   ← captured automatically via Interview GUID
        └── Interview GUID, current element, message, stack trace
```

{% endcode %}

## Do / Don't

{% hint style="success" %}
**Do**

* **Log the very first action from the UI** so the transaction has a clear origin point.
* **Pick one propagation strategy per chain** — explicit passing *or* cache — and stay consistent so the id's journey is easy to follow.
* **Always pass `$Flow.InterviewGuid`** to Add Log actions so unhandled Flow errors correlate.
* **Flush your Apex logs** before returning from the controller (or use `Triton.logNow` for critical ones).
* **Expect the Flow leg to arrive slightly later** — that's the deferred batch working as designed.
  {% endhint %}

{% hint style="danger" %}
**Don't**

* **Don't set the Flow's Transaction ID parameter** when the Flow is downstream of Apex in the same transaction — it already inherits the id, and overriding it can split the trace.
* **Don't assume the cached id crosses user boundaries.** Org cache works in async and system contexts, but the id is keyed per user — a context running as a different user won't resume it. Pass the id explicitly there. See [Transactions & Platform Cache](/end-to-end/transactions-and-cache.md#when-the-cache-is-not-available).
* **Don't call `Triton.startTransaction()` mid-chain** — it mints a *new* id and disconnects everything logged afterward.
* **Don't hand-roll type/summary/stack trace for exceptions** — `Triton.t.exception(e)` does all of it.
  {% endhint %}

## Where to go next

* [Transactions & Platform Cache](/end-to-end/transactions-and-cache.md) — the mechanics behind `resumeTransaction`, `withCache`, and the cache manager.
* [Spans & Distributed Tracing](/end-to-end/spans-and-tracing.md) — add parent-child structure inside a single transaction.
* [Logging in LWC](/beyond-apex/lwc.md) · [Logging in Flow](/flows.md) · [Logging in Apex](/common-apex-usage-patters.md)
* Coming from 1.x? The transaction methods are unchanged in spirit but now static — see [Migration](/migration.md).
