Process Automation & Logic PD1 · Largest section · 30% of exam

Overview

This is the biggest section on PD1 — 30% of the exam, covering Apex language fundamentals, triggers, async processing, and the declarative-vs-programmatic decision that runs through almost every scenario question. It's dense, but the traps repeat: bulkification, Flow ordering, and "which tool is correct" judgment calls.

30%Of the whole exam
1Trigger per object (best practice)
2Master-Detail max per object
25Roll-up fields max per object

Exam checklist

  • Flow types & before-save vs after-save ordering · Flow-vs-Apex decision criteria · declarative-first principle
  • Apex OOP: access modifiers, static/final, interfaces vs abstract classes, enums, Comparable
  • Collections: List/Set/Map gotchas, sObject casting, Iterable/Iterator
  • Trigger recursion prevention, handler pattern, undefined multi-trigger order
  • Async Apex + Invocable Methods (the Flow ↔ Apex bridge, bulkification contract)
  • Formula vs Roll-Up Summary (and what to do when neither works) · Custom Metadata/Settings/Labels · Platform Events vs CDC
Top trapsProcess Builder/Workflow Rules are legacy — Flow is the current answer for new automation · trigger order across multiple triggers on the same object+event is undefined · Roll-Up Summary works only on Master-Detail, never Lookup · @InvocableMethod parameters are always a List, even for "one record" · LimitException generally can't be usefully caught-and-continued.

Declarative Automation (Flow) & Order of Execution

High-yield

Flow is the current answer whenever declarative automation is possible. Process Builder and Workflow Rules are legacy — still functional in older orgs, but never the "recommended" answer on a current PD1 exam.

Flow types

TypeTriggered by
Screen FlowUser interaction — launched from Lightning pages, utility bar, Experience Cloud. Only type with Screen elements.
Record-Triggered FlowCreate / update / delete of a record — before-save or after-save.
Scheduled FlowRuns on a schedule against a batch of records (Get Records + filters) — declarative equivalent of simple Scheduled Apex.
Autolaunched Flow (no trigger)Invoked by Apex, another Flow (subflow), REST API, or Einstein Bots.
Platform Event–Triggered FlowSubscribes to a platform event channel, runs when an event message arrives.
Retirement statusProcess Builder and Workflow Rules are officially legacy — Salesforce blocks creating new ones in current orgs and provides "Migrate to Flow" tooling. If a question asks "what's the recommended tool for new automation," the answer is Flow, never Process Builder or Workflow Rule.

Full order of execution — with Flow's exact position ⭐

Before-save Flow Before trigger System validation + custom rules Save (not committed) After trigger After-save Flow Assignment / auto-response / workflow rules Escalation / entitlement rules Roll-up / cross-object workflow Sharing recalc Commit Post-commit async
TrapBefore-save flows run before before-triggers. Validation rules run after before-triggers, before the save. If validation fails, the whole save aborts — including any in-memory writes the before-save flow and before-trigger already made.

Before-save vs after-save flow ⭐

Before-save flowAfter-save flow
Can only update fields on the triggering record itselfCan create/update/delete other records, call invocable Apex
No record Id yet on insertRecord Id available (already committed... in-memory)
~10x faster — no second save round-tripRequires a second DML/save cycle
Cannot send email, post to Chatter, call DML-performing actionsCan do all of the above
Trigger phrase"Most performant way to update a field on the same record, no other DML" → before-save (fast field update) Record-Triggered Flow.

Flow vs Apex — decision criteria

  • Use Flow when: single/simple multi-object field updates, straightforward branching, no complex loops with heavy computation, admin-maintainability matters, requirement fits Flow's element set.
  • Use Apex when: complex logic beyond Flow's practical limits, custom error handling/rollback control, bulk-optimized SOQL/DML, HTTP callouts with complex parsing, recursive/algorithmic logic, or reusable logic invoked from multiple channels (trigger + Flow + batch) via one Apex service class.
Exam biasSalesforce's stated principle: "use the most declarative option that meets the requirement." If Flow can technically do it, Flow is the "correct" exam answer — even if Apex could also do it.

Flow Builder elements to know

  • Data elements: Get Records, Create Records, Update Records, Delete Records (each configurable for filters/sort/how-many)
  • Logic elements: Decision (branching outcomes), Loop (iterates a collection, "for each" + after-last exit), Assignment (set variable values)
  • Screen components: input fields, display text, choice/picklist — Screen Flows only
  • Subflow: calls another flow
Governor limits are sharedFlow and Apex governor limits are cumulative within one transaction — invoking a Flow from Apex (or vice versa) does NOT reset SOQL/DML counters. Multiple record-triggered flows on the same object can now have an explicit Trigger Order (1–2000) configured, rather than relying on undefined ordering.

Apex OOP Fundamentals

Access modifiers — exact rules ⭐

