name: territory-api-and-assignment description: "Territory2 API in Apex: programmatic territory member management (UserTerritory2Association, ObjectTerritory2Association), bulk assignment DML, and SOAP-based rule evaluation. NOT for ETM admin setup, territory model design, declarative assignment rules, or Opportunity territory configuration (use admin/enterprise-territory-management)." category: apex salesforce-version: "Spring '25+" well-architected-pillars:
- Performance
- Reliability
- Security triggers:
- "how do I assign users to a territory in Apex"
- "programmatically add accounts to a territory using ObjectTerritory2Association"
- "bulk territory assignment rule evaluation from Apex"
- "UserTerritory2Association DML insert not working in async context"
- "how to remove a user from a territory via code without manual setup" tags:
- territory2
- etm
- UserTerritory2Association
- ObjectTerritory2Association
- territory-assignment
- apex-dml inputs:
- Salesforce org with Enterprise Territory Management enabled and at least one active Territory2Model
- Territory2 record IDs (territory members or accounts to assign)
- User IDs or Account IDs to associate with territories
- Whether bulk assignment rule evaluation is needed (requires SOAP API callout) outputs:
- Apex DML patterns for inserting/deleting UserTerritory2Association and ObjectTerritory2Association records
- Guidance on when and how to invoke assignment rule evaluation via SOAP callout
- Checker report flagging territory API anti-patterns in existing Apex classes dependencies: [] version: 1.0.0 author: Pranav Nagrecha updated: 2026-04-06
Territory API and Assignment
This skill activates when a practitioner needs to manage Enterprise Territory Management (ETM) memberships or account territory associations programmatically in Apex — by inserting or deleting UserTerritory2Association or ObjectTerritory2Association records — or when bulk territory assignment rule evaluation must be triggered from code via a SOAP API callout. Use it for automated onboarding/offboarding of territory reps, data-migration-driven bulk account assignments, and any scenario where territory membership must be driven by code rather than manual setup.
Before Starting
Gather this context before working on anything in this domain:
- Confirm Enterprise Territory Management (ETM) is enabled in the org and that at least one Territory2Model is in Active state. ETM-related objects are not queryable until the feature is on and a model is active.
- Identify the
Territory2record IDs you need to work with. You cannot use Territory2 names directly in DML; you must resolve IDs first. - Determine whether you need to evaluate assignment rules after inserting
ObjectTerritory2Associationrows. Assignment rules do not run automatically on DML toObjectTerritory2Association— rule evaluation requires a separate SOAP API callout. - Know the session ID constraints:
UserInfo.getSessionId()returnsnullin asynchronous contexts (Batch Apex, Queueable, Scheduled Apex, future methods). Any SOAP callout for rule evaluation must be made in a synchronous user context or from a Platform Event subscriber running in a synchronous context. - Know the 200-record-per-SOAP-call limit for bulk assignment rule evaluation requests.
Core Concepts
UserTerritory2Association
UserTerritory2Association is the junction object that places a user into a Territory2. It supports standard Apex DML (insert, delete) and can be queried with SOQL. Key fields:
| Field | Type | Description |
|---|---|---|
Territory2Id | ID | The territory the user belongs to |
UserId | ID | The user being assigned |
RoleInTerritory2 | String | Picklist value (e.g., Account Executive, empty string for no role) |
IsActive | Boolean | Read-only; controlled by the territory model state |
Inserting a UserTerritory2Association record is the programmatic equivalent of clicking "Add Users" in Setup > Territories. The insert fires standard field-level security checks but does not fire Apex triggers — there is no trigger support on UserTerritory2Association.
Duplicate inserts (same Territory2Id + UserId combination) produce a DUPLICATE_VALUE DML error. Use Database.insert(list, false) and inspect SaveResult errors to handle gracefully.
ObjectTerritory2Association
ObjectTerritory2Association places an Account (the only supported object type) into a Territory2. Key fields:
| Field | Type | Description |
|---|---|---|
ObjectId | ID | The Account ID being assigned |
Territory2Id | ID | The territory the account belongs to |
AssociationCause | String | Territory (manual/API) or Territory2RuleAssociation (rule-based; platform-controlled) |
Critical constraints:
- Standard DML (
insert,delete) is supported. - Apex triggers do not fire on
ObjectTerritory2Association— not before-insert, not after-insert, nothing. You cannot use triggers to react to territory-account associations. - Inserting with
AssociationCause = 'Territory'creates a manual/API-driven association. Associations created by assignment rules useTerritory2RuleAssociation(read-only from DML; set by the platform after rule evaluation). - Only Account records can be the
ObjectId. Associating Contacts, Opportunities, or other objects directly is not supported.
Assignment Rule Evaluation
ETM assignment rules evaluate which territories an Account belongs to based on account field values (e.g., BillingState, Industry). Rule evaluation is not triggered automatically when you insert ObjectTerritory2Association rows or update Account fields — you must explicitly request evaluation.
From Apex, rule evaluation requires a SOAP API callout to the Territory2 evaluation service endpoint. The pattern is:
- Obtain a valid session ID via
UserInfo.getSessionId()— available only in synchronous user-context Apex. - Build an HTTP callout to
/services/Soap/s/<api-version>/with the correct SOAP envelope. - Pass up to 200 Account IDs per request.
- Parse the SOAP response to determine success or fault.
Because UserInfo.getSessionId() returns null in async Apex, bulk rule evaluation for large datasets is typically orchestrated via a Platform Event published from a batch finish method, consumed by a synchronous trigger or flow that performs the callout.
Common Patterns
Pattern 1: Bulk User Territory Assignment from Apex
When to use: A territory assignment change (e.g., sales rep territory realignment, new hire onboarding) must be applied to many users at once from a migration script, process automation, or an admin tool.
How it works:
- Collect
(Territory2Id, UserId)pairs to add. - Query
UserTerritory2Associationto find which pairs already exist (avoidDUPLICATE_VALUE). - Build
UserTerritory2AssociationsObjects for net-new pairs. - Insert with
Database.insert(newAssociations, false)and inspectSaveResultfor any errors beyondDUPLICATE_VALUE. - For removals, query by
Territory2Id IN :ids AND UserId IN :userIds, then delete.
public static void assignUsersToTerritory(Id territory2Id, List<Id> userIds) {
Set<Id> existingUserIds = new Set<Id>();
for (UserTerritory2Association uta :
[SELECT UserId FROM UserTerritory2Association
WHERE Territory2Id = :territory2Id AND UserId IN :userIds]) {
existingUserIds.add(uta.UserId);
}
List<UserTerritory2Association> toInsert = new List<UserTerritory2Association>();
for (Id uid : userIds) {
if (!existingUserIds.contains(uid)) {
toInsert.add(new UserTerritory2Association(
Territory2Id = territory2Id,
UserId = uid,
RoleInTerritory2 = ''
));
}
}
if (!toInsert.isEmpty()) {
List<Database.SaveResult> results = Database.insert(toInsert, false);
for (Database.SaveResult sr : results) {
if (!sr.isSuccess()) {
for (Database.Error err : sr.getErrors()) {
if (err.getStatusCode() != StatusCode.DUPLICATE_VALUE) {
throw new DmlException('Territory user assignment failed: ' + err.getMessage());
}
}
}
}
}
}
Why not the alternative: Using insert without Database.insert(list, false) rolls back the entire transaction on the first duplicate, which is common in realignment scenarios where some users are already assigned.
Pattern 2: Manual Account-Territory Association via ObjectTerritory2Association
When to use: Accounts must be pinned to specific territories independent of assignment rules — for example, a data migration that preserves legacy territory assignments, or a key account that must always appear in an executive territory regardless of field values.
How it works:
- Resolve the
Territory2Idvalues for the target territories. - For each account, build an
ObjectTerritory2AssociationwithAssociationCause = 'Territory'. - Insert with
Database.insert(list, false)to handle duplicates. - Query
ObjectTerritory2Associationto verify the associations are in place.
public static void pinAccountsToTerritory(Id territory2Id, List<Id> accountIds) {
Set<Id> existingAccounts = new Set<Id>();
for (ObjectTerritory2Association ota :
[SELECT ObjectId FROM ObjectTerritory2Association
WHERE Territory2Id = :territory2Id AND ObjectId IN :accountIds
AND AssociationCause = 'Territory']) {
existingAccounts.add(ota.ObjectId);
}
List<ObjectTerritory2Association> toInsert = new List<ObjectTerritory2Association>();
for (Id acctId : accountIds) {
if (!existingAccounts.contains(acctId)) {
toInsert.add(new ObjectTerritory2Association(
ObjectId = acctId,
Territory2Id = territory2Id,
AssociationCause = 'Territory'
));
}
}
if (!toInsert.isEmpty()) {
Database.insert(toInsert, false);
}
}
Why not the alternative: Using assignment rules to pin accounts requires crafting a rule that matches a specific field value, which is fragile. Manual API associations via AssociationCause = 'Territory' are explicit and survive rule re-evaluation without being overwritten.
Pattern 3: SOAP-Based Assignment Rule Evaluation
When to use: After updating Account fields that affect territory rule criteria, you need to re-evaluate which territories those accounts belong to — from a synchronous user context where a session ID is available.
How it works:
- Collect Account IDs in chunks of at most 200.
- Build the SOAP envelope with the session ID from
UserInfo.getSessionId(). - Make an HTTP callout to the Territory2 evaluation endpoint.
- Parse the SOAP response for success or fault elements.
public static void evaluateRulesForAccounts(List<Id> allAccountIds) {
String sessionId = UserInfo.getSessionId();
if (sessionId == null) {
throw new IllegalStateException(
'evaluateRules requires a synchronous user context; '
+ 'UserInfo.getSessionId() returned null.'
);
}
String baseEndpoint = URL.getOrgDomainUrl().toExternalForm()
+ '/services/Soap/s/' + System.requestVersion().major + '.0';
Integer batchSize = 200;
for (Integer i = 0; i < allAccountIds.size(); i += batchSize) {
List<Id> chunk = allAccountIds.subList(i, Math.min(i + batchSize, allAccountIds.size()));
String body = buildSoapEnvelope(chunk);
HttpRequest req = new HttpRequest();
req.setEndpoint(baseEndpoint);
req.setMethod('POST');
req.setHeader('Content-Type', 'text/xml; charset=UTF-8');
req.setHeader('SOAPAction', '""');
req.setHeader('Authorization', 'Bearer ' + sessionId);
req.setBody(body);
HttpResponse resp = new Http().send(req);
if (resp.getStatusCode() != 200) {
throw new CalloutException('SOAP rule eval failed: ' + resp.getBody());
}
}
}
Why not the alternative: There is no native Apex method to trigger ETM assignment rule evaluation. The SOAP callout is the only supported mechanism from Apex code.
Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| Add a user to a territory programmatically | Insert UserTerritory2Association with Database.insert(list, false) | Standard DML; pre-query existing assignments to avoid DUPLICATE_VALUE |
| Remove a user from a territory | Query UserTerritory2Association by Territory2Id + UserId, then delete | Standard DML delete; no recalculation hook needed |
| Pin an account to a territory regardless of rules | Insert ObjectTerritory2Association with AssociationCause = 'Territory' | Manual associations survive rule re-evaluation |
| Re-evaluate which territories an account belongs to after field changes | SOAP API callout in synchronous context, chunked to ≤200 IDs per call | No native Apex method exists; rule evaluation is not automatic |
| React to a new territory-account association in code | Cannot use triggers on ObjectTerritory2Association; dispatch Platform Events from the service layer | Apex triggers never fire on ObjectTerritory2Association DML |
| Evaluate rules in async Apex (Batch, Queueable) | Publish a Platform Event in finish; consume in synchronous subscriber | Session ID unavailable in async context; SOAP callout must be synchronous |
| Bulk assignment for >200 accounts needing rule evaluation | Chunk into batches of 200, call SOAP sequentially in a loop | 200-record limit per SOAP call; 100 callout limit per transaction |
Recommended Workflow
Step-by-step instructions for an AI agent or practitioner activating this skill:
- Confirm ETM is enabled and the Territory2Model is in Active state — query
Territory2Modeland verifyState = 'Active'before attempting any DML on territory association objects. - Resolve Territory2 IDs — query
Territory2by name, external ID, or hierarchy criteria to obtain the IDs you will use in association records. - Check for existing associations — query
UserTerritory2AssociationorObjectTerritory2Associationbefore inserting to build a net-new list and avoidDUPLICATE_VALUEerrors. - Perform DML with
Database.insert(list, false)— inspectSaveResulterrors, ignoreDUPLICATE_VALUE, and surface all others. - If assignment rule evaluation is needed, confirm you are in a synchronous user context, chunk account IDs into groups of ≤200, and make the SOAP callout using
UserInfo.getSessionId(). - Validate the result — query
ObjectTerritory2AssociationorUserTerritory2Associationafter DML to confirm the records were created and spot partial failures. - Run
check_territory_api_and_assignment.pyagainst the Apex source to catch anti-patterns before deploying.
Review Checklist
Run through these before marking work in this area complete:
- ETM is enabled and at least one Territory2Model is in Active state in the target org
- All DML on
UserTerritory2AssociationandObjectTerritory2AssociationusesDatabase.insert(list, false)and inspectsSaveResult - No Apex triggers are relied upon to fire on
ObjectTerritory2AssociationDML - SOAP callout for rule evaluation is not called from async Apex (Batch, Queueable, Scheduled, future)
- SOAP requests are chunked to ≤200 account IDs per call
-
AssociationCauseis explicitly set to'Territory'for managed associations (never'Territory2RuleAssociation') - Deletion paths exist to remove stale associations when users leave territories or accounts are reassigned
- Session ID availability is verified before the SOAP callout path is invoked
Salesforce-Specific Gotchas
Non-obvious platform behaviors that cause real production problems:
-
Apex triggers do not fire on ObjectTerritory2Association — A trigger declared on
ObjectTerritory2Associationdeploys without error but its body never executes — not on DML from Apex, not from Setup, not from rule evaluation. Use Platform Events dispatched from the service layer that writes the association. -
UserInfo.getSessionId() returns null in all async Apex — This is the primary cause of SOAP-based rule evaluation failures in Batch Apex
execute, Queueableexecute,@futuremethods, and Scheduled Apexexecute. Any code path that depends ongetSessionId()for a SOAP callout must run in a synchronous request context. -
Assignment rules do not re-evaluate automatically on Account field updates or ObjectTerritory2Association DML — Updating
BillingStateor any other rule-relevant field on an Account does not trigger rule re-evaluation. You must explicitly call the SOAP evaluation endpoint. -
ObjectTerritory2Association DML with AssociationCause = 'Territory2RuleAssociation' throws a runtime error — This value is platform-controlled and cannot be set via Apex DML. Attempting to insert with this cause throws
DmlException: FIELD_INTEGRITY_EXCEPTION. Always setAssociationCause = 'Territory'explicitly. -
200-record-per-SOAP-call limit for rule evaluation — The SOAP API for territory rule evaluation caps at 200 Account IDs per request. Callers that pass more than 200 IDs receive a SOAP fault. Chunk the list into batches of ≤200 and make sequential requests.
Output Artifacts
| Artifact | Description |
|---|---|
UserTerritory2Association DML snippet | Apex pattern for bulk user-to-territory assignment with duplicate handling |
ObjectTerritory2Association DML snippet | Apex pattern for pinning accounts to territories with AssociationCause = 'Territory' |
| SOAP rule evaluation wrapper | Apex HTTP callout pattern for triggering ETM assignment rule evaluation (synchronous context only) |
check_territory_api_and_assignment.py report | Static analysis of Apex classes for territory API anti-patterns |
Related Skills
- admin/enterprise-territory-management — Use for ETM admin setup, territory model design, territory types, assignment rule configuration, and Opportunity territory assignment; this skill handles only the programmatic Apex API layer
- apex-managed-sharing — Use when territory membership drives record sharing via
__Shareobjects; the two patterns are often combined - callout-and-dml-transaction-boundaries — Use when the SOAP callout for rule evaluation must be combined with DML in the same transaction
- async-apex — Use when territory assignment operations exceed synchronous DML limits and must be moved to batch or queueable context (excluding SOAP callout steps)