Saltar al contenido
10 de febrero de 2026·Actualizado el ·31 min de lectura

Flow Builder vs Apex: ¿cuándo elegir cada uno?

Guía práctica de decisión para elegir entre Flow Builder y Apex según las necesidades de automatización de tu Salesforce.

← Volver

Una de las preguntas más habituales en el desarrollo Salesforce: "¿Debería usar un Flow o escribir código Apex?" Esta guía te ayuda a tomar la decisión correcta según tu contexto.

El principio "Clics, no código"

Salesforce recomienda la filosofía "Clics, no código": preferir las herramientas declarativas (Flow Builder) frente al código siempre que sea posible. Process Builder y las Workflow Rules no tienen soporte desde el 31 de diciembre de 2025: toda nueva automatización declarativa se construye en Flow.

¿Por qué?

  • ✅ Desarrollo más rápido
  • ✅ Mantenimiento más sencillo (interfaz visual)
  • ✅ Menos tests requeridos (sin cobertura del 75%)
  • ✅ Accesible para administradores sin perfil de desarrollador

Pero cuidado: este principio tiene límites. A veces el código es la mejor opción.

Matriz de decisión rápida

CriterioFlow BuilderApex
Lógica simple✅ Excelente⚠️ Excesivo
Lógica compleja⚠️ Posible pero verboso✅ Excelente
Rendimiento crítico⚠️ Limitado✅ Optimizable
Reutilización⚠️ Limitada✅ Capa de servicio
Mantenimiento✅ Interfaz visual⚠️ Requiere perfil técnico
Testing✅ Sin cobertura requerida❌ 75% mínimo
Depuración⚠️ Debug Logs✅ Debug logs + IDE
Operaciones masivas⚠️ Cuidado con los límites✅ Control total
Integraciones externas⚠️ Acción HTTP Callout para llamadas REST simples✅ Control total (autenticación, transformación, reintentos)

Escenarios: Flow Builder es ideal

1. Automatizaciones CRUD simples

Ejemplo: Crear automáticamente un Contacto al crear una Cuenta.

Flow: "Auto-Create Primary Contact"
Type: Record-Triggered Flow
Trigger: Account - After Save - Create

Logic:
1. Get Records: Check if Contact exists for this Account
2. Decision: Contact exists?
   - No → Create Contact with default values
   - Yes → End

¿Por qué Flow?

  • Lógica lineal y sencilla
  • Sin lógica de negocio compleja
  • Visible y modificable por un administrador

2. Validaciones de negocio con mensajes de error

Ejemplo: Impedir la eliminación de una Cuenta con Oportunidades abiertas.

Flow: "Prevent Account Deletion with Open Opps"
Type: Record-Triggered Flow
Trigger: Account - Before Delete

Logic:
1. Get Records: Count Opportunities (StageName != 'Closed Won/Lost')
2. Decision: Count > 0?
   - Yes → Show Error: "Cannot delete Account with open Opportunities"
   - No → Allow deletion

¿Por qué Flow?

  • Validación simple basada en una condición
  • Mensaje de error amigable para el usuario
  • No se necesita código para este caso

3. Actualización de campos calculados

Ejemplo: Calcular el importe total de Oportunidades de una Cuenta.

Flow: "Calculate Account Total Opportunity Amount"
Type: Record-Triggered Flow
Trigger: Opportunity - After Save - Create, Update, Delete

Logic:
1. Get Records: Get parent Account
2. Get Records: Sum all Opportunities.Amount for this Account
3. Update Records: Update Account.Total_Opportunity_Amount__c

¿Por qué Flow?

  • Cálculo aritmético simple
  • Automatización estándar
  • Fácil de mantener para un administrador

4. Envío de correos transaccionales

Ejemplo: Enviar un correo de bienvenida tras la creación de un Lead.

Flow: "Welcome Email to New Leads"
Type: Record-Triggered Flow
Trigger: Lead - After Save - Create

Logic:
1. Decision: Lead Source = 'Web'?
   - Yes → Send Email (Email Template: "Welcome_Lead")
   - No → End

