Home / admin / activity-and-task-patterns
activity-and-task-patterns
Task and Event objects: polymorphic WhatId/WhoId, Activity object model, ActivityHistory vs OpenActivity, activity timeline customization, bulk task creation, Einstein Activity Capture boundaries. NOT for turning on EAC or calendar sync — use admin/einstein-activity-capture-setup. NOT for Email-to-Case — use admin/case-management-setup. Covers ActivitiesSettings metadata, enableActivities, shared Task/Event custom fields and FLS, TaskStatus value sets, Shared Activities and TaskRelation/EventRelation, recurring and child activities, and partial-success bulk DML on Task.
Open the skill folder on GitHub
Activity and Task Patterns
Activate when designing interactions with Salesforce Activities — Task and Event records that attach to other objects via polymorphic WhatId and WhoId. The Activity object model is unusual: Activity is a read-only abstract parent, Task and Event are concrete children, and ActivityHistory / OpenActivity are read-only related lists, not queryable in bulk.
Before Starting
- Understand the Activity object model.
Activitycannot be queried directly; queryTaskorEvent.ActivityHistoryandOpenActivityappear on related lists and subqueries only. - Know the polymorphic fields.
WhatIdcan reference any object enabled for activities;WhoIdreferences Contact or Lead. RequireTYPEOFor explicit type filters in SOQL. - Check Einstein Activity Capture. EAC-captured emails/events are stored outside standard Task/Event and are not reportable the same way.
Questions to Ask Before Configuring
Ask these before writing a line of Apex or opening Setup. Each one maps to a platform behaviour documented in references/gotchas.md; skipping them produces activity code that passes review and then misbehaves in production.
| Ask | Why it matters | What a good answer adds |
|---|---|---|
| "Is Shared Activities on in this org, and who turned it on?" | It cannot be enabled or disabled by a deploy — allowUsersToRelateMultipleContactsToTasksAndEvents has been read-only in every API version since v36.0 | Whether TaskWhoIds, TaskRelation and multi-contact relates exist at all, before code assumes them |
| "Who owns these activities, and should each one email its owner?" | Apex DML that creates or modifies a Task triggers the assignment notification by default | A decision to set EmailHeader.triggerUserEmail, and a backfill that doesn't mail thousands of people |
| "What is the read path — which field will the WHERE clause filter on?" | Subject, TaskStatus and TaskPriority are on the platform's can't-index list; WhatId / OwnerId / date ranges are not | Whether Task/Event survives the volume, or the design needs a custom Interaction__c |
| "Does this field make sense on both a Task and an Event?" | There is no Task-only custom field; Activity fields land on both children and share field-level security | The validation rule that scopes it, and the FLS entries for both objects in the same deploy |
| "Are any of these activities recurring, or occurrences of a series?" | Recurring Tasks can't be flipped after creation and reject ActivityDate/Status updates; child Events accept updates to IsReminderSet and ReminderDateTime only | A partition step before the bulk DML instead of a half-failed job |
| "Is Einstein Activity Capture on for anyone here?" | EAC-captured mail and events live outside the Task/Event tables, so reports and triggers silently miss them | An explicit source-of-truth statement in the data dictionary |
| "Can one bad row be allowed to fail on its own?" | The bare insert verb rolls the whole transaction back; Database.insert(..., dmlOptions, ...) does not | A partial-success write plus the Database.SaveResult loop that reports what didn't land |
What a proper configuration adds over just creating the records: the activity model matches the org's actual read path and volume, the shared Task/Event surface is deployed as one unit instead of half of it silently disabling FLS, and bulk writes fail one row at a time and quietly instead of rolling back a 200-record save while emailing everyone.
Core Concepts
Task vs Event
Task: to-do item with due date. Event: calendar appointment with start/end time. Both share the polymorphic WhatId / WhoId pattern and an IsTask discriminator on Activity queries.
Polymorphic SOQL
SELECT Id, Subject, What.Type, TYPEOF What
WHEN Account THEN Name, Industry
WHEN Opportunity THEN Amount, StageName
END FROM Task
Without TYPEOF, only ID and Type are accessible via What.Type.
ActivityHistory vs OpenActivity
ActivityHistory: closed activities (completed tasks, past events). OpenActivity: open activities (due, upcoming). Only queryable as subqueries from activity-enabled parents. Cannot create/update these objects directly.
Einstein Activity Capture
EAC syncs emails and calendar events from Exchange/Gmail into Salesforce. Data lives in a separate EAC store — visible on timeline, but not in Task/Event tables. Reporting requires Activity Metrics or EAC-specific features.
Common Patterns
Pattern: Bulk task creation from trigger
List<Task> tasks = new List<Task>();
for (Opportunity o : Trigger.new) {
tasks.add(new Task(WhatId = o.Id, Subject = 'Review',
ActivityDate = Date.today().addDays(7), OwnerId = o.OwnerId));
}
Database.DMLOptions dml = new Database.DMLOptions();
dml.optAllOrNone = false; // one bad WhoId must not roll back the save
dml.EmailHeader.triggerUserEmail = false; // suppress "a task was assigned to you"
List<Database.SaveResult> results =
Database.insert(tasks, dml, AccessLevel.SYSTEM_MODE);
Collect and write once — never loop-DML. The two DMLOptions lines are not optional polish: creating or modifying a Task is one of the events that fires triggerUserEmail, and the bare insert verb has nowhere to put either setting. Full version with result handling in references/metadata-examples.md section 5.
Pattern: Custom Object for high-volume activity-like data
Past roughly 50k activities/day per object — UNVERIFIED (2026-09-05): a field heuristic, no activity-volume ceiling is published in the App Limits cheat sheet or the LDV guide — consider a custom Interaction__c with a real lookup instead of polymorphic Task. The grounded reason to switch is the read path, not the number: the LDV guide lists Activity Subject, TaskStatus and TaskPriority among the standard fields the platform can't index, so a query that must filter on any of them will not get selective on a large Task table.
Pattern: Activity rollup via Lightning component or Apex
Activity count / last-activity-date rollups: use formula-friendly patterns (e.g., Salesforce's Activity Metrics, EAC Insights, or DLRS).
Decision Guidance
| Need | Approach |
|---|---|
| Standard to-do with reminders | Task |
| Calendar appointment with participants | Event |
| Query must filter on activity subject or status at volume | Custom Interaction__c with an indexed field |
| Email tracking without EAC | EmailMessage + Task linking |
| Reporting on email activity | Activity Metrics or EAC Insights |
Recommended Workflow
- Establish what the org already does. Retrieve
Settings:Activitiesand check whether Shared Activities is on — it is a read-only field no deploy can change, andTaskWhoIds/TaskRelation/EventRelation.IsParentonly exist when it is. Note whether Einstein Activity Capture is enabled for anyone (admin/einstein-activity-capture-setup). - Decide Task/Event vs custom
Interaction__cusing the Decision Guidance table above and the tradeoff table inreferences/well-architected.md. Drive the decision from the read path and the sharing requirement, not from record count alone. - Write the metadata from
references/metadata-examples.md—Activities.settings-meta.xml,enableActivitieson every object that must be a validWhatId, the custom field underActivity(never under Task), theTaskStatusvalue set ifIsClosedsemantics are changing, and FLS entries for Task and Event in the same permission set. - Write the read path with
TYPEOFor aWhat.Typefilter, and the write path withDatabase.insert(records, dmlOptions, accessLevel)carryingoptAllOrNone = falseandEmailHeader.triggerUserEmail = false. Partition out recurring Tasks and child Events (IsChild = true) before any bulk update. - Lint the working tree with
python3 skills/admin/activity-and-task-patterns/scripts/check_activity_and_task_patterns.py --manifest-dir force-app/main/default. It flagsFROM Activity, DML onActivityHistory/OpenActivity, loop-DML on Task/Event,What.<field>withoutTYPEOF, bareinsert/updateof Task collections,ActivitiesSettingsfiles that try to set the read-only Shared Activities flag, and permission-set FLS that names one child object but not the other. - Read
references/gotchas.mdend to end before deploying — the ten entries are the failures this skill exists to prevent, and several of them (the 500-row subquery cap, shared FLS, the EmailMessage task swap) are invisible in a sandbox with test-sized data. - Verify in the target org using the three checks at the end of
references/metadata-examples.md: a Task insert against the newly activity-enabled object, aStatus/IsClosed/CompletedDateTimequery, and aFieldPermissionsquery that must return a row for bothTask.<field>andEvent.<field>.
Review Checklist
- [ ] Activity volume estimated and matched to object choice
- [ ] Polymorphic SOQL uses TYPEOF or explicit type filters
- [ ] Bulk DML used for task/event creation
- [ ] ActivityHistory / OpenActivity queries only in subquery form
- [ ] EAC data strategy documented
- [ ] Custom fields on Activity limited (they propagate to both Task and Event)
- [ ] Sharing model for activities understood (inherits from WhatId parent)
- [ ] FLS deployed for
Task.<field>andEvent.<field>— a missing entry disables, it does not preserve - [ ] Bulk writes use
Database.insert(..., dmlOptions, accessLevel)withoptAllOrNone = false - [ ]
EmailHeader.triggerUserEmailset deliberately on any job that creates or modifies Tasks - [ ] Recurring Tasks and
IsChild = trueEvents partitioned out before bulk update - [ ] Shared Activities state confirmed in the org, not assumed from a deploy
- [ ]
WHEREclauses filter on an indexable field, notSubject/Status/Priorityalone - [ ]
scripts/check_activity_and_task_patterns.py --manifest-dir …run clean
Salesforce-Specific Gotchas
- Custom fields on
Activitypropagate to both Task and Event. You cannot add a field only to Task — architect with this in mind. - Activities inherit sharing from the
WhatIdparent. No independent sharing rules. OpenActivityandActivityHistorycannot be modified. They are projections of Task/Event — the Object Reference listsdescribeSObjects()as their only supported call (object_reference.txt L23515–L23516, L191459–L191460).- Shared Activities is not deployable. Its
ActivitiesSettingsfield has been read-only in every API version since v36.0; a deploy that sets it succeeds and changes nothing. Task.IsClosedis never written directly. It is derived from theStatusvalue'sclosedflag in theTaskStatusstandard value set, andEventhas noStatusat all.
The ten platform behaviours behind these — with what happens, when, and how to avoid — are in references/gotchas.md.
Output Artifacts
| Artifact | Description |
|---|---|
| Activity model decision | Task vs Event vs custom object |
| SOQL pattern library | Polymorphic queries with TYPEOF |
| Bulk creation template | Apex trigger / batch pattern |
Reference Files
| File | Read it when |
|---|---|
references/metadata-examples.md | Writing the deployable XML — ActivitiesSettings, enableActivities, an Activity custom field and its FLS, TaskStatus, plus the bulk-safe Apex and the post-deploy verification queries |
references/gotchas.md | Before deploying, and whenever activity behaviour differs between sandbox and production |
references/examples.md | Two worked scenarios end to end: a polymorphic timeline query for an LWC, and bulk task generation from a record-triggered context |
references/llm-anti-patterns.md | Reviewing AI-generated Apex or metadata that touches Task, Event or Activity |
references/well-architected.md | Choosing between Task/Event and a custom Interaction__c, and citing the sources behind any claim in this skill |
Related Skills
apex/apex-polymorphic-soql—TYPEOF,What.Typeandinstanceofin depthapex/apex-dml-patterns—Database.DMLOptionsbeyond the email header, and partial-success result handlingapex/trigger-framework— where activity-creating logic belongs in a handleradmin/einstein-activity-capture-setup— EAC sync scope, and where EAC data actually livesadmin/case-management-setup— case-related activity handlingadmin/email-to-case-configuration— the intake side of theEmailMessage.ActivityIdtaskadmin/picklist-and-value-sets— standard vs global value sets, for theTaskStatuschangeadmin/queues-and-public-groups—Task.OwnerIdaccepts Groups of type Queue onlyarchitect/large-data-volume-architecture— when activity volume forces a custom objectdata/data-archival-strategies— what to do withIsArchivedactivities