Salesforce upsert trigger – How many times does it fire?

How the Salesforce upsert trigger manages mixed operations

If you are coding a Salesforce upsert trigger, you’ve likely asked yourself: “How many times is this thing actually going to fire?” It is a fair question. I’ve seen plenty of developers get tripped up because they assume an upsert is its own special event in the execution order. But that’s not how it works at all. Salesforce is actually doing some heavy lifting behind the scenes to decide what happens to each individual record in your list.

The short answer? For every record you’re pushing, the trigger fires once for the specific action taken – either an insert or an update. You won’t see a record go through both an insert and an update context in the same transaction. If the record is brand new, it hits the insert events. If it already exists, it hits the update events. It’s that simple, but the way Salesforce handles these in bulk is where things get interesting.

One thing that trips people up is forgetting that a single DML statement can result in two different trigger contexts. If you’re upserting 200 records and half are new, your trigger will run in both insert and update modes during that transaction.

When you’re dealing with a mixed batch, Salesforce groups the records. It runs the before insert and after insert for the new guys, and the before update and after update for the existing ones. If you want a deeper look at the technical sequence, check out this guide on the Apex trigger upsert execution flow to see how the platform prioritizes these steps.

A technical flowchart diagram illustrating the logic flow of a database upsert operation, showing a decision point branching into two distinct paths.
A technical flowchart diagram illustrating the logic flow of a database upsert operation, showing a decision point branching into two distinct paths.

Writing a cleaner Salesforce upsert trigger

So what does this actually mean for your code? It means you can’t just check if an ID exists and call it a day. Honestly, the best way to handle this is to stick to the standard trigger context variables. I always tell my team to rely on Trigger.isInsert and Trigger.isUpdate. These are your best friends when you’re trying to figure out which logic should run for which record.

Here’s the thing: bulkification still matters. Even though Salesforce splits the records into insert and update groups, they are still processed in chunks. If you’re looking for more examples of how to handle these scenarios, you might find these Apex trigger interview questions helpful for seeing how others structure their logic.

Handling Before and After contexts

For whatever action actually happens, you’re going to get both the before and after events. If a record is being inserted, you’ll get before insert first, then the record saves, then after insert fires. But remember, if you have a Flow or a workflow rule that updates a field after the record is saved, that might kick off another update trigger. This is where you can run into recursion issues if you aren’t careful.

The External ID factor

When you use an External ID to perform an upsert, Salesforce uses that value to match against existing records. If it finds a match, it’s an update. If it doesn’t, it’s an insert. Don’t try to guess what’s going to happen by looking at the data yourself. Let the Salesforce upsert trigger context tell you what it decided to do. It’s much safer than trying to manually check for IDs in your code.

A standard pattern that works

I’ve found that keeping your trigger logic light and delegating to a handler class is the only way to stay sane. Here is a basic look at how you should structure your Salesforce upsert trigger to handle these events properly:


trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
    if (Trigger.isBefore) {
        if (Trigger.isInsert) {
            // New records only
        }
        if (Trigger.isUpdate) {
            // Existing records only
        }
    }
    if (Trigger.isAfter) {
        if (Trigger.isInsert) {
            // Post-save logic for new records
        }
        if (Trigger.isUpdate) {
            // Post-save logic for updates
        }
    }
}

Key Takeaways

  • Each record in an upsert only fires the trigger context for the action performed (insert or update).
  • A mixed batch of records will trigger both insert and update events in the same transaction.
  • Always use Trigger.isInsert and Trigger.isUpdate to separate your logic.
  • Watch out for “invisible” updates from Flows or Workflows that can cause triggers to run multiple times.
  • External IDs are the source of truth for how Salesforce decides between an insert or an update.

At the end of the day, an upsert isn’t some weird third type of DML. It’s just a smart way to let Salesforce decide whether to create or update for you. As long as you write your trigger to handle the standard insert and update contexts, you’ll be fine. Just keep an eye on your debug logs to make sure other automations aren’t causing your code to fire more often than you expect.