This guide walks through 43 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.
Write a before insert / before update trigger on Account.
Set the Rating field based on AnnualRevenue:
Must handle bulk operations of up to 200 records without hitting governor limits.
Trigger.isBefore — true when running in a before contextTrigger.isInsert — true on insert eventsTrigger.isUpdate — true on update eventsTrigger.new — list of new/updated records (available on insert & update)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.
CloseDate against Date.today()opp.CloseDate.addError() with a descriptive messageCloseDate equal to today (same-day close is valid)CloseDate = yesterday → Error: "Close Date cannot be in the past." CloseDate = today → OK CloseDate = tomorrow → OK
Write a before insert / before update trigger on Employee__c
that validates the Salary__c field.
Use addError() on the field so the error appears inline in the UI.
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
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.
Website = null → unchanged (skip) Website = 'example.com' → 'https://example.com' Website = 'http://x.com' → unchanged Website = 'https://x.com' → unchanged
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().
Amount field, not on the recordWrite a before insert / before update trigger on Case
that automatically sets the Priority field based on keywords in the
Subject.
Matching is case-insensitive.
Write a before insert trigger on Lead that sets a custom
Lead_Score__c field based on LeadSource:
Write a before insert, before update trigger on Opportunity
that validates:
Amount must be greater than 0CloseDate must not be in the pastIf either condition fails, call addError() on the record with a descriptive message.
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."
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.
Write a before insert trigger on Lead that sets
default values for missing fields:
LeadSource is null, set it to "Web"Status is null, set it to "New"Rating is null, set it to "Cold"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.
before insert — do not trigger on updateWrite 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.NameContact.Phone = Account.PhoneContact.AccountId = the new account's IdWrite 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 |
Map<String, Integer> to store the stage-to-probability mappingProbability when StageName is non-null and exists in the mapProbability unchanged for stages not in the mapWrite 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.'
OpportunityTriggerHandler.Trigger.new, not a single record.CloseDate != null.Write a before insert / before update trigger on Account
that automatically sets a custom field Revenue_Tier__c based on
AnnualRevenue:
EnterpriseMid-MarketSMBStartupnull AnnualRevenue (classify as Startup).AccountTriggerHandler.Write a before insert / before update trigger on Contact
that rejects any Email address that does not end with @salesforce.com.
before insert and before update.Email == null — null emails are allowed.c.Email.addError('Email must be a @salesforce.com address.')
to highlight the specific field.final String ALLOWED_DOMAIN)..toLowerCase().endsWith() for case-insensitive comparison.addError() on the field
(c.Email.addError()), not on the record — this highlights the field in the UI.Write a before insert / before update trigger on Account
with handler AccountRatingHandler that sets Rating based on
AnnualRevenue.
AnnualRevenue has not changed.AnnualRevenue explicitly (assign Cold).Write a before delete trigger on Opportunity that
prevents any closed Opportunity (IsClosed = true) from being deleted.
before delete — use Trigger.old.Trigger.old; if opp.IsClosed == true, call
opp.addError('Closed Opportunities cannot be deleted...').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.Trigger.old.Implement a Trigger + Handler that automatically sets
LeadSource to 'Web' when a Lead is
inserted without a source.
LeadSource when it is blank/null — never overwrite an existing value'Web'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."
Trigger.oldMap.keySet() to get the set of Account IDs being deleted.SELECT Id FROM Opportunity WHERE AccountId IN :accountIds — one call outside any loop.Trigger.old and call acc.addError('Cannot delete an account with related opportunities.').addError() directly on the sObject — no DML needed.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."
Map<Id, Integer> to track counts per Account.SELECT AccountId, COUNT(Id) cnt FROM Contact WHERE AccountId IN :accIds GROUP BY AccountId.(Id)ar.get('AccountId') and (Integer)ar.get('cnt').Map<Id, Integer> then check countMap.containsKey(acc.Id) inside the loop.acc.addError('Cannot delete an account with related contacts.') when count > 0.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.
Map<Id, Account> with a null value initially.SELECT Id, Contact_Created__c FROM Account WHERE Id IN :accountMap.keySet().acc.Contact_Created__c = true and store the Account back in the map.update accountMap.values() — the map now holds only Accounts that need updating.Every problem above runs in a real in-browser Apex editor with instant pass/fail feedback and best-practice linting. No Salesforce org needed.
Create Free Account →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 Idnew Contact(FirstName='Info', LastName='Default', Email='info@websitedomain.tld', AccountId=acc.Id).contactsToInsert.insert contactsToInsert outside the loop.if (!contactsToInsert.isEmpty()) before the DML.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'.
Group object where Type = 'Queue' and
Name = 'High Priority Cases'.OwnerId directly on the Case record.SELECT Id FROM Group WHERE Name = 'High Priority Cases' AND Type = 'Queue' LIMIT 1.List<Group> and check !queues.isEmpty() before using the Id.if (c.Priority == 'High') c.OwnerId = queueId;.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.
before insert, before update — modify Trigger.new directly, no DML needed.l.FirstName != null before calling startsWith('Dr ').l.FirstName = 'Dr ' + l.FirstName;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'.
SELECT Id, Rating FROM Account WHERE Id IN :accountIds.acc.Rating = 'Hot' then update accounts.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.
Trigger.oldMap.get(acc.Id) to access previous Phone.acc.Name = acc.Name + ' - ' + acc.Phone — no DML in before trigger.Write an Apex trigger SyncAccountCityToOpportunities on the Account
that fires after update.
When City__c changes, update all related Opportunity City__c
to match.
acc.City__c != Trigger.oldMap.get(acc.Id).City__c.SELECT Id, AccountId, City__c FROM Opportunity WHERE AccountId IN :accountIds.opp.City__c = Trigger.newMap.get(opp.AccountId).City__c.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.
opp.TestPhoneOpportunity__c != Trigger.oldMap.get(opp.Id).TestPhoneOpportunity__c.SELECT Id, TestPhoneAccount__c, (SELECT Id, TestPhoneContact__c FROM Contacts) FROM Account WHERE Id IN :accountIds.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'.
opp.StageName == 'Closed Won'.Trigger.oldMap.get(opp.Id).StageName == 'Closed Won'.new Task(Subject='Follow Up Test Task', WhatId=opp.Id, Status='Not Started', Priority='Normal').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.
Decimal share = acc.Balance__c / acc.Contacts.size();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.
acc.AnnualRevenue != null && acc.AnnualRevenue > 50000.new Contact(FirstName='Smriti', LastName='Sharan', AccountId=acc.Id).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.
acc.Industry == 'Banking'.new Contact(LastName=acc.Name, Phone=acc.Phone, AccountId=acc.Id).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'.
c.Origin == 'Email'.Write an Apex trigger SetLeadRatingBySource on the Lead
that fires before insert.
For every new Lead: if LeadSource == 'Web' → Rating = 'Hot';
otherwise → Rating = 'Cold'.
if/else: LeadSource == 'Web' → Rating = 'Hot', else → Rating = 'Cold'.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.
SELECT Id, Phone FROM Account WHERE Id IN :accountIds.new Map<Id, Account>([SELECT Id, Phone FROM Account WHERE Id IN :accountIds]).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'.
SELECT Id FROM User WHERE Username = 'smriti@sfdc.com' LIMIT 1.if (users.isEmpty()) return;acc.Industry == 'Education' then set OwnerId.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.
c.AssistantPhone != null && c.AssistantPhone != old.AssistantPhone.SELECT Id, Phone, (SELECT Id, Phone__c FROM Opportunities) FROM Account WHERE Id IN :accountIds.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.
AccountTriggerHandler).Account — AnnualRevenue (standard Currency), Rating__c (custom Picklist: Hot/Warm/Cold)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().
Trigger.oldMap).Opportunity — StageName, Amount (both standard)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.
Set<String> — one SOQL query total.Lead — Email (standard)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.
MAX(...)) to find the last used sequence — no per-record query.String.leftPad().Invoice__c — Invoice_Sequence__c (Number), Invoice_Number__c (Text)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.
Employee__c — Manager__c (self-lookup to Employee__c)Write and run real Apex code right in your browser — instant pass/fail feedback, best-practice linting, and governor limit monitoring. No Salesforce org needed.
Create Free Account → Explore All ProblemsApexArena 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