ModifierVisibility
private (default)Only within the defining class (incl. its inner classes)
protectedDefining class + subclasses. Cannot apply to top-level classes or static members
publicAnywhere within the same app/namespace
globalAnywhere, across namespaces/packages, required for cross-package/API-exposed access
TrapsIf a class is global, every method meant to be externally visible must also be explicitly global — a public method inside a global class stays app-internal only. Once a method/class is global in a managed package, it can never be deleted, only deprecated. Sharing keywords (with/without/inherited sharing) are separate from access modifiers entirely.

static, final & the recursion-guard link

  • static members belong to the class, shared across instances, no new needed. Static variables persist for the duration of one transaction — this is exactly why the recursive-trigger guard pattern works.
  • final on a variable = assignable only once. Apex has no final class/final method concept like Java — a plain class is simply non-extensible unless declared virtual or abstract.

Interfaces vs Abstract classes

  • Interface — signatures only, no implementation/state. A class can implement multiple interfaces.
  • Abstract class — can mix implemented + abstract methods, hold state/constructors, but isn't instantiable. A class extends only one abstract class.

Use an interface for an unrelated-classes contract (Comparable, Database.Batchable); use abstract for related classes sharing state (e.g. a trigger handler base class).

Enums, inner classes, constructors & the Comparable interface

public enum Season { WINTER, SPRING, SUMMER, FALL }

public class Employee implements Comparable {
    public Decimal salary;
    public Integer compareTo(Object compareTo) {
        Employee that = (Employee) compareTo;
        return this.salary == that.salary ? 0
             : this.salary > that.salary ? 1 : -1;
    }
}
// employees.sort();  -- now sorts by salary
  • Enums are backed by integers in declaration order; cannot add methods; can't be a generic type parameter.
  • Inner classes can be public/private/virtual/abstract, but not global unless the outer class is global too.
  • Apex has no default/named parameters — overload constructors/methods explicitly instead.
  • Comparable is required to .sort() a List of custom objects — must return negative/0/positive, and must handle nulls explicitly. System.Comparator<T> lets you pass an external comparator instead of modifying the class.

Data Types & Collections

sObject casting

SObject s = new Account(Name = 'Acme');
Account a = (Account) s;              // OK — safe downcast

SObject s2 = new Contact();
Account bad = (Account) s2;           // throws System.TypeException at RUNTIME
                                       // (not a compile error!)

Casting between unrelated sObject types compiles fine but throws at runtime — a classic exam trap for candidates expecting a compile-time catch.

List / Set / Map gotchas

  • list.get(i) throws "List index out of bounds" on an invalid index.
  • map.get(key) returns null (no exception) for a missing key.
  • Two sObject variables compared with .equals() compare all populated fields, not just Id — a common misconception.
  • Lists can be multi-dimensional: List<List<Integer>>.

Iterable / Iterator

  • Custom classes can implement Iterator<T> (hasNext(), next()) to drive a for loop.
  • Iterable<T> is used as an alternative return type for Database.Batchable's start() when the data source isn't SOQL (instead of QueryLocator).

Triggers: Recursion & the Handler Pattern

Recursive trigger prevention (static boolean guard)

public class TriggerHandler {
    public static Boolean isFirstRun = true;
}

trigger AccountTrigger on Account (before update) {
    if (TriggerHandler.isFirstRun) {
        TriggerHandler.isFirstRun = false;
        // logic that might re-fire this trigger via its own DML
    }
}

Works because static variables persist for the whole transaction — a second entry into the trigger within the same transaction sees the flag already flipped.

Why "one trigger per object" is best practice ⭐

  • Best practice: a single thin trigger per object dispatches to a Handler class (beforeInsert(), afterUpdate(), etc.) — keeps logic testable and centralized.
  • The real reason: if multiple triggers exist for the same object + event (e.g., two before update triggers on Account), the order they fire in is undefined/unpredictable by Salesforce.
  • Each trigger still receives the full Trigger.new/Trigger.old list for the whole bulk operation, but a later-firing trigger will see whatever mutations an earlier (unknown-which) trigger already made — pure uncertainty.
Exam wording"What's true about execution order when multiple triggers exist for the same object/event?" → "Order is not guaranteed / not defined by Salesforce." Distractors claiming alphabetical order or creation-date order are always false.

Async Apex & Invocable Methods

The Flow ↔ Apex bridge. Exposing Apex to Flow via @InvocableMethod has a strict, always-bulkified contract.

Async Apex quick recap

TypeUse when
@futureSimple fire-and-forget; primitives only, no chaining
QueueablePreferred — sObjects allowed, chainable, returns Job Id
BatchMillions of records, fresh limits per chunk
ScheduledTime-based; often kicks off a Batch

@InvocableMethod — exact contract ⭐

