This guide walks through 53 beginner-friendly Apex trigger exercises — field updates, simple validation, and basic before/after insert & update logic. Each entry below includes the full problem statement, the governor-limit and best-practice constraints it's testing, and a numbered approach to solving it — then you can open the same problem in ApexArena's browser-based Apex editor and get instant pass/fail feedback against real test cases.
Easy
TriggersGovernor
1. Bulk Account Rating Updater
Problem #1 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Account.
Set the Rating field based on AnnualRevenue:
- Revenue > 1,000,000 → Rating = Hot
- Revenue 100,000 – 1,000,000 → Rating = Warm
- Revenue < 100,000 → Rating = Cold
Must handle bulk operations of up to 200 records without hitting governor limits.
Trigger Context Variables
Trigger.isBefore — true when running in a before context
Trigger.isInsert — true on insert events
Trigger.isUpdate — true on update events
Trigger.new — list of new/updated records (available on insert & update)
Constraints
- No SOQL inside the loop
- Must be fully bulkified
- Handle null AnnualRevenue gracefully
Approach
- 1Use Trigger.isBefore and (Trigger.isInsert || Trigger.isUpdate) to guard your logic — this is best practice even when the trigger event already implies the context.
- 2Check acc.AnnualRevenue != null before comparing — null comparisons can throw NPE.
- 3Use a simple if / else if / else chain inside the loop. No SOQL needed here.
- 4This is a before trigger — mutate acc.Rating directly, no DML required.
Easy
Triggers
2. Opportunity Close Date Validator
Problem #11 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Opportunity
that prevents saving a record when the CloseDate is set to a date in the past.
Use addError() to surface a user-friendly validation message directly on the field.
Requirements
- Trigger must fire on before insert and before update
- Compare
CloseDate against Date.today()
- Call
opp.CloseDate.addError() with a descriptive message
- Handle bulk operations — no SOQL inside the loop
- Allow
CloseDate equal to today (same-day close is valid)
Example
CloseDate = yesterday → Error: "Close Date cannot be in the past."
CloseDate = today → OK
CloseDate = tomorrow → OK
Approach
- 1Use opp.CloseDate < Date.today() — the < operator works directly on Date values in Apex.
- 2Call opp.CloseDate.addError('Close Date cannot be in the past.') to attach the error to the field itself.
- 3Before triggers block the DML automatically when addError() is called — no return or exception needed.
Easy
Triggers
3. Employee Salary Validator
Problem #12 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Employee__c
that validates the Salary__c field.
- Salary must not be null
- Salary must be greater than 0
- Salary must not exceed $500,000
Use addError() on the field so the error appears inline in the UI.
Example
Salary__c = null → Error: "Salary is required."
Salary__c = -1000 → Error: "Salary must be greater than 0."
Salary__c = 600000 → Error: "Salary cannot exceed $500,000."
Salary__c = 90000 → OK
Approach
- 1Check emp.Salary__c == null || emp.Salary__c <= 0 in one condition.
- 2Call emp.Salary__c.addError('Your message here') — the error attaches directly to the field.
- 3Use else if for the upper bound so you only show one error per record.
Easy
Triggers
4. Account Website Normalizer
Problem #13 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Account
that normalizes the Website field.
If the Website is set but does not start with http:// or
https://, prepend https:// automatically.
Examples
Website = null → unchanged (skip)
Website = 'example.com' → 'https://example.com'
Website = 'http://x.com' → unchanged
Website = 'https://x.com' → unchanged
Approach
- 1Use acc.Website.startsWith('https://') — Apex String.startsWith() works just like Java.
- 2Combine both checks: !startsWith http:// AND !startsWith https://
- 3Prepend with: acc.Website = 'https://' + acc.Website;
Easy
Triggers
5. Opportunity Amount Validator
Problem #14 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Opportunity
that validates the Amount field must be greater than zero.
Null and zero amounts are both invalid — attach the error to the Amount field
using addError().
Constraints
- Bulk-safe — no SOQL inside the loop
- Error must be on the
Amount field, not on the record
Approach
- 1opp.Amount == null evaluates to true in Apex when the field is blank.
- 2Call opp.Amount.addError('Amount must be greater than zero.') to surface the error on the field.
- 3One condition covers both cases: opp.Amount == null || opp.Amount <= 0
Easy
Triggers
6. Case Priority Auto-Setter
Problem #16 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Case
that automatically sets the Priority field based on keywords in the
Subject.
- Subject contains urgent, critical, or asap
→ Priority = High
- Subject contains help or question
→ Priority = Low
- All other non-null subjects → Priority = Medium
- Null subject → skip (no change)
Matching is case-insensitive.
Approach
- 1Use subjectLower.contains('urgent') — String.contains() checks for a substring.
- 2Chain conditions: if / else if / else to avoid overwriting a valid priority.
- 3Priority field accepts: 'High', 'Medium', 'Low'
Easy
Triggers
7. Lead Score Auto-Assign Trigger
Problem #28 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert trigger on Lead that sets a custom
Lead_Score__c field based on LeadSource:
- Web → 80
- Phone Inquiry → 60
- Trade Show → 40
- Everything else → 20
Approach
- 1Use if/else if or a Map for cleaner code.
- 2Default to 20 when no match.
Easy
Triggers
8. Opportunity Amount Validation Trigger
Problem #30 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert, before update trigger on Opportunity
that validates:
Amount must be greater than 0
CloseDate must not be in the past
If either condition fails, call addError() on the record with a descriptive message.
Approach
- 1Date.today() gives today's date.
- 2opp.addError('message') adds the error to the record.
Easy
Triggers
9. Prevent Deletion of Won Opportunities
Problem #33 · Salesforce Apex Coding Challenge
Problem Statement
Write a before delete trigger on Opportunity that prevents
deletion of any Opportunity with StageName = 'Closed Won'.
Add an error message: "Cannot delete a Closed Won opportunity."
Approach
- 1Use Trigger.old to access records being deleted.
- 2opp.addError('Cannot delete a Closed Won opportunity.');
Easy
TriggersSOQL
10. Stamp Created-By Full Name on Record
Problem #40 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert trigger on Case that sets a custom text
field Created_By_Name__c to the full name of the user inserting the record.
Use UserInfo.getUserId() to get the current user, then query the User object
to get Name.
Approach
- 1User u = [SELECT Name FROM User WHERE Id = :userId LIMIT 1];
- 2Loop Trigger.new and set c.Created_By_Name__c = u.Name;
Easy
Triggers
11. Lead Default Field Setter
Problem #79 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert trigger on Lead that sets
default values for missing fields:
- If
LeadSource is null, set it to "Web"
- If
Status is null, set it to "New"
- If
Rating is null, set it to "Cold"
Key Concept — Before Insert Trigger
In a before insert trigger, Trigger.new contains the new
records before they are saved. Records have no Id yet.
You can modify fields directly — no DML needed. Trigger.old is null
in before insert because there are no prior versions.
Constraints
- Only use
before insert — do not trigger on update
- Do not overwrite values that are already set
- Must be bulkified — handle 200 records in one transaction
Approach
- 1Use: if (ld.LeadSource == null) ld.LeadSource = 'Web';
- 2Before insert triggers mutate the record in memory — no DML required.
- 3Trigger.old is always null in before insert — don't reference it here.
Easy
TriggersAutomation
12. Auto-Create Contact for IT Industry Accounts
Problem #138 · Salesforce Apex Coding Challenge
Problem Statement
Write an after insert trigger on Account.
Whenever a new account is created with Industry = 'Technology',
automatically create a Contact for that account using:
Contact.LastName = Account.Name
Contact.Phone = Account.Phone
Contact.AccountId = the new account's Id
Best Practices
- Use after insert (Ids are available after insert).
- Build the full list of new Contacts first, then insert in a single DML call.
- Skip accounts without Industry = 'Technology' silently.
Approach
- 1Loop through Trigger.new; check acc.Industry == 'Technology'.
- 2Build a List; add a new Contact(LastName=acc.Name, Phone=acc.Phone, AccountId=acc.Id).
- 3Insert the list after the loop — one insert call regardless of batch size.
- 4Test: insert 3 accounts — 2 IT + 1 other — assert exactly 2 contacts created.
Easy
Triggers
13. Opportunity Probability Setter
Problem #144 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger called
OpportunityProbabilityTrigger on Opportunity that
automatically sets the Probability field based on StageName
using the following mapping:
| Stage | Probability |
| Prospecting | 10 |
| Qualification | 20 |
| Proposal/Price Quote | 60 |
| Negotiation/Review | 80 |
| Closed Won | 100 |
| Closed Lost | 0 |
Requirements
- Fire on before insert and before update
- Use a
Map<String, Integer> to store the stage-to-probability mapping
- Only set
Probability when StageName is non-null and exists in the map
- Leave
Probability unchanged for stages not in the map
- Bulk-safe — handle up to 200 records with no SOQL inside the loop
Approach
- 1Declare the map outside the loop so it is built once for the entire batch.
- 2Use stageMap.containsKey(opp.StageName) before calling stageMap.get() to avoid null issues.
- 3This is a before trigger — assign opp.Probability directly without DML.
Easy
TriggersValidation
14. Trigger: Prevent Past Close Date on Opportunity
Problem #154 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Opportunity
that prevents reps from setting a CloseDate in the past.
If CloseDate < Date.today(), call addError() on the
CloseDate field with the message 'Close Date cannot be in the past.'
Best Practices
- Use a before trigger — no DML needed.
- Delegate logic to a handler class
OpportunityTriggerHandler.
- Bulkify: iterate
Trigger.new, not a single record.
- Guard null: only validate when
CloseDate != null.
Approach
- 1Use Date.today() to get today's date — no time-zone issues.
- 2Call opp.CloseDate.addError('Close Date cannot be in the past.') inside the loop.
- 3In a before trigger you do NOT need to call update — mutations apply automatically.
- 4A good test covers: future date (passes), today (passes), yesterday (throws).
Easy
Triggers
15. Trigger: Set Revenue Tier on Account
Problem #155 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Account
that automatically sets a custom field Revenue_Tier__c based on
AnnualRevenue:
- > 10,000,000 →
Enterprise
- 1,000,000 – 10,000,000 →
Mid-Market
- 100,000 – 1,000,000 →
SMB
- < 100,000 or null →
Startup
Best Practices
- No SOQL — purely in-memory classification.
- Handle
null AnnualRevenue (classify as Startup).
- Bulkify: handle 200 records in one invocation.
- Delegate to handler class
AccountTriggerHandler.
Approach
- 1Check from highest to lowest to avoid overlap: > 10M first, then >= 1M, then >= 100K, else Startup.
- 2AnnualRevenue is a Decimal — use >= and > for boundary checks.
- 3A null AnnualRevenue falls to the else clause — no need for a separate null check if ordered correctly.
- 4Test all four tiers plus a null revenue record.
Easy
Triggers
16. Apex Trigger: Validate Contact Email Domain Before Save
Problem #217 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Contact
that rejects any Email address that does not end with @salesforce.com.
Requirements
- Fires on
before insert and before update.
- Skip Contacts where
Email == null — null emails are allowed.
- Call
c.Email.addError('Email must be a @salesforce.com address.')
to highlight the specific field.
- Store the allowed domain in a constant (
final String ALLOWED_DOMAIN).
Best Practices
- Use
.toLowerCase().endsWith() for case-insensitive comparison.
- Call
addError() on the field
(c.Email.addError()), not on the record — this highlights the field in the UI.
- Use a named constant for the domain string — never a magic string in the condition.
- No SOQL or DML needed — pure in-memory validation.
Approach
- 1Case-insensitive check: c.Email.toLowerCase().endsWith('@salesforce.com')
- 2Field-level error: c.Email.addError('message') highlights the Email field specifically.
- 3Skip nulls: if (c.Email == null) continue;
- 4Constant: final String ALLOWED_DOMAIN = 'salesforce.com'; — avoids magic strings.
Easy
Triggers
17. Trigger + Handler: Enforce Account Rating Based on Annual Revenue
Problem #226 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert / before update trigger on Account
with handler AccountRatingHandler that sets Rating based on
AnnualRevenue.
Rating Rules
- Hot — AnnualRevenue ≥ 10,000,000
- Warm — AnnualRevenue 1,000,000 – 9,999,999
- Cold — AnnualRevenue < 1,000,000 or null
Best Practices
- On updates, skip records where
AnnualRevenue has not changed.
- Handle null
AnnualRevenue explicitly (assign Cold).
- Tests cover all four paths: Hot, Warm, Cold (low), Cold (null), and unchanged update.
Approach
- 1Handler signature: public static void setRating(List newList, Map oldMap)
- 2Skip unchanged on update: if (oldMap != null && oldMap.get(acc.Id).AnnualRevenue == acc.AnnualRevenue) continue;
- 3Null check first: if (acc.AnnualRevenue == null) { acc.Rating = 'Cold'; }
- 4Test null revenue: new Account(Name='X') has no AnnualRevenue — should yield Cold.
Easy
Triggers
18. Apex Trigger: Prevent Deletion of Closed Opportunities
Problem #230 · Salesforce Apex Coding Challenge
Problem Statement
Write a before delete trigger on Opportunity that
prevents any closed Opportunity (IsClosed = true) from being deleted.
Requirements
- Fires on
before delete — use Trigger.old.
- Loop
Trigger.old; if opp.IsClosed == true, call
opp.addError('Closed Opportunities cannot be deleted...').
- No SOQL or DML needed — pure in-memory check on the record being deleted.
Best Practices
- Use the formula field
IsClosed — it is automatically true for both
Closed Won and Closed Lost, so you do not need to check each stage name.
addError() on the record (not a field) prevents the delete and shows
the message to the user.
- No SOQL needed — all required data is available on
Trigger.old.
Approach
- 1IsClosed is a standard read-only formula field — true for both Closed Won and Closed Lost.
- 2addError on record: opp.addError('message') — prevents the delete and surfaces error to UI.
- 3No SOQL needed — all data is on Trigger.old already.
- 4Test: insert a Closed Won opp → try delete → expect DmlException.
Easy
Apex Trigger
19. Trigger + Handler: Default Lead Source to "Web" When Blank
Problem #287 · Salesforce Apex Coding Challenge
Problem Statement
Implement a Trigger + Handler that automatically sets
LeadSource to 'Web' when a Lead is
inserted without a source.
Trigger Requirements
- Fire on before insert only
- No logic in the trigger — delegate to the handler
Handler Requirements
- Store the default value in a named constant
- Only set
LeadSource when it is blank/null — never overwrite an existing value
- Bulk-safe: iterate over the full list
Test Class Requirements
- Assert blank source defaults to
'Web'
- Assert existing source is preserved
- Bulk test with 200 leads
Approach
- 1Fire the trigger on before insert only — there is no update scenario here.
- 2Use String.isBlank(ld.LeadSource) to catch both null and empty string.
- 3Store 'Web' in a constant so it is easy to change and self-documenting.
- 4The bulk test asserts all 200 leads — query back with WHERE Id IN :leads.
Easy
Triggers
20. Trigger: Prevent Account Deletion with Related Opportunities
Problem #324 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger PreventAccountDeletion on the Account
object that fires before delete.
If the Account being deleted has one or more related Opportunity records,
call addError() on that Account to block the deletion with the message:
"Cannot delete an account with related opportunities."
Requirements
- Bulk-safe: handle multiple Account deletions in one transaction.
- Use a single SOQL query outside any loop.
- Add the error on the Account record, not a field.
Expected Behaviour
- Account with 0 Opportunities → deleted successfully.
- Account with ≥ 1 Opportunity → blocked with error message.
Approach
- 1Use
Trigger.oldMap.keySet() to get the set of Account IDs being deleted. - 2Query the related Opportunities for the whole batch of Account IDs in a single bulk call outside any loop — you only need enough fields to know which Accounts have at least one.
- 3If the list is not empty, loop over
Trigger.old and call acc.addError('Cannot delete an account with related opportunities.'). - 4A before-delete trigger lets you call
addError() directly on the sObject — no DML needed.
Easy
Triggers
21. Trigger: Prevent Account Deletion with Related Contacts
Problem #325 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger PreventAccountDeletionContacts on Account
that fires before delete.
If any Account being deleted has one or more related Contact records, block
deletion with addError() and the message:
"Cannot delete an account with related contacts."
Requirements
- Bulk-safe — use aggregate SOQL, not a per-record query.
- Use a
Map<Id, Integer> to track counts per Account.
Approach
- 1Use an aggregate query on Contact that counts related records per Account, for the whole batch of IDs, in a single call.
- 2Cast aggregate results:
(Id)ar.get('AccountId') and (Integer)ar.get('cnt'). - 3Store in a
Map<Id, Integer> then check countMap.containsKey(acc.Id) inside the loop. - 4Call
acc.addError('Cannot delete an account with related contacts.') when count > 0.
Easy
Triggers
22. Trigger: Set Contact_Created__c Checkbox on Account
Problem #330 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger AccountForContact on Contact
that fires after insert.
When a Contact is inserted with a non-null AccountId, set the checkbox
field Contact_Created__c = true on the related Account.
Requirements
- Collect all Account IDs from the batch in one pass.
- Query only Accounts that need to be updated.
- Issue a single bulk DML update.
Approach
- 1Add the AccountId as a key to a
Map<Id, Account> with a null value initially. - 2Query the Accounts that correspond to the keys already collected in
accountMap, in a single bulk call, retrieving the checkbox field you need to set. - 3Set
acc.Contact_Created__c = true and store the Account back in the map. - 4Call
update accountMap.values() — the map now holds only Accounts that need updating.
Easy
Triggers
23. Trigger: Auto-Create Default Contact on Account Insert
Problem #332 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger DefaultContactOnAccount on Account
that fires after insert.
For every newly created Account, automatically create a default Contact
with the following values:
FirstName = 'Info'
LastName = 'Default'
Email = 'info@websitedomain.tld'
AccountId = the new Account's Id
Requirements
- Create one Contact per Account in the batch.
- Use a single bulk insert — no insert inside a loop.
Approach
- 1Inside the loop, create
new Contact(FirstName='Info', LastName='Default', Email='info@websitedomain.tld', AccountId=acc.Id). - 2Add each Contact to
contactsToInsert. - 3Issue a single
insert contactsToInsert outside the loop. - 4Guard with
if (!contactsToInsert.isEmpty()) before the DML.
Easy
Triggers
24. Trigger: Auto-Assign High Priority Cases to a Support Queue
Problem #335 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger AutoAssignCaseOwner on Case
that fires before insert.
If a Case is being created with Priority = 'High', automatically
assign its OwnerId to the Id of the queue named
'High Priority Cases'.
Requirements
- Query the
Group object where Type = 'Queue' and
Name = 'High Priority Cases'.
- Guard against the queue not existing — only assign if the queue is found.
- This is a before trigger — set
OwnerId directly on the Case record.
Approach
- 1Query the
Group object to find the specific queue named in the requirements, filtering to only Queue-type groups, and limit it to the one record you need. - 2Store results in a
List<Group> and check !queues.isEmpty() before using the Id. - 3Inside the loop:
if (c.Priority == 'High') c.OwnerId = queueId;. - 4SOQL must be outside the loop — run it once before iterating Trigger.new.
Easy
Triggers
25. Trigger: Prefix First Name with Dr for New/Updated Leads
Problem #338 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger PrefixDrTrigger on the Lead object that fires
before insert and before update.
For every Lead, if FirstName is not null and does not already start with
'Dr ', prepend 'Dr ' to FirstName.
Requirements
- Fires on both insert and update.
- Skip leads where FirstName is null or already starts with 'Dr '.
- Bulk-safe: handles multiple leads in one transaction.
Approach
- 1Use
before insert, before update — modify Trigger.new directly, no DML needed. - 2Check
l.FirstName != null before calling startsWith('Dr '). - 3Assign:
l.FirstName = 'Dr ' + l.FirstName;
Easy
Triggers
26. Trigger: Update Account Rating to Hot on Closed Won Opportunity
Problem #339 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger UpdateAccountRating on the Opportunity
that fires after insert and after update.
Whenever StageName is 'Closed Won', set the related Account's
Rating to 'Hot'.
Requirements
- Bulk-safe: one SOQL outside loop, one DML update.
- Only update accounts linked to Closed Won opportunities.
Approach
- 1Collect AccountIds where StageName == 'Closed Won'.
- 2Query the Accounts for that batch of accountIds in one call, retrieving the Rating field you need to set.
- 3Set
acc.Rating = 'Hot' then update accounts.
Easy
Triggers
27. Trigger: Append Phone Number to Account Name on Update
Problem #340 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger UpdateAccountNameWithPhone on the Account
that fires before update.
When the Phone field changes and is not null, set
Name = Name + ' - ' + Phone.
Requirements
- Only fire when Phone actually changed.
- Skip if the new Phone is null.
- Bulk-safe.
Approach
- 1Use
Trigger.oldMap.get(acc.Id) to access previous Phone. - 2Set
acc.Name = acc.Name + ' - ' + acc.Phone — no DML in before trigger.
Easy
Triggers
28. Trigger: Sync City Field from Account to Related Opportunities
Problem #341 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger SyncAccountCityToOpportunities on the Account
that fires after update.
When City__c changes, update all related Opportunity City__c
to match.
Requirements
- Only sync when City__c actually changes.
- Bulk-safe: one SOQL, one DML.
- Use Trigger.newMap to get the new city value for each Opportunity.
Approach
- 1Collect acc.Id when
acc.City__c != Trigger.oldMap.get(acc.Id).City__c. - 2Query the related Opportunities for the changed accountIds in one call, retrieving the AccountId and City__c fields you need to sync.
- 3Set
opp.City__c = Trigger.newMap.get(opp.AccountId).City__c.
Easy
Triggers
29. Trigger: Cascade Custom Phone from Opportunity to Account and Contact
Problem #342 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger CascadePhoneFromOpportunity on the Opportunity
that fires after update.
When TestPhoneOpportunity__c changes, sync the new value to the related
Account's TestPhoneAccount__c and all related Contacts'
TestPhoneContact__c.
Requirements
- One SOQL with parent-to-child Contacts subquery.
- Bulk-safe: one SOQL, separate DML for accounts and contacts.
Approach
- 1Detect change:
opp.TestPhoneOpportunity__c != Trigger.oldMap.get(opp.Id).TestPhoneOpportunity__c. - 2Query the Accounts for the changed accountIds in one call, using a parent-to-child subquery to pull their related Contacts along with it so both can be updated without a second query.
Easy
Triggers
30. Trigger: Add Follow-Up Task When Opportunity Reaches Closed Won
Problem #343 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger AddTaskOnClosedWon on the Opportunity
that fires after insert and after update.
When StageName becomes 'Closed Won' (and was NOT already Closed Won on update),
create a Task: Subject = 'Follow Up Test Task', WhatId = Opportunity Id,
Status = 'Not Started', Priority = 'Normal'.
Requirements
- On update: only create task when StageName transitions TO 'Closed Won'.
- Bulk-safe: collect tasks, insert once.
Approach
- 1Check
opp.StageName == 'Closed Won'. - 2On update: skip if
Trigger.oldMap.get(opp.Id).StageName == 'Closed Won'. - 3Create:
new Task(Subject='Follow Up Test Task', WhatId=opp.Id, Status='Not Started', Priority='Normal').
Easy
Triggers
31. Trigger: Divide Account Balance Equally Among Related Contacts
Problem #344 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger DivideAccountBalance on the Account
that fires after insert and after update.
For each Account with Balance__c > 0 and related Contacts, divide
Balance__c equally by setting each Contact's
IndividualBalance__c.
Requirements
- Use parent-to-child SOQL with Contacts subquery.
- Skip Accounts with no Contacts.
- Bulk-safe: one SOQL, one DML.
Approach
- 1Divide the Account's Balance__c by its number of related Contacts to get an equal share — use a Decimal so the split isn't truncated.
- 2Inner loop contacts, set IndividualBalance__c = share, add to update list.
Easy
Triggers
32. Trigger: Auto-Create Contact for High Annual Revenue Accounts
Problem #345 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger AddContactForHighRevenue on the Account
that fires after insert.
For every new Account where AnnualRevenue > 50000, create a Contact with
FirstName = 'Smriti', LastName = 'Sharan', AccountId = Account Id.
Requirements
- Only fires on insert.
- Only create contact when AnnualRevenue is not null and > 50000.
- Bulk-safe: collect contacts, insert once.
Approach
- 1Null-check:
acc.AnnualRevenue != null && acc.AnnualRevenue > 50000. - 2Create:
new Contact(FirstName='Smriti', LastName='Sharan', AccountId=acc.Id).
Easy
Triggers
33. Trigger: Auto-Create Contact for Banking Industry Accounts
Problem #346 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger CreateContactForBanking on the Account
that fires after insert.
For every new Account where Industry == 'Banking', create a Contact with
LastName = Account Name, Phone = Account Phone, AccountId = Account Id.
Requirements
- Fires only on insert, only when Industry = 'Banking'.
- Bulk-safe: collect contacts, insert once.
Approach
- 1Check
acc.Industry == 'Banking'. - 2Create:
new Contact(LastName=acc.Name, Phone=acc.Phone, AccountId=acc.Id).
Easy
Triggers
34. Trigger: Set Case Status and Priority for Email Origin Cases
Problem #347 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger SetCaseFieldsOnEmailOrigin on the Case
that fires before insert.
For every new Case where Origin == 'Email', set
Status = 'New' and Priority = 'Normal'.
Requirements
- Fires only on insert, only when Origin = 'Email'.
- No DML needed — it is a before trigger.
- Bulk-safe.
Approach
- 1Check
c.Origin == 'Email'. - 2Set fields directly — no DML in a before trigger.
Easy
Triggers
35. Trigger: Set Lead Rating Based on Lead Source
Problem #348 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger SetLeadRatingBySource on the Lead
that fires before insert.
For every new Lead: if LeadSource == 'Web' → Rating = 'Hot';
otherwise → Rating = 'Cold'.
Requirements
- Fires only on insert. No DML needed. Bulk-safe.
Approach
- 1Use
if/else: LeadSource == 'Web' → Rating = 'Hot', else → Rating = 'Cold'.
Easy
Triggers
36. Trigger: Set Contact OtherPhone from Parent Account Phone on Insert
Problem #349 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger SetContactOtherPhone on the Contact
that fires before insert.
For every new Contact with an AccountId, look up the related Account's
Phone and set the Contact's OtherPhone to that value.
Requirements
- Skip contacts without AccountId.
- One SOQL outside loop, Map for lookups.
- No DML — before trigger.
Approach
- 1Query the related Accounts for the batch of accountIds in one call, pulling back their Phone field.
- 2Wrap that query directly in a
new Map<Id, Account>(...) constructor so you get an Id-keyed lookup in one step. - 3Check containsKey before accessing the map.
Easy
Triggers
37. Trigger: Assign Account Owner When Industry Is Education
Problem #350 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger AssignOwnerByIndustry on the Account
that fires before insert.
For every new Account where Industry == 'Education', assign ownership to
the User with username 'smriti@sfdc.com'.
Requirements
- Query User once outside loop. Guard if not found.
- Only assign when Industry = 'Education'.
Approach
- 1Query the User object once, outside the loop, filtering to the target username from the requirements, and limit it to one record.
- 2Guard:
if (users.isEmpty()) return; - 3Check
acc.Industry == 'Education' then set OwnerId.
Easy
Triggers
38. Trigger: Cascade Contact AssistantPhone to Account and Opportunity
Problem #351 · Salesforce Apex Coding Challenge
Problem Statement
Write an Apex trigger CascadeAssistantPhone on the Contact
that fires after update.
When AssistantPhone changes and is not null, sync it to the related
Account's Phone and all related Opportunities' Phone__c.
Requirements
- Only sync when AssistantPhone actually changes.
- One SOQL with Account + Opportunities subquery. Bulk-safe.
Approach
- 1Detect:
c.AssistantPhone != null && c.AssistantPhone != old.AssistantPhone. - 2Query the Accounts for the batch of accountIds in one call, using a parent-to-child subquery to bring back each Account's related Opportunities (Id and Phone__c) alongside it.
- 3Update accounts then oppsToUpdate separately.
Easy
TriggersValidation
39. Set Account Rating to Hot on High Annual Revenue
Problem #365 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert, before update trigger on the standard object
Account that automatically sets a custom field Rating__c to
"Hot" whenever AnnualRevenue exceeds 1,000,000.
Best Practices
- Separate trigger logic into a handler class (
AccountTriggerHandler).
- Field-only logic belongs in before context — no DML/SOQL needed.
- Bulkify: handle up to 200 records in one trigger invocation.
Objects / Fields
Account — AnnualRevenue (standard Currency), Rating__c (custom Picklist: Hot/Warm/Cold)
Approach
- 1Loop through Trigger.new (works for both insert and update since it is before-context).
- 2Check acc.AnnualRevenue != null && acc.AnnualRevenue > 1000000.
- 3Set acc.Rating__c = 'Hot' directly on the in-memory record — no DML needed in before triggers.
- 4Test: one Account above the threshold, one below, and a bulk list of 50 mixed records.
Easy
TriggersValidation
40. Prevent Opportunity Closed Won Without an Amount
Problem #366 · Salesforce Apex Coding Challenge
Problem Statement
Write a before update trigger on the standard object Opportunity
that blocks the StageName from being changed to "Closed Won" if
Amount is null or 0.
Add a field error on Amount using addError().
Best Practices
- Only trigger the check on an actual transition into Closed Won (compare against
Trigger.oldMap).
- Handler class pattern, bulk-safe for up to 200 records.
Objects / Fields
Opportunity — StageName, Amount (both standard)
Approach
- 1Compare opp.StageName to oldMap.get(opp.Id).StageName to detect a real transition into Closed Won.
- 2Only block when the OLD stage was not already Closed Won (avoid re-blocking on unrelated edits).
- 3Call opp.Amount.addError('...') so the error surfaces on the Amount field.
- 4Test: transition with null Amount (blocked), with a positive Amount (allowed), and an unrelated field edit while already Closed Won (allowed).
Easy
TriggersValidation
41. Prevent Duplicate Lead by Email
Problem #367 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert trigger on the standard object Lead
that checks for duplicate leads based on Email and blocks insertion if a Lead
with that email already exists.
Best Practices
- Collect all incoming emails into a
Set<String> — one SOQL query total.
- Never query per record inside a loop.
Objects / Fields
Approach
- 1Build a Set of non-null emails from Trigger.new.
- 2Query existing Leads by email in one batched call — never per record inside a loop.
- 3Store the matches in a Set for O(1) lookups, then loop again to call addError() where matched.
- 4Test: insert a Lead, then attempt a duplicate email and a unique email in the same handler call.
Easy
TriggersAutomation
42. Auto-Generate Sequential Invoice Number
Problem #368 · Salesforce Apex Coding Challenge
Problem Statement
Write a before insert trigger on the custom object Invoice__c
that auto-generates a unique Invoice_Number__c using the pattern
INV-00001, INV-00002, etc.
Best Practices
- Use one aggregate SOQL (
MAX(...)) to find the last used sequence — no per-record query.
- Pad the number using
String.leftPad().
- Production caveat: this pattern has a race condition under concurrent inserts.
A native Auto Number field type is safer at scale — this exercise is for learning the
aggregate-query + padding technique.
Objects / Fields
Invoice__c — Invoice_Sequence__c (Number), Invoice_Number__c (Text)
Approach
- 1A single aggregate query for the highest existing sequence, run once outside any loop, tells you where to resume numbering.
- 2Increment a running counter for each new Invoice__c in the batch (so a bulk insert of 5 gets 5 unique numbers).
- 3Format with String.valueOf(seq).leftPad(5, '0') and prefix "INV-".
- 4Test: first invoice → INV-00001; insert it; second invoice in a new call → INV-00002.
Easy
TriggersValidation
43. Prevent Employee From Being Their Own Manager
Problem #369 · Salesforce Apex Coding Challenge
Problem Statement
Write a before update trigger on the custom object Employee__c
that validates Manager__c is not self-referencing — an employee cannot be their
own manager.
Best Practices
- Only relevant on before update — on insert, the record has no Id yet,
so self-reference is impossible at that point.
- Pure in-memory check — no SOQL required.
Objects / Fields
Employee__c — Manager__c (self-lookup to Employee__c)
Approach
- 1Loop through Trigger.new (the newList passed into the handler).
- 2Compare emp.Manager__c to emp.Id — if equal and not null, that is a self-reference.
- 3Call emp.addError('An employee cannot be their own manager.')
- 4Test: self-reference (blocked), a different manager (allowed), and a bulk mix of both.
Easy
Triggers
44. Auto-Create Case When VIP Account Is Downgraded
Problem #545 · Salesforce Apex Coding Challenge
Problem Statement
Write a trigger CreateCaseOnVipDowngrade on Account that
fires after update.
Whenever an Account's Customer_Tier__c changes from 'VIP' to
'Standard', automatically insert a follow-up Case for that
Account.
Requirements
- Only create a Case when the tier actually transitions VIP → Standard.
- Collect all Cases into a list and insert once, outside any loop.
Approach
- 1Compare
Trigger.oldMap.get(acc.Id).Customer_Tier__c to acc.Customer_Tier__c. - 2Build a
List<Case> inside the loop, then call insert once after the loop.
Easy
Triggers
45. Prevent Contact Creation Without Mobile Number for Premium Accounts
Problem #546 · Salesforce Apex Coding Challenge
Problem Statement
Write a trigger PreventContactWithoutMobileForPremium on
Contact that fires before insert.
If a new Contact's parent Account has Customer_Tier__c = 'Premium' and
the Contact's MobilePhone is blank, block the insert with
addError().
Requirements
- Use a single bulk SOQL query against Account, outside any loop.
- Bulk-safe: handle multiple Contact inserts in one transaction.
Approach
- 1Use
String.isBlank(con.MobilePhone) to check for a missing value. - 2Query
Account WHERE Id IN :accountIds AND Customer_Tier__c = 'Premium' once, then check membership with a Set.
Easy
Triggers
46. Create Renewal Task After Contract Activation
Problem #548 · Salesforce Apex Coding Challenge
Problem Statement
Write a trigger CreateTaskOnContractActivation on Contract
that fires after update.
When a Contract's Status transitions to 'Activated',
automatically create a follow-up Task assigned to the Contract's
OwnerId for renewal planning.
Requirements
- Only fire when the status actually becomes Activated (not if it was already Activated).
- Collect Tasks into a list and insert once, outside any loop.
Approach
- 1Compare
Trigger.oldMap.get(con.Id).Status to con.Status to detect the transition into Activated. - 2Set
Task.WhatId to the Contract's Id so the Task shows up related to it.
Easy
Triggers
47. Prevent Case Reopening After Final Closure
Problem #549 · Salesforce Apex Coding Challenge
Problem Statement
Write a trigger PreventCaseReopenAfterFinalClosure on Case
that fires before update.
If a Case was Closed with its Final_Closure__c checkbox selected, block
any attempt to change its Status away from 'Closed'.
Requirements
- Only block when the Case was actually Closed AND flagged as a final closure.
- Editing other fields on a finally-closed Case (without reopening it) must still work.
Approach
- 1Read the case's prior state from
Trigger.oldMap.get(c.Id). - 2Three conditions must ALL be true to block: old Status was Closed, old Final_Closure__c was true, and the new Status is no longer Closed.
Easy
Triggers
48. Scenario: Flag High-Value Accounts for VIP Support
Problem #435 · Salesforce Apex Coding Challenge
Business Scenario
Support wants to proactively fast-track large customers. Whenever an Account is saved,
set its checkbox VIP_Support__c to true if
both AnnualRevenue >= 5,000,000 and
NumberOfEmployees >= 250 — otherwise false.
Requirements
- Runs on both insert and update, before save (no DML needed for the current record).
- Both thresholds must hold — a huge company with no revenue on file, or a small
high-revenue company, should not qualify.
- Null
AnnualRevenue/NumberOfEmployees must never qualify.
Approach
- 1A before-insert/before-update trigger can set fields on Trigger.new directly — no DML statement is needed to persist them.
- 2Guard both AnnualRevenue and NumberOfEmployees against null before comparing — a null Decimal compared with >= behaves unexpectedly.
- 3Use named constants (VIP_REVENUE_THRESHOLD, VIP_EMPLOYEE_THRESHOLD) instead of bare numbers for readability and easy tuning.
Easy
Triggers
49. Scenario: Standardize Contact Phone Numbers on Save
Problem #436 · Salesforce Apex Coding Challenge
Business Scenario
Sales reps enter Contact phone numbers in inconsistent formats
(1234567890, 123-456-7890, (123) 456 7890...).
Standardize every 10-digit US phone number to (XXX) XXX-XXXX on save.
Requirements
- Strip all non-digit characters first, then reformat.
- Only reformat when there are exactly 10 digits — leave anything else
(international numbers, extensions, partial numbers) untouched.
- Skip blank
Phone values entirely.
Approach
- 1String.isNotBlank(c.Phone) skips records with no phone number at all.
- 2c.Phone.replaceAll('[^0-9]', '') strips every character that isn't a digit.
- 3Only rebuild the formatted string when digitsOnly.length() == 10 — anything else should be left as-is.
- 4String.substring(start, end) lets you slice out the area code, prefix, and line number.
Easy
Triggers
50. Scenario: Default Case Origin for Web-Sourced Records
Problem #437 · Salesforce Apex Coding Challenge
Business Scenario
Cases created through the company's public web form arrive with
SuppliedEmail populated (a self-service field) but sometimes no
Origin set. Default Origin to "Web" whenever a new
Case has a SuppliedEmail but no Origin yet.
Requirements
- Only applies on creation — never override an Origin an agent later changes.
- Never override an
Origin the web form (or an integration) already set.
- Only defaults when
SuppliedEmail is present — internally-created Cases
with no SuppliedEmail are unaffected.
Approach
- 1A before insert trigger is enough here — this defaulting logic should never run on update.
- 2String.isBlank(c.Origin) checks that no Origin was already supplied.
- 3Only default when c.SuppliedEmail != null — that's the signal this Case came from the web form.
Easy
Triggers
51. Update Account Industry Based on the First Opportunity Created
Problem #469 · Salesforce Apex Coding Challenge
Business Scenario
Data quality wants every Account's Industry field backfilled automatically
the first time an Opportunity is created for it, inferring the industry from the
Opportunity's LeadSource (e.g. a simple lookup table). Accounts that already
have an Industry must be left alone.
Requirements
- After insert trigger on
Opportunity.
- Only update Accounts whose
Industry is currently blank.
- If multiple new Opportunities in the same batch reference the same Account, only one
update per Account should be attempted.
Best Practices
- Query Accounts once with
WHERE Industry = null so the trigger never
touches an Account that already has a value — no redundant/overwriting updates.
- Bulkify with Sets and Maps: one SOQL query and one DML statement regardless of how
many Opportunities/Accounts are in the batch.
- Wrap the update in
try/catch(DmlException).
Approach
- 1Query Accounts with Industry = null first — this both bulkifies the "already has a value" check and avoids a wasted update.
- 2Use a Map keyed by AccountId so multiple Opportunities on the same new Account only produce one update.
- 3A small private method (or inline if/else chain) can map LeadSource values like "Web" or "Referral" to an Industry string.
Easy
Triggers
52. Automatically Create a Welcome Task for New Leads
Problem #470 · Salesforce Apex Coding Challenge
Business Scenario
Sales wants every new Lead to automatically get a follow-up Task assigned to its owner,
due the next day, so no new Lead is ever missed.
Requirements
- After insert trigger on
Lead.
- Create one
Task per Lead: subject referencing the Lead's name, related
to the Lead via WhoId, owned by the Lead's owner, due tomorrow.
- Must handle a bulk insert of up to 200 Leads in one operation.
Best Practices
- Build the full list of Tasks in memory first, then insert once — never insert inside
the loop.
- Wrap the DML in
try/catch(DmlException) so one problematic Task (e.g. an
owner without task-creation permission) doesn't throw an unhandled exception that blocks
the whole trigger.
Approach
- 1WhoId links a Task to a Lead or Contact (polymorphic) — use ld.Id here.
- 2Date.today().addDays(1) gives tomorrow's date for ActivityDate.
- 3Collect every Task into tasksToInsert inside the loop, then insert once after the loop ends.
Easy
Triggers
53. Update Account Last Purchase Date After Opportunity Close
Problem #471 · Salesforce Apex Coding Challenge
Business Scenario
Account Management wants a custom field Last_Purchase_Date__c (Date) on
Account automatically updated whenever one of its Opportunities transitions
into Closed Won, reflecting the most recent purchase date.
Requirements
- After update trigger on
Opportunity.
- Only act on a real transition into Closed Won (was not already Closed Won).
- If several Opportunities for the same Account close in the same transaction, use the
latest
CloseDate among them.
Best Practices
- Detect the transition using an index loop over
Trigger.new/Trigger.old
rather than Trigger.oldMap, since both lists are already index-aligned in an
update context.
- Aggregate the latest CloseDate per Account in a Map before any DML — one bulk update
regardless of how many Opportunities closed.
- Guard the update with a not-empty check and wrap it in
try/catch(DmlException).
Approach
- 1Compare newOpp.StageName == "Closed Won" && oldOpp.StageName != "Closed Won" to detect the transition.
- 2Keep only the latest CloseDate per Account: compare against the current Map value before overwriting.
- 3Build the Account update list from latestCloseDateByAccount.keySet(), one Account per key.
About ApexArena
ApexArena is a free, browser-based Salesforce Apex coding practice platform covering every major topic tested on the Salesforce Platform Developer I (PD1) and Platform Developer II (PD2) certification exams. All problems run directly in your browser with instant pass/fail feedback, best-practice linting (SOQL in loops, DML in loops, empty catch blocks), and governor limit monitoring — no Salesforce Developer Edition org required.
Related tutorials:
Apex Triggers ·
SOQL ·
Batch Apex ·
Interview Q&A ·
Governor Limits