¿Por qué Flow?

  • Acción nativa "Send Email" disponible
  • Plantilla de correo gestionada desde Setup
  • Sin complejidad que justifique código

Escenarios: Apex es necesario

1. Lógica de negocio compleja con algoritmos

Ejemplo: Cálculo dinámico de precios con descuentos progresivos.

public class PricingEngine {
    
    public static Decimal calculatePrice(Opportunity opp) {
        return calculatePrice(opp, System.today());
    }
 
    // La fecha es un parámetro: el descuento Q4 se puede probar con una fecha fija.
    // opp debe consultarse con Account.Years_as_Customer__c: un registro recién
    // insertado no tiene cargada la relación Account (NullPointerException).
    public static Decimal calculatePrice(Opportunity opp, Date pricingDate) {
        Decimal basePrice = opp.Amount;
        Decimal discount = 0;
        
        // Descuento progresivo por tramo
        if (basePrice > 100000) {
            discount = 0.15; // 15% por encima de 100k
        } else if (basePrice > 50000) {
            discount = 0.10; // 10% entre 50k-100k
        } else if (basePrice > 10000) {
            discount = 0.05; // 5% entre 10k-50k
        }
        
        // Descuento adicional para cliente fiel
        if (opp.Account.Years_as_Customer__c > 5) {
            discount += 0.05;
        }
        
        // Descuento estacional (Q4)
        if (pricingDate.month() >= 10) {
            discount += 0.03;
        }
        
        return basePrice * (1 - discount);
    }
}

¿Por qué Apex?

  • Lógica con varias condiciones anidadas
  • Cálculos aritméticos complejos
  • Código reutilizable y testeable
  • Un Flow sería demasiado verboso y difícil de mantener

2. Operaciones masivas con miles de registros

Ejemplo: Actualización por lotes de 50.000 Contactos.

// Database.AllowsCallouts es obligatorio para hacer callouts en un batch. Lanzar con
// Database.executeBatch(new ContactUpdateBatch(), 100): 1 callout por Contacto, 100 como máximo por transacción.
public class ContactUpdateBatch implements Database.Batchable<SObject>, Database.AllowsCallouts {
    
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator([
            SELECT Id, MailingPostalCode, MailingCity 
            FROM Contact 
            WHERE LastModifiedDate < LAST_N_DAYS:365
        ]);
    }
    
    public void execute(Database.BatchableContext bc, List<Contact> scope) {
        // Llamada a la API de geocodificación para cada Contacto
        Map<Id, Contact> contactsToUpdate = new Map<Id, Contact>();
        
        for (Contact con : scope) {
            // Llamar al servicio externo de geocodificación
            Geocoding.Result result = GeocodingService.geocode(
                con.MailingPostalCode, 
                con.MailingCity
            );
            
            if (result.success) {
                con.Geolocation__Latitude__s = result.latitude;
                con.Geolocation__Longitude__s = result.longitude;
                contactsToUpdate.put(con.Id, con);
            }
        }
        
        if (!contactsToUpdate.isEmpty()) {
            update contactsToUpdate.values();
        }
    }
    
    public void finish(Database.BatchableContext bc) {
        // Enviar correo de finalización
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        mail.setToAddresses(new String[] {'admin@company.com'});
        mail.setSubject('Contact Geocoding Batch Completed');
        mail.setPlainTextBody('Batch job finished successfully.');
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
}

¿Por qué Apex?

  • Procesamiento de decenas de miles de registros
  • Llamadas HTTP externas (API de geocodificación)
  • Control fino del tamaño de los lotes (100 registros como máximo aquí, para no superar el límite de 100 callouts por transacción)
  • Manejo avanzado de errores

3. Integraciones REST/SOAP complejas

Ejemplo: Integración bidireccional con un ERP externo.

public class ERPIntegrationService {
    
