Trace's Four Lifecycle Methods

Apex Stem Docs
Apex StemApexTraceLoggingSalesforceApex
How to instrument a Usecase with Trace: why the instance field is created once, how start / log / skip / abort / finish divide the work, and what the three exit paths buy you.

Trace's Four Lifecycle Methods

The Trace class exposes four lifecycle methods that express the flow of a Usecase.

Hold it in an instance field

Create the Trace exactly once, as an instance field on the Usecase class.

APEX
public with sharing class CopyAccountIndustryToOpportunityUsecase {
  private Trace t = Trace.of('Copy the parent account industry to the opportunity');
  // ...
}

Why once, and why an instance field, is covered in Nested traces and the two modes. For now it is enough to remember: one Trace per Usecase.

Four methods, three exit paths

MethodPurposeWhen to call it
t.start()Processing beginsAt the top of invoke()
t.log(msg)An intermediate noteAnywhere, any number of times
t.skip(msg)Exit by skippingRight before an early return (nothing to process, etc.)
t.abort(msg)Exit abnormallyWhen bailing out on an exception or a business error
t.finish(msg)Exit normallyAt the end of invoke()

log / skip / abort / finish all have a no-argument overload. When there is no message worth keeping — you just want to close the context — call t.finish(). That fits cases where only the path (skip / finish / abort) matters and no extra log line is needed.

The important part is the three exit paths.

  • finish: the work completed normally
  • skip: nothing to do (no targets, conditions not met) and it exited normally
  • abort: it did not complete, because of an exception or a business error

Keep these three distinct and TraceFlow can later assert which path a Usecase took.

Example

Here is CopyAccountIndustryToOpportunityUsecase, viewed through the lens of Trace.

APEX
public with sharing class CopyAccountIndustryToOpportunityUsecase {
  @TestVisible static final String LBL_FETCH = 'oppFetch';
  @TestVisible static final String LBL_UPDATE = 'oppUpdate';
 
  private final Set<Id> opportunityIds;
  private final IEloquent eloquent;
  private Trace t = Trace.of('Copy the parent account industry to the opportunity');
 
  // (constructors omitted — see step 3 of the introduction guide)
 
  public void invoke() {
    this.t.start();
 
    if(this.opportunityIds == null || this.opportunityIds.isEmpty()) {
      this.t.skip('No target opportunities, exiting.');
      return;
    }
 
    Scribe oppScribe = Scribe.of(Opportunity.class)
      .field('Id')
      .parentField(Scribe.asParent('AccountId').field('Industry'))
      .whereIn('Id', this.opportunityIds);
    List<IEntry> oppEntries = this.eloquent.label(LBL_FETCH).get(oppScribe);
 
    for(IEntry oppEntry : oppEntries) {
      IEntry accountEntry = oppEntry.getParent('AccountId');
      oppEntry.put('Industry__c', accountEntry.get('Industry'));
    }
    this.eloquent.label(LBL_UPDATE).doUpdate(oppEntries);
 
    this.t.finish('Copied the industry onto ' + oppEntries.size() + ' opportunities.');
  }
}

This Usecase ends on two paths: skip when the target set is empty, finish once the work is done. The next chapter shows how to tell those two apart in a test.