Apex Academy
Módulo 7·04·36 min

La guardia y el interruptor

Con la arquitectura ya ordenada, dos protecciones viven en un solo sitio: una guardia estática que impide hacer dos veces lo mismo en una transacción, y un interruptor con permiso personalizado para cargas de datos.

En Admin esEl permiso «Bypass» que ya usas en tus reglas de validación

✓ En esta sub-lección

  1. 01Explicar por qué comparar old y new no basta cuando una regla de workflow vuelve a disparar el trigger.
  2. 02Poner la guardia static Set<Id> en el sitio donde se hace el trabajo: el servicio.
  3. 03Crear un interruptor con un permiso personalizado y FeatureManagement.checkPermission.
  4. 04Saber para quién es un bypass y para quién no.

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

El botón «Recalcular prioridad» aplica la regla del servicio a casos que ya existen. ¿Qué tiene que hacer después?

Soporte pidió una regla más: si un agente sube a mano la prioridad de un caso a 'High', su propietario también debe recibir la tarea de revisión. Se escribió con la defensa 2 del Módulo 6 (solo si la prioridad CAMBIÓ a 'High') y en la sandbox iba perfecto. En producción, los agentes empezaron a ver dos tareas por caso. Lo que avisó Operaciones al final de la tarea 3 era verdad.

Por qué la defensa 2 no basta aquí

La org heredada tiene una regla de workflow sobre Case que rellena un campo al guardar. En el Módulo 6 viste que, cuando una actualización de campo de workflow cambia el registro, los triggers de update se ejecutan una vez más. Lo que no viste es el detalle traicionero: en esa segunda pasada, Trigger.old NO trae la versión de justo antes del workflow, sino la de antes de la actualización ORIGINAL. Así que la comparación vuelve a ver 'Medium' → 'High', cree que la prioridad acaba de cambiar… y crea la tarea otra vez.

Un agente sube la prioridad de un caso de 'Medium' a 'High'. Nuestra regla crea una tarea de revisión. Pero la org heredada tiene una regla de workflow que, al guardar, rellena un campo del caso… y eso vuelve a disparar los triggers de update una vez más.

Elige los interruptores y pulsa «Guardar».

Guarda primero sin guardia, luego con ella, y por último con el permiso de bypass.

La nutria: ¿Todavía hay reglas de workflow?

Te las vas a encontrar, como me las encontré yo: Salesforce dejó de dar soporte a Workflow Rules y Process Builder a finales de 2025 y recomienda migrarlas a Flow, pero en las orgs antiguas siguen activas muchas. Y la lección vale igual para los flows after-save que actualizan el mismo registro: cualquier segunda vuelta por el trigger es una ocasión de hacer las cosas dos veces.

La guardia, en su sitio

La defensa 3 del Módulo 6 es la que resuelve esto: un Set<Id> static que recuerda qué registros ya se procesaron en la transacción. Lo nuevo es DÓNDE vive. Con la arquitectura de este módulo hay un sitio obvio: el método del servicio que hace el trabajo. Así la protección se aplica venga de donde venga la llamada (el after insert, el after update o una segunda pasada por culpa del workflow) y nadie tiene que acordarse de añadirla en cada puerta.

Apex
public with sharing class CaseEscalationService {
    // Casos que ya tienen su tarea en ESTA transacción
    private static Set<Id> tasksCreatedFor = new Set<Id>();

    public static void createReviewTasks(List<Case> cases) {
        List<Task> tasks = new List<Task>();
        for (Case c : cases) {
            if (c.Priority == 'High' && !tasksCreatedFor.contains(c.Id)) {
                tasksCreatedFor.add(c.Id);
                tasks.add(new Task(WhatId = c.Id, OwnerId = c.OwnerId,
                                   Subject = 'Revisar caso escalado'));
            }
        }
        insert tasks;
    }
}
private: nadie de fuera necesita tocar la guardia. static: vive lo que dura la transacción, y todas las llamadas de esa transacción la comparten.

El interruptor: un bypass con permiso personalizado

La última petición de Soporte es la contraria: van a migrar 50.000 casos históricos de un sistema antiguo, y no quieren ni prioridades recalculadas ni miles de tareas en las colas. Hace falta un interruptor que apague las reglas… pero solo para quien hace la migración. La forma limpia es un (Custom Permission) que se asigna con un permission set solo al usuario de integración, y en el código una línea que lo comprueba: FeatureManagement.checkPermission devuelve true si el usuario que está ejecutando tiene ese permiso.

Apex
trigger CaseTrigger on Case (before insert, after insert, after update) {
    // Interruptor: si quien guarda tiene el permiso, no se aplica ninguna regla
    if (FeatureManagement.checkPermission('Bypass_Case_Triggers')) {
        return;
    }
    CaseTriggerHandler handler = new CaseTriggerHandler();
    switch on Trigger.operationType { /* … */ }
}
Va arriba del todo, antes de crear el handler: una sola comprobación apaga todas las reglas del objeto a la vez.

La nutria: Es el mismo truco de tus reglas de validación

Seguro que lo has configurado; yo lo hice en varias orgs: un permiso personalizado «Bypass Validation Rules» y en cada regla una condición NOT($Permission.Bypass_Validation_Rules), para que las cargas masivas no choquen con validaciones pensadas para usuarios. El bypass del trigger es exactamente eso: $Permission en una fórmula, FeatureManagement.checkPermission en Apex. Mismo permiso, mismo permission set, misma persona.

La nutria: ¿Y por qué no un Flow? Porque en Flow el interruptor va en cada flow

En Flow también puedes poner $Permission en las condiciones de entrada, pero en cada flow del objeto, uno por uno, y el día que alguien crea uno nuevo y se olvida, la migración lo dispara. Aquí es una línea al principio del único trigger, y cubre todas las reglas, las de hoy y las que vengan. La guardia, como viste en el Módulo 6, no tiene equivalente en Flow: no hay memoria static.