public class AccountScoreAction {
    @InvocableMethod(label='Score Accounts' description='Returns risk score')
    public static List<Result> scoreAccounts(List<Request> requests) {
        List<Result> results = new List<Result>();
        for (Request r : requests) {
            Result res = new Result();
            res.score = r.revenue > 1000000 ? 'High' : 'Low';
            results.add(res);
        }
        return results;
    }
    public class Request {
        @InvocableVariable(required=true)
        public Decimal revenue;
    }
    public class Result {
        @InvocableVariable
        public String score;
    }
}
  • Method must be public static (or global static), on a top-level class. Only one @InvocableMethod per class.
  • Parameter must be exactly one List<T> — even though each Flow interview logically passes "one record," Flow batches (boxcars) multiple invocations into a single Apex call.
  • Return type is void or a List<T> of a class with @InvocableVariable fields.
Bulkification trapSince the method always receives a List, your code must loop over the entire list with bulk-safe SOQL/DML. Code written assuming requests.size() == 1 will break the moment a Flow runs over 200 records in a loop.

Formula Fields & Roll-Up Summaries

Formula fields

  • Read-only, calculated on read (not stored), can traverse parent relationships (cross-object, dot notation) up to 10 relationships deep.
  • Cannot reference child (one-to-many) records — no aggregation across children.
  • Function categories: logical (IF, CASE), math, text, date/time.

Roll-Up Summary fields ⭐

  • Only definable on the parent side of a Master-Detail relationship — never natively on Lookup.
  • Functions: COUNT, SUM, MIN, MAX of a child field, with optional filter criteria.
  • Max 25 roll-up summary fields per object.

When native roll-up isn't possible — alternatives

  1. Relationship is Lookup, not Master-Detail → use an Apex trigger, a record-triggered Flow that aggregates and updates the parent, or a free tool like DLRS (Declarative Lookup Rollup Summaries).
  2. Need aggregation beyond COUNT/SUM/MIN/MAX (median, weighted average) → must use Apex.
  3. Need real-time recalculation without child DML (e.g., time-based) → a formula field, not a roll-up (roll-ups only recalc on child DML).
Most-repeated trap"Sum an Amount field on a custom object related via Lookup" → Roll-Up Summary is NOT available — this Lookup-vs-Master-Detail distinction is one of the single most reliably tested traps in this whole section.

Config Data & Events

Custom Metadata Type vs Custom Setting vs Custom Label ⭐

Custom Metadata TypeCustom SettingCustom Label
Deployable with metadata?YesNo — org-specific dataYes
Queryable in SOQL?Yes; usable directly in Flow/Validation Rules/FormulasVia getInstance(), no governor-limit hitNo — accessed via System.Label.X
Typical useApp config/business rules that ship with your codeEnvironment-specific values admins tweak per-orgTranslatable UI text
Managed-package featureProtected custom metadata hides data from subscribers
Trigger phrase"Config that must be identical across Dev/Test/Prod and travel with deployment" → Custom Metadata Type. "A value that differs per sandbox without redeploying" → Custom Setting (often Hierarchy type).

Platform Events vs Change Data Capture

  • Platform Events — custom event objects (MyEvent__e), explicitly published via EventBus.publish() (Apex/Flow/REST), consumed by subscribers. Fire-and-forget, pub/sub, decoupled — good for custom business events.
  • Change Data Capture (CDC) — automatically publishes change events (ObjectName__ChangeEvent) whenever a record is created/updated/deleted/undeleted, after commit. You enable it on the object; you don't write publish code.
TrapCDC is not the same as a trigger — CDC is async/decoupled for external consumption; a trigger is synchronous, in-transaction. "Notify external systems whenever ANY change happens to Account" → CDC. "Broadcast a custom business event with your own payload" → Platform Event.

Exception Handling Deep Dive

Custom exceptions & chaining

public class OrderException extends Exception {}

try {
    processOrder();
} catch (DmlException original) {
    throw new OrderException('Order failed', original);   // chaining
}
// later: ex.getCause() retrieves the original DmlException

Custom exceptions must extend Exception; Apex auto-generates constructors accepting (String message) and (String message, Exception cause).

Built-in exceptions — exact behavior

ExceptionThrown whenCatchable?
DmlExceptionPlain DML (insert/update) fails; has getDmlFieldNames(i)/getDmlMessage(i)Yes
QueryExceptionSingle-sObject SOQL returns 0 or >1 rows ("List has no rows for assignment" / "...more than 1 row")Yes
NullPointerExceptionDereferencing a null referenceYes
LimitExceptionA governor limit is exceededTechnically yes, but rarely useful — the limit is already breached
  • Multiple catch blocks are matched top-to-bottom — most specific first, generic Exception last. Apex actually enforces this: an unreachable catch block (generic before specific) is a compile error.
  • finally always executes, regardless of a return or throw in try/catch.
TrapA single-sObject query with 0 matching rows throws QueryException, it does not return null. Catching a LimitException generally can't let you "continue" the same operation — prevent it proactively via bulkification instead of catching it reactively.

Flashcards

Active recall

Answer in your head first, then reveal. Use "Reveal all" in the header to read straight through.