name: entitlement-apex-hooks description: "Use this skill when writing Apex triggers or classes that interact with CaseMilestone records — completing milestones, detecting violations, or reacting to SLA state changes. Trigger keywords: CaseMilestone trigger, auto-complete milestone Apex, milestone violation polling, CompletionDate write pattern. NOT for entitlement process admin setup, milestone configuration in Setup UI, or Flow-based milestone actions." category: apex salesforce-version: "Spring '25+" well-architected-pillars:
- Reliability
- Performance
- Operational Excellence triggers:
- "How do I auto-complete a CaseMilestone from an Apex trigger when a case status changes?"
- "CaseMilestone.IsCompleted is read-only — how do I mark a milestone as complete in Apex?"
- "How do I detect and react to SLA milestone violations in Apex without a native callback?" tags:
- entitlements
- milestones
- case-milestone
- sla
- apex-trigger
- service-cloud inputs:
- "List of MilestoneType names that the trigger should auto-complete"
- "Case status values that should trigger milestone completion"
- "Whether violation detection should be synchronous (trigger) or asynchronous (scheduled job)" outputs:
- "After-update trigger on Case that writes CompletionDate to open CaseMilestone records"
- "Scheduled Apex class that queries and processes violated milestones"
- "Test class covering IsCompleted read-only constraint and bulk DML patterns" dependencies: [] version: 1.0.0 author: Pranav Nagrecha updated: 2026-04-06
Entitlement Apex Hooks
Use this skill when you need Apex code that reads or writes to CaseMilestone records as part of entitlement and SLA enforcement. It covers the platform constraints that make this area unexpectedly difficult: IsCompleted is read-only, SlaExitDate is system-managed, and there is no native Apex callback for milestone violations.
Before Starting
Gather this context before working on anything in this domain:
- Confirm that Entitlement Management is enabled in the org (Setup > Entitlement Settings). Triggers on
CaseMilestonecompile but produce no records if entitlement processes are not active. - Know the exact
MilestoneType.Namevalues whose completion the trigger should control. Name strings are case-sensitive and must match what is configured in Setup. - Understand whether you need synchronous completion (trigger on Case field change) or asynchronous violation detection (scheduled Apex polling). These are separate patterns and require separate implementations.
Core Concepts
CaseMilestone.IsCompleted Is Read-Only
CaseMilestone.IsCompleted is a formula-derived field. The platform sets it to true automatically when CompletionDate is non-null. You cannot write IsCompleted = true in DML — the attempt compiles silently but the field is ignored at save time, leaving the milestone incomplete. The correct write is caseMilestone.CompletionDate = System.now() in an update DML call.
This is the single most common source of broken milestone automation. The field appears writable in the schema explorer because it is not marked as a formula field in the UI, but the Salesforce Entitlements Implementation Guide explicitly states that CompletionDate is the control field.
SlaExitDate Is System-Managed
CaseMilestone.SlaExitDate represents when the SLA window closes. It is calculated and maintained entirely by the platform based on the entitlement process definition and business hours. Apex cannot write to this field. Any attempt to set it in DML is silently discarded. If your requirement involves changing when a milestone deadline occurs, that change must be made through the entitlement process configuration in Setup, not through Apex.
No Native Apex Callback for Milestone Violations
Salesforce does not fire a trigger, platform event, or callback when a CaseMilestone transitions to a violated state. The IsViolated field on CaseMilestone is set by the platform when TargetDate < NOW() AND CompletionDate = null, but this state change does not cause a trigger execution. The only supported Apex pattern for acting on violations is a Scheduled Apex class that periodically queries:
SELECT Id, CaseId, MilestoneType.Name, TargetDate
FROM CaseMilestone
WHERE IsViolated = true
AND CompletionDate = null
Alternatively, you can use declarative milestone violation actions (email alerts, field updates) configured in the entitlement process — these are native and do not require Apex.
Trigger Context for CaseMilestone Completion
Milestone completion triggered by a case field change (e.g., Status becomes "Resolved") should be handled in an after update trigger on Case. The pattern is:
- Filter
Trigger.newrecords for cases where the relevant field changed. - Query
CaseMilestonerecords for those case IDs whereCompletionDate = nullandMilestoneType.Namematches the target types. - Set
CompletionDate = System.now()on each returned record. updatethe list.
The trigger must be after update (not before update) because writing CompletionDate requires a separate DML call. Writing to CaseMilestone in a before update trigger on Case is unsupported and will cause mixed-DML errors in certain contexts.
Common Patterns
Pattern: Auto-Complete Milestone on Case Status Change
When to use: A business rule requires that a specific milestone (e.g., "First Response") is marked complete when the case status changes to a particular value (e.g., "In Progress" or any non-new status).
How it works:
after updatetrigger onCasefires.- Collect IDs of cases where
Statuschanged to the target value. - Query
CaseMilestone WHERE CaseId IN :changedCaseIds AND CompletionDate = null AND MilestoneType.Name = 'First Response'. - For each result, set
CompletionDate = System.now(). updatetheCaseMilestonelist.
Why not the alternative: Setting IsCompleted = true directly appears to work (no DML exception) but the field value is discarded by the platform. The milestone stays open. This silent failure is the canonical trap in this domain.
Pattern: Violation Detection via Scheduled Apex
When to use: The org needs custom business logic when milestones are violated — for example, escalating to a manager, creating a task, or updating a related field — and the declarative milestone actions in the entitlement process are insufficient.
How it works:
- Implement
Schedulableand queryCaseMilestone WHERE IsViolated = true AND CompletionDate = null. - For each violated milestone, apply the business logic (create a task, send a notification, update the parent Case).
- Schedule the class to run at an appropriate interval (e.g., every 15–30 minutes via a cron expression).
- Guard against re-processing: add a custom checkbox field on
Case(e.g.,ViolationEscalated__c) and filter it out of the query to avoid duplicate actions.
Why not the alternative: There is no trigger or platform event that fires when IsViolated becomes true. Attempting to intercept this via a trigger on CaseMilestone itself will not work for violation detection because the field is set by a background platform process, not by a DML operation that would fire a trigger.
Decision Guidance
| Situation | Recommended Approach | Reason |
|---|---|---|
| Mark milestone complete when a Case field changes | after update trigger on Case writing CompletionDate = System.now() | Only supported write path; IsCompleted is read-only |
| React to milestone violations with custom logic | Scheduled Apex polling IsViolated = true | No native callback exists; polling is the only Apex path |
| Change when a milestone deadline occurs | Modify entitlement process configuration in Setup | SlaExitDate is system-managed and cannot be written by Apex |
| Send email or update fields on violation | Declarative milestone violation actions in entitlement process | No Apex required; native and more reliable than polling |
| Bulk-complete milestones for many cases | Batch Apex querying CaseMilestone and writing CompletionDate | Avoids trigger CPU/heap limits; respects governor limits per batch |
Recommended Workflow
Step-by-step instructions for an AI agent or practitioner working on this task:
- Confirm entitlement setup — Verify Entitlement Management is enabled and at least one entitlement process with milestones is active. Identify the exact
MilestoneType.Namestrings you need to target (case-sensitive). - Choose the correct pattern — Decide whether you need synchronous completion (trigger on Case field change) or asynchronous violation detection (Scheduled Apex). Document the requirement before writing code.
- Write the trigger or class — For completion:
after updateon Case, query openCaseMilestonerecords for affected cases, writeCompletionDate, bulk-safe DML. For violation detection:Schedulableclass, queryIsViolated = true AND CompletionDate = null, apply business logic with idempotency guard. - Write the test class — Cover: (a) the
CompletionDatewrite path succeeds, (b)IsCompletedreads back astrueafter the write, (c) bulk scenario with 200 cases, (d) theIsViolatedpolling query returns expected records in test context. - Validate in a sandbox with an active entitlement process —
CaseMilestonerecords only exist when a case has an active entitlement process applied. Tests that run without this setup will pass vacuously with empty lists. Use@testSetupto create the entitlement hierarchy. - Deploy and monitor — After deployment, verify milestone completion timestamps appear correctly on cases. For scheduled jobs, confirm the job is scheduled and check the Apex Jobs log for exceptions.
Review Checklist
Run through these before marking work in this area complete:
- Trigger writes
CompletionDate = System.now()— NOTIsCompleted = true - Trigger is
after updateon Case (notbefore update, not a trigger on CaseMilestone itself for completion) - No attempt to write
SlaExitDateanywhere in the code - Violation detection uses
IsViolated = truequery, not a trigger callback - Bulk-safe: trigger uses
Trigger.newlist, queries useIN :idSet, DML uses listupdate - Test class creates the full entitlement process hierarchy so
CaseMilestonerecords actually exist during test execution - Idempotency guard in scheduled violation handler (prevents double-escalation on the same milestone)
Salesforce-Specific Gotchas
Non-obvious platform behaviors that cause real production problems:
- IsCompleted silently ignores direct writes — Writing
caseMilestone.IsCompleted = truedoes not throw an exception but the value is discarded. The milestone remains incomplete. Always writeCompletionDateinstead. - SlaExitDate DML is silently discarded — Attempting to set
SlaExitDatein an update call produces no error but the field value is not persisted. The only way to change the milestone deadline is through the entitlement process setup in the admin UI. - No trigger fires on IsViolated state transition — The platform sets
IsViolated = truethrough a background calculation, not through a DML operation. No Apex trigger onCaseMilestonefires at the moment of violation. Relying on aCaseMilestoneafter-update trigger to catch violations will produce false results. - CaseMilestone records do not exist in developer or partial-copy sandboxes without entitlement processes — If the sandbox org does not have an active entitlement process configured and applied to cases,
CaseMilestonerecords are never created and trigger code runs against empty result sets. This causes all tests to pass vacuously. - MilestoneType.Name is case-sensitive in SOQL —
MilestoneType.Name = 'first response'will not match a type named'First Response'. Query failures here produce no records, not an exception, so the silent failure is easy to miss in testing.
Output Artifacts
| Artifact | Description |
|---|---|
| CaseMilestone completion trigger | after update trigger on Case that writes CompletionDate to open milestone records when a case status change occurs |
| Milestone violation scheduler | Scheduled Apex class that queries violated open milestones and applies custom business logic with idempotency |
| Test class | Apex test covering bulk completion, read-back of IsCompleted, and violation polling query |
Related Skills
admin/case-management-setup— Entitlement process and milestone configuration in Setup UI; required before any Apex milestone code has records to act onapex/opportunity-trigger-patterns— General Apex trigger bulk-safety patterns applicable here