Execution Order & Cost
What one TriggerOrchestrator.run(…) does, in order, and what it costs.
One Run per Chunk
The platform fires a trigger once per context for each chunk of up to 200 records. Each firing is one run. An insert of 201 records makes four runs: before insert and after insert for the first 200 records, then both again for the last one.
Each run calls <ctx>Handlers(), loads parents, runs providers and commits again. Handlers created in <ctx>Handlers() start with empty instance fields. Static fields, TriggerOrchestrator.bypass() switches and recursion counts last the whole transaction.
Order of a Run
- Run switches.
TriggerOrchestrator.bypass()(all(),sObject(…),orchestrator(…)) or a checkedTriggerObject__mdt.Bypass__cends the run before any of your code. So does an orchestrator that does not implement the context. - Handler list.
<ctx>Handlers()runs. A class that implements two roles runs only as the first: Populator over Validator, Writer over Dispatcher. - Handler switches.
bypass().handler(…), thenTriggerHandler__mdt.Bypass__c, thenbypassOn<Ctx>When(). A skipped handler declares no parents. - Parents load. One load for all remaining handlers, before the first handler runs.
- Each handler runs, in list order.
- Its RelatedQuery providers run over all records.
- Per record, the predicate decides and the action runs. A Dispatcher collects the records and dispatches once.
- The Finalizer runs when at least one record qualified.
- A Writer with OwnUnitOfWork or ContinueOnError commits its own unit of work.
- Shared commit. In after contexts, the shared unit of work commits once.
- Logger. The outermost run calls
finalize()on the org's Logger.
Query Cost
SOQL queries a run spends outside your code:
| Source | SOQL queries |
|---|---|
| Bypass metadata | 0, once per transaction |
| Logger discovery | 1, once per transaction |
| Parents in before contexts and after delete | 1 per declared lookup, per run |
| Parents in after insert, update and undelete | 1 on the trigger object per run, plus 1 per lookup it missed and, in after update, per PriorParentQuery lookup |
| Parents after a Populator | 1 per lookup re-pointed to a parent not loaded yet |
| RelatedQuery providers | what each query runs, per handler |
- Declarations merge. Handlers that declare the same lookup share one query.
- Parents and providers run before the first predicate, even when no record qualifies.
- Never query in a predicate or an action. They run once per record. Declare a ParentQuery or a RelatedQuery, or query once in a Finalizer.
DML Cost
- Before insert and before update: none. DML or an event publish throws.
- Shared unit: one commit per run. It costs one statement per operation and object type. An empty unit costs nothing.
- A Writer with OwnUnitOfWork or ContinueOnError: one more commit per run, when a record qualified.
- Direct DML in a Dispatcher, a before delete handler or a Finalizer costs what it runs.
- Events: a Publish After Commit event counts as a DML statement. A Publish Immediately event counts toward
Limits.getPublishImmediateDML().
See Unit of Work.
Nested Runs
A commit or direct DML fires the triggers of the saved records at once. Each of them is a full nested run with its own parents, providers and commit.
- Recursion counts are shared. In the update contexts, a Populator, Writer or Dispatcher skips a record after acting on it 3 times in the transaction. Change the limit with a RecursionGuard.
- Only the outermost run calls
finalize(). - Salesforce allows 16 levels. The next one fails with "maximum trigger depth exceeded".