    @future(callout=true)
    public static void syncOrderToERP(Id orderId) {
        Order order = [
            SELECT Id, OrderNumber, TotalAmount, Account.Name, 
                   (SELECT Product2.Name, Quantity, UnitPrice FROM OrderItems)
            FROM Order 
            WHERE Id = :orderId
        ];
        
        // Construir el payload JSON
        Map<String, Object> payload = new Map<String, Object>{
            'orderNumber' => order.OrderNumber,
            'customer' => order.Account.Name,
            'totalAmount' => order.TotalAmount,
            'items' => buildOrderItems(order.OrderItems)
        };
        
        // Callout HTTP
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:ERP_API/orders');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(payload));
        req.setTimeout(120000);
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        if (res.getStatusCode() == 201) {
            // Parsear la respuesta y actualizar el Order con el ID del ERP
            Map<String, Object> responseData = 
                (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            
            order.ERP_Order_ID__c = (String) responseData.get('erpOrderId');
            order.ERP_Sync_Status__c = 'Synced';
            update order;
        } else {
            throw new ERPIntegrationException('ERP sync failed: ' + res.getBody());
        }
    }
    
    private static List<Map<String, Object>> buildOrderItems(List<OrderItem> items) {
        List<Map<String, Object>> result = new List<Map<String, Object>>();
        for (OrderItem item : items) {
            result.add(new Map<String, Object>{
                'productName' => item.Product2.Name,
                'quantity' => item.Quantity,
                'unitPrice' => item.UnitPrice
            });
        }
        return result;
    }
}

¿Por qué Apex?

  • Construcción dinámica de JSON complejo
  • Gestión fina de timeouts y reintentos
  • Parseo estructurado de la respuesta
  • Manejo de errores con excepciones personalizadas

4. Tests unitarios críticos

Ejemplo: Clase de negocio crítica que requiere cobertura del 100%.

@isTest
private class PricingEngine_Test {
    
    @isTest
    static void testCalculatePrice_BaseDiscount() {
        // Arrange
        Account acc = new Account(Name = 'Test Corp', Years_as_Customer__c = 3);
        insert acc;
        
        Opportunity opp = new Opportunity(
            Name = 'Test Deal',
            AccountId = acc.Id,
            Amount = 60000,
            StageName = 'Prospecting',
            CloseDate = Date.today().addDays(30)
        );
        insert opp;
        // Consultar el campo de relación que lee PricingEngine (si no, NullPointerException)
        opp = [SELECT Id, Amount, Account.Years_as_Customer__c FROM Opportunity WHERE Id = :opp.Id];
        
        // Act
        Test.startTest();
        // Fecha fija fuera de Q4: el resultado no depende del día en que se ejecuta el test
        Decimal finalPrice = PricingEngine.calculatePrice(opp, Date.newInstance(2026, 3, 15));
        Test.stopTest();
        
        // Assert
        Decimal expected = 60000 * 0.90; // 10% de descuento
        System.assertEquals(expected, finalPrice, 
            'Price should have 10% discount for amount between 50k-100k');
    }
    
    @isTest
    static void testCalculatePrice_LoyalCustomer() {
        // Arrange
        Account acc = new Account(Name = 'Loyal Corp', Years_as_Customer__c = 7);
        insert acc;
        
        Opportunity opp = new Opportunity(
            Name = 'Test Deal',
            AccountId = acc.Id,
            Amount = 60000,
            StageName = 'Prospecting',
            CloseDate = Date.today().addDays(30)
        );
        insert opp;
        // Consultar el campo de relación que lee PricingEngine (si no, NullPointerException)
        opp = [SELECT Id, Amount, Account.Years_as_Customer__c FROM Opportunity WHERE Id = :opp.Id];
        
        // Act
        Test.startTest();
        // Fecha fija fuera de Q4: el resultado no depende del día en que se ejecuta el test
        Decimal finalPrice = PricingEngine.calculatePrice(opp, Date.newInstance(2026, 3, 15));
        Test.stopTest();
        
        // Assert
        Decimal expected = 60000 * 0.85; // 10% base + 5% fidelidad
        System.assertEquals(expected, finalPrice, 
            'Loyal customer should get additional 5% discount');
    }
}

¿Por qué Apex?

  • Tests con aserciones precisas
  • Control total del contexto de test
  • Validación de cada rama de la lógica
  • Cobertura de código requerida

Enfoque híbrido: lo mejor de ambos mundos

En muchos casos, la mejor solución es una combinación de Flow + Apex.

Patrón recomendado: el Flow llama a Apex

Ejemplo: Validación de un número SIRET francés con una API externa.

1. Crear una clase Apex invocable:

public class SIRETValidator {
    
    @InvocableMethod(label='Validate SIRET' 
                     description='Validates French SIRET number via external API'
                     callout=true)
    public static List<Result> validateSIRET(List<Request> requests) {
        List<Result> results = new List<Result>();
        
        // Un callout por solicitud: 100 como máximo por transacción, a tener en cuenta con grandes volúmenes
        for (Request req : requests) {
            Result result = new Result();
            result.isValid = callSIRETAPI(req.siretNumber);
            results.add(result);
        }
        
        return results;
    }
    
    private static Boolean callSIRETAPI(String siret) {
        // Callout HTTP a la API SIRENE
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:SIRENE_API/siret/' + siret);
        req.setMethod('GET');
        
        Http http = new Http();
        HttpResponse res = http.send(req);
        
        return res.getStatusCode() == 200;
    }
    
    public class Request {
        @InvocableVariable(required=true)
        public String siretNumber;
    }
    
    public class Result {
        @InvocableVariable
        public Boolean isValid;
    }
}

2. Llamarla desde un Flow:

Flow: "Validate Account SIRET"
Type: Record-Triggered Flow
Trigger: Account - After Save - Create, Update
Path: Run Asynchronously

Logic:
1. Decision: Country = 'France' AND SIRET changed?
   - Yes → Continue
   - No → End

2. Action: Apex - SIRETValidator.validateSIRET
   Input: SIRET__c
   Output: isValid

3. Update Records: SIRET_Status__c = isValid ? 'Valid' : 'Invalid'

¿Por qué no un flow before-save? Un flow before-save solo admite los elementos Assignment, Decision, Get Records y Loop: no puede llamar ni a una acción Apex ni a un callout. Por eso la verificación se hace después de guardar, en una ruta asíncrona donde los callouts están permitidos, y su resultado se registra en un campo de estado en lugar de bloquear la entrada.

Ventajas:

  • ✅ Lógica de Flow simple y legible
  • ✅ Complejidad técnica aislada en Apex
  • ✅ Testeable (tests Apex para la llamada a la API)
  • ✅ Mantenible (el admin puede modificar el Flow, el dev se encarga de la API)

Checklist de decisión

Usa esta checklist para elegir:

Prefiere Flow Builder si:

  • La lógica es lineal (< 10 pasos)
  • No hay algoritmos complejos
  • No hay llamadas HTTP externas, o solo una llamada REST simple (acción HTTP Callout)
  • Se procesan < 1.000 registros
  • El mantenimiento lo hacen administradores sin perfil de desarrollador

Prefiere Apex si:

  • Lógica con múltiples condiciones anidadas
  • Cálculos matemáticos complejos
  • Integraciones REST/SOAP
  • Procesamiento masivo (> 10.000 registros)
  • Reutilización crítica (capa de servicio)
  • Rendimiento crítico
  • Se requieren tests unitarios avanzados

Enfoque híbrido (Flow + Apex invocable) si:

  • Lógica global simple PERO con un paso complejo
  • Necesitas llamar a una API externa a mitad del Flow
  • Mantenimiento compartido entre admin y desarrollador

Conclusión

La regla de oro: empieza siempre evaluando si Flow Builder es suficiente. Si identificas una limitación clara (rendimiento, complejidad, integración), pasa entonces a Apex.

Patrón recomendado:

  1. Flow para la orquestación y la lógica de alto nivel
  2. Apex invocable para operaciones específicas complejas
  3. Trigger/batch Apex solo para casos avanzados

Este enfoque equilibra mantenibilidad (Flow), potencia (Apex) y gobernanza (separación de responsabilidades).


Recursos útiles:

Aïcha Imène DAHOUMANE
Sobre la autora
Aïcha Imène DAHOUMANE
Administradora y Desarrolladora Salesforce

Desarrolladora Salesforce con formación en infraestructura IT — construyo soluciones fiables y mantenibles con Apex, Flows, LWC y CI/CD.

CompartirCompartir en LinkedIn