Apex Academy
Módulo 6·02·35 min

Contexto: Trigger.new, Trigger.old, Trigger.newMap

Lo que Salesforce le entrega a tu trigger en cada ejecución: los registros nuevos, los viejos, sus mapas por Id y unas preguntas de sí o no para saber qué está pasando.

En Admin es$Record y $Record__Prior de un Flow, pero en listas

✓ En esta sub-lección

  1. 01Saber qué contiene Trigger.new, Trigger.old, Trigger.newMap y Trigger.oldMap, y en qué eventos existe cada uno.
  2. 02Detectar si un campo cambió comparando el valor nuevo con el viejo.
  3. 03Usar Trigger.isInsert, isUpdate, isBefore e isAfter para separar la lógica.

La nutria: ¿Te acuerdas? · Repaso de la lección 1

Un trigger after insert recibe 150 leads. ¿Qué tiene Trigger.new?

Cuando Salesforce ejecuta tu trigger, no lo hace a ciegas: le deja preparadas unas con todo lo que necesita saber. Qué registros llegan, cómo eran antes del cambio y qué operación se está haciendo. Toda la lógica de un trigger se escribe a partir de ellas.

La nutria: El paralelo de Admin

En mis Record-Triggered Flows de actualización usaba {!$Record.StageName} para el valor nuevo y {!$Record__Prior.StageName} para el que tenía antes. Y la condición de entrada «Only when a record is updated to meet the condition requirements» comparaba los dos por mí. En un trigger tienes lo mismo, pero en plural: Trigger.new son todos los $Record del lote y Trigger.old todos los $Record__Prior. La comparación la escribes tú.

Las cuatro colecciones

  • Trigger.new: List con los registros tal como van a quedar. Es tu $Record en plural.
  • Trigger.old: List con los registros tal como estaban antes del cambio. Tu $Record__Prior en plural.
  • Trigger.newMap: los mismos registros nuevos, en un Map<Id, sObject>. Para buscar por Id sin recorrer, como el VLOOKUP del Módulo 2.
  • Trigger.oldMap: los registros viejos en un Map<Id, sObject>. Es la que más usarás: con ella encuentras el «antes» de cada registro de Trigger.new.

La oportunidad de Acme pasa de 'Prospecting' a 'Negotiation'. Elige el evento que está corriendo y mira qué variables existen.

Trigger.new✓ solo lectura

[ Acme · StageName 'Negotiation' ]

Trigger.newMap✓ solo lectura

{ 006…Acme → StageName 'Negotiation' }

Trigger.old✓ solo lectura

[ Acme · StageName 'Prospecting' ]

Trigger.oldMap✓ solo lectura

{ 006…Acme → StageName 'Prospecting' }

PRUÉBALO DENTRO DE for (Opportunity o : Trigger.new)

La misma oportunidad dos veces: como estaba (oldMap) y como va a quedar (new). El Id es el hilo que las une.

No todas existen en todos los eventos

Tiene lógica si piensas en el registro: al crearlo no hay un «antes», así que no hay Trigger.old. Al borrarlo no hay un «después», así que no hay Trigger.new. Y en before insert el registro todavía no tiene Id, así que no puede haber un mapa por Id.

EventonewnewMapoldoldMap
before insert✓ (editable)✗ sin Id✗✗
after insert✓ lectura✓✗✗
before update✓ (editable)✓✓✓
after update✓ lectura✓✓✓
before delete✗✗✓✓
after delete✗✗✓✓
after undelete✓ lectura✓✗✗

¿Cambió el campo? El patrón old frente a new

La pregunta más frecuente en un trigger de update no es «¿cómo está el registro?» sino «¿qué cambió?». Si no la haces, tu lógica se ejecuta cada vez que alguien edita cualquier campo, aunque sea la descripción. Se responde recorriendo Trigger.new y buscando cada registro en Trigger.oldMap por su Id.

Apex
trigger OpportunityStage on Opportunity (before update) {
    for (Opportunity opp : Trigger.new) {
        Opportunity before = Trigger.oldMap.get(opp.Id);

        if (opp.StageName != before.StageName) {
            opp.NextStep = 'Etapa: ' + before.StageName + ' → ' + opp.StageName;
        }
    }
}
Trigger.oldMap.get(opp.Id) es el $Record__Prior de esa oportunidad concreta. Y como es before update, basta con cambiar opp.NextStep: se guarda solo, sin DML.

Las preguntas de sí o no

Un mismo trigger puede escuchar varios eventos. Para saber cuál es el de esta ejecución, Salesforce te da unos Boolean: Trigger.isInsert, isUpdate, isDelete, isUndelete, y para el momento, isBefore e isAfter. Con los if del Módulo 2 separas cada rama. También tienes Trigger.size (cuántos registros llegan) y Trigger.operationType, que lo resume todo en un valor como BEFORE_UPDATE.

Apex
trigger AccountTrigger on Account (before insert, before update, after insert) {
    if (Trigger.isBefore && Trigger.isInsert) {
        // solo al crear, antes de guardar
    }
    if (Trigger.isBefore && Trigger.isUpdate) {
        // aquí sí existe Trigger.oldMap
    }
    if (Trigger.isAfter && Trigger.isInsert) {
        // ya hay Ids: Trigger.newMap disponible
    }
}

Trigger.new solo se puede tocar en before

En before insert y before update, cambiar un campo de un registro de Trigger.new es la forma de modificarlo: el cambio se guarda solo, porque el registro todavía no se ha escrito. En los eventos after, en cambio, Trigger.new es de solo lectura: si intentas asignar un campo, Salesforce lanza System.FinalException: Record is read-only. Y Trigger.old no se puede cambiar nunca. La lección 3 explica por qué y qué se hace entonces.

La nutria: ISCHANGED() y PRIORVALUE(): old y new en tus fórmulas

Esto lo escribiste mil veces, como yo: en una regla de validación usas ISCHANGED(StageName) para saber si la etapa cambió, y PRIORVALUE(StageName) para leer la que había antes. Son exactamente Trigger.oldMap.get(o.Id).StageName comparado con o.StageName. La diferencia es de escala: la fórmula mira un registro; el trigger recibe hasta 200 y compara cada uno con su versión anterior, buscándola por Id en oldMap.

La nutria: ¿Y por qué no un Flow? Aquí el flow era la herramienta correcta

Con franqueza: el rastro de etapas era un flow before-save con $Record__Prior, y es justo lo que Salesforce recomienda para cambiar un campo del propio registro. Se migra por otra razón: en la tarea 5, Opportunity va a tener un trigger sí o sí, y la guía oficial pide no mezclar flows y triggers como punto de entrada del mismo objeto. Si el objeto no tuviera trigger, el flow se quedaba donde estaba.