Choosing Between Salesforce Flow and Apex Triggers for Business Logic
A concise decision guide for choosing Salesforce Flow versus Apex triggers, with a comparison table, trade‑offs, and a validation example for updating an Opportunity when a Project is closed won.
13 Jul 2025, 12:02 UTC

Decision and constraints
When you need to enforce business logic on record changes, you can either build a declarative Flow or write an Apex trigger. Choose Flow when the logic can be expressed with point‑and‑click tools, stays within Flow governor limits, and you want admins to maintain it without code. Choose Apex when you need complex calculations, external callouts, precise control of transaction boundaries, or bulk processing that exceeds Flow limits.
Comparison table
| Aspect | Flow | Apex Trigger |
|---|---|---|
| Declarative | Yes – drag‑and‑drop in Flow Builder | No – requires code |
| Governor limits per execution | Lower (e.g., 2,000 elements, 150 SOQL rows) | Higher but still limited (e.g., 100 SOQL queries, 50,000 rows) |
| Maintenance audience | Admin‑friendly, versioned in Setup | Developer‑friendly, source‑controlled |
| Callouts | Not allowed unless wrapped in Invocable Apex | Allowed via @future, Queueable, or HTTP callout |
| Bulk processing | Platform bulkifies; practical limit ~200 records/transaction | Fully bulk‑aware; use Batch Apex for thousands |
Trade‑offs
Flow speeds up deployment because admins can activate a new version without a deployment pipeline. Error handling is built‑in, and you can see the flow of execution in the Debug pane. However, complex logic with many decision elements or loops can quickly hit the element limit, and you cannot fine‑tune transaction boundaries or make outbound calls directly.
Apex gives you full programming power: you can write intricate algorithms, perform callouts, and control exactly when DML occurs. The trade‑off is the need for developer resources, a test class with ≥75% coverage, and careful monitoring of governor limits to avoid runtime exceptions that are harder to trace than Flow limit warnings.
Concrete implementation/validation
The following example shows how to update a related Opportunity’s Amount when a custom object Project__c reaches the stage "Closed Won". Both approaches achieve the same result; you can implement either in a sandbox and verify the outcome.
Flow implementation
- In Setup, open Flow Builder and create a new Record‑Triggered Flow on the
Project__cobject. - Set the trigger to fire after a record is updated.
- Add a Decision element that checks
[Project__c].Stageequals "Closed Won". - If true, use a Get Records element to retrieve the related Opportunity (assuming a lookup field
Opportunity__con Project__c). - Add an Update Records element to set
Opportunity.Amountto[Project__c].Amount(or any calculation you need). - Save, activate the Flow, and note the version number.
To validate, create a test Project__c record with Stage = "Closed Won" and a related Opportunity. After saving, open the Opportunity record and confirm the Amount field updated as expected. You can also run the Flow in Debug mode with the same input values to see each step and any limit warnings.
Apex trigger implementation
Deploy the following trigger and test class from your IDE (e.g., VS Code with SFDX) or the Developer Console. Ensure you have the "Modify All Data" and "Author Apex" permissions.
trigger ProjectTrigger on Project__c (after update) {
// Collect Opportunity IDs to update
Set oppIds = new Set();
for (Project__c p : Trigger.new) {
Project__c old = Trigger.oldMap.get(p.Id);
if (p.Stage__c == 'Closed Won' && old.Stage__c != 'Closed Won') {
if (p.Opportunity__c != null) oppIds.add(p.Opportunity__c);
}
}
if (oppIds.isEmpty()) return;
// Query the related Opportunities
List opps = [SELECT Id, Amount FROM Opportunity WHERE Id IN :oppIds];
for (Opportunity o : opps) {
// Find the Project that caused the update (assuming one‑to‑many)
for (Project__c p : Trigger.new) {
if (p.Opportunity__c == o.Id && p.Stage__c == 'Closed Won') {
o.Amount = p.Amount__c; // adjust as needed
break;
}
}
}
update opps;
}
@IsTest
private class ProjectTriggerTest {
@IsTest
static void testClosedWonUpdatesOpportunity() {
// Setup
Opportunity opp = new Opportunity(Name='Test Opp', CloseDate=Date.today(), StageName='Prospecting', Amount=0);
insert opp;
Project__c proj = new Project__c(Name='Test Project', Opportunity__c=opp.Id, Stage__c='Open', Amount__c=5000);
insert proj;
// Act
proj.Stage__c = 'Closed Won';
update proj;
// Verify
opp = [SELECT Amount FROM Opportunity WHERE Id = :opp.Id];
System.assertEquals(5000, opp.Amount, 'Opportunity amount should reflect Project amount');
}
}
After deploying the trigger and test class, run the test (via Setup → Apex Test Execution or your CI pipeline) to confirm ≥75% coverage. Then insert or update a Project__c record as described and check that the related Opportunity’s Amount field changes accordingly.
Verification and rollback
For the Flow, open Debug in Flow Builder, provide the same field values used in the test, and inspect the step‑by‑step execution for any limit warnings or errors. For the Apex trigger, the test class already asserts the expected outcome; a passing test confirms correct behavior in the sandbox.
If you need to roll back the changes made during validation, simply delete the test Project__c and Opportunity records you created. Because both approaches only write to standard objects, removing those records restores the original state without affecting existing data.
Limitations and practical checks
- Flows are limited to 2,000 executed elements per transaction; complex logic with many loops or decision nodes may exceed this limit and produce a runtime error that is harder to debug than an Apex limit exception.
- Apex triggers require a test class; without sufficient coverage the deployment will fail. Ensure your test covers bulk scenarios (e.g., 200 records) to verify governor‑limit safety.
- Callouts from Flow are not possible unless you expose the logic as an Invocable Apex method; otherwise, use Apex for any external integration.
- Always verify in a sandbox or scratch org before promoting to production, and monitor the Debug Logs (Setup → Monitoring → Debug Logs) for any unexpected limit warnings after deployment.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.