Automatically assigns Account owners based on configurable geographic territory rules, with a round-robin fallback for accounts that don't match any rule. Includes a Lightning Web Component admin UI for managing rules, round-robin rotations, and manual overrides — all with a full audit log.
force-app/
├── territory-rule/ # Territory Rule engine, LWC, objects
When an Account is inserted or updated (segment, billing/shipping address changes), the trigger fires the assignment engine:
-
Rule Match — All active
Territory_Rule__crecords are evaluated inPriority__corder (ascending). A rule matches when the Account'sAccount_Segment__cequals the rule'sBusiness_Line__cand the billing or shipping address (controlled byAddress_Type__c) matches the rule'sCountry__candState__c. Blank rule fields act as wildcards. -
Round Robin Fallback — If no geographic rule matches, the engine looks up the
Round_Robin_State__crecord for the account's business line and cycles through the configured user list, advancing the index on each assignment. -
Audit Log — Every assignment attempt (matched or not, successful or failed) writes a
Territory_Assignment_Log__crecord with the method used, the matched rule name, and a human-readable notes field. -
Bypass — Any user holding the
Territory_Management_Bypasscustom permission skips trigger processing entirely.
| Object | Purpose |
|---|---|
Territory_Rule__c |
Stores geographic assignment rules — one record per territory/business-line combination |
Round_Robin_State__c |
Tracks the current rotation index and user list for each business line |
Territory_Assignment_Log__c |
Immutable audit trail of every assignment event |
| Field | Type | Description |
|---|---|---|
Business_Line__c |
Picklist | Must match Account.Account_Segment__c |
Country__c |
Text | Country to match (blank = any country) |
State__c |
Text | State/province to match (blank = any state) |
Address_Type__c |
Picklist | Billing, Shipping, or Either |
Assigned_User_Id__c |
Text | Salesforce User ID to assign as owner |
Assigned_User_Name__c |
Text | Auto-populated from the User record on save |
Priority__c |
Number | Lower number = evaluated first |
Is_Active__c |
Checkbox | Inactive rules are skipped entirely |
| Field | Type | Description |
|---|---|---|
Business_Line__c |
Text | Matches Account.Account_Segment__c |
User_Ids__c |
Long Text | Newline-separated list of Salesforce User IDs |
Last_Index__c |
Number | Index of the last assigned user; resets to -1 when user list changes |
| Field | Description |
|---|---|
Account__c |
Lookup to the assigned Account |
Assigned_To__c |
Lookup to the User who was assigned |
Assignment_Method__c |
Rule Match, Round Robin, or Manual |
Matched_Rule__c |
Name of the matched Territory_Rule__c (if applicable) |
Business_Line__c |
Business line at time of assignment |
Triggered_By__c |
Insert, Update, or Manual |
Notes__c |
Human-readable explanation of what happened |
| Field | Description |
|---|---|
Account_Segment__c |
Picklist — determines which business line rules and round-robin pool apply |
ARR_Bookings__c |
Currency — Annual Recurring Revenue / bookings tracking |
Core assignment engine. Called by the trigger handler and by the manual reassignment flow.
assign(List<Account> accounts, String triggerType)— Bulk-safe entry point. Evaluates rules and round-robin state for each account, then performs DML in a single pass.manualAssign(Id accountId, Id userId)—@AuraEnabledmethod for explicit owner overrides from the UI.
@AuraEnabled controller backing the territoryRuleManager LWC.
| Method | Description |
|---|---|
getRules(businessLine, activeOnly) |
Returns filtered, priority-sorted Territory_Rule__c records |
saveRule(rule) |
Upserts a rule; auto-resolves Assigned_User_Name__c from the User record |
deleteRule(ruleId) |
Deletes a single rule by ID |
getRoundRobinStates() |
Returns all Round_Robin_State__c records |
saveRoundRobinState(state) |
Upserts a round-robin state; resets Last_Index__c to -1 if the user list changes |
manualAssign(accountId, userId) |
Delegates to TerritoryAssignmentService.manualAssign() |
getAssignmentLog(accountId) |
Returns the 20 most recent log records for an account |
searchAccounts(searchKey) |
LIKE-based account search (up to 10 results) |
getUsersForBusinessLine(businessLine) |
Returns active users in a business line's round-robin pool |
getActiveUsers() |
Returns all active users (up to 200), sorted by name |
Thin handler class between the trigger and the service.
- Filters inserted accounts to only those with a populated
Account_Segment__c. - On update, fires only when
Account_Segment__c,BillingState,BillingCountry,ShippingState, orShippingCountryhas changed — avoids unnecessary processing. - Uses a static
isFirstRunflag to prevent recursive trigger execution. - Checks
Territory_Management_Bypasscustom permission viaFeatureManagement.checkPermission()before doing any work.
Single after insert, after update trigger on Account. Delegates directly to TerritoryTriggerHandler.
An admin UI surfaced as a Lightning App Page (also available on Record and Home pages).
Capabilities:
- View, create, edit, and delete
Territory_Rule__crecords filtered by business line and active status - Manage
Round_Robin_State__cuser pools per business line - Manually reassign an Account's owner and log the action
- View the assignment history log for any account
Targets: lightning__AppPage, lightning__RecordPage, lightning__HomePage
Grants the access needed to use the Territory Rule Manager UI.
| Access | Detail |
|---|---|
| Apex Classes | TerritoryRuleController, TerritoryAssignmentService |
Territory_Rule__c |
Create, Read, Edit, Delete |
Round_Robin_State__c |
Create, Read, Edit, Delete |
Territory_Assignment_Log__c |
Read only |
Account.Account_Segment__c |
Read/Edit |
All Territory_Rule__c fields |
Read/Edit |
All Round_Robin_State__c fields |
Read/Edit |
All Territory_Assignment_Log__c fields |
Read only |
| Tab | Territory_Management (Available) |
| Custom Permission | Territory_Management_Bypass (included so admins can also bypass if needed) |
| User Permission | ApiEnabled |
# Deploy Territory Rule package only
sf project deploy start --source-dir force-app/territory-rule
# Deploy both packages
sf project deploy start
# Run all territory tests
sf apex run test --test-level RunLocalTests --wait 10| Class | Coverage |
|---|---|
TerritoryRuleControllerTest |
TerritoryRuleController — all @AuraEnabled methods including error paths |
TerritoryAssignmentServiceTest |
TerritoryAssignmentService — rule match, round-robin, manual assign, edge cases |
AccountTerritoryTriggerTest |
End-to-end trigger → handler → service flow |
- Bulk safe — The assignment service processes all accounts in a single SOQL + DML pass; no per-record queries.
- Round-robin index resets — Changing the
User_Ids__clist on aRound_Robin_State__crecord resetsLast_Index__cto-1so the rotation starts fresh from the first user. - No assignment if no segment — Accounts with a blank
Account_Segment__care silently skipped by both the trigger handler and the assignment service. - Owner unchanged if already correct — The service only adds an account to the update list if
OwnerIdis actually changing, avoiding unnecessary DML. - Bypass permission — Assign
Territory_Management_Bypassto integration users or admins who manage account records but should not trigger reassignment.
