Activities and notes
An activity is a record of something done or to do with someone: an email, a task, a phone call, an appointment, or a
row of a custom activity table. Every activity can be regarding one record (an account, a contact, a custom table), and
it lists its parties (sender, recipients, attendees) in party list columns. Every activity is also readable through one
shared table, activitypointer, so one query finds all the activities of a record whatever their type. A note
(annotation) is a text with an optional attached file, linked to one record. xrmverse follows the Dataverse model of tables,
columns, messages and error codes. Whether each detail matches a real Dataverse is not verified (see the
checklist). The Microsoft documentation describes the model in
Activity tables,
Activities data model and storage,
ActivityParty table,
Custom activities and the table
references of activitypointer,
email and
annotation. This page says
what xrmverse does and where it stops.
The tables
Every organization is seeded with seven tables (the object type codes are from memory, checklist 1a):
| Table | Entity set | Type code | Owner | What it holds |
|---|---|---|---|---|
activitypointer |
activitypointers |
4200 | user | one read-only row per activity, with the common columns |
activityparty |
activityparties |
135 | none | one row per party of an activity |
email |
emails |
4202 | user | emails |
task |
tasks |
4212 | user | tasks |
phonecall |
phonecalls |
4210 | user | phone calls |
appointment |
appointments |
4201 | user | appointments |
annotation |
annotations |
5 | user | notes and their attached files |
Common columns. Every activity table and activitypointer have activityid (the primary key, the same id in the
activity's table and in the pointer), subject (the primary name), description, activitytypecode (the activity's table,
read-only), regardingobjectid with regardingobjecttypecode, scheduledstart, scheduledend, actualstart,
actualend, scheduleddurationminutes, actualdurationminutes, prioritycode (0 Low, 1 Normal, 2 High; default 1),
isbilled, isworkflowcreated, the read-only isregularactivity, transactioncurrencyid and exchangerate (as for any
table with money, see Currencies; the relationships are TransactionCurrency_ActivityPointer,
TransactionCurrency_Email, …), statecode, statuscode and the owner and system columns.
Own columns. email: directioncode (default true), trackingtoken, senton (read-only), messageid and the party
lists from, to, cc, bcc, related. task: percentcomplete (0 to 100), category, subcategory, no party list.
phonecall: phonenumber, directioncode, leftvoicemail, party lists from, to, related. appointment:
location, isalldayevent, party lists requiredattendees, optionalattendees, organizer. Any other standard column
of these tables (torecipients, sender, sortdate, …) is not seeded: reading it is the usual "attribute not found" 400.
EntityName columns. activitytypecode, regardingobjecttypecode, objecttypecode (on annotation) and
partyobjecttypecode are EntityName columns: their value is a table's logical name ("email", "account"), in the SDK
and on the Web API. The three …typecode columns that go with a lookup are read from the lookup itself: a value you send is
accepted only when it names the lookup's table, otherwise 400. Filters on them take a logical name (eq 'email') or the
type code. Only the seed creates EntityName columns: CreateAttribute with that type is a 400.
activitypointer is a kept copy. Each activity table holds the full row of its activities. The server writes the
pointer row in the same transaction as every Create, Update, Assign, SetState and Delete of an activity, and queries on
activitypointer are plain queries on one table: FetchXML, QueryExpression, the Web API, paging and the security filter
work on it as on any table. The common columns are therefore stored twice. A subject longer than the pointer's 400
characters (an email's can be 800) is cut to 400 in the pointer, and a description longer than the pointer's 2000 is cut
to 2000; the email keeps the full text. An activityid is unique across all activity tables, as in Dataverse, where every
activity is a row of one base table: a Create with the id of an existing activity of another table (a task with an
email's id, say) is refused with DuplicateRecord (0x80040237, 412, "A record that has the attribute values
activityid = … already exists"), the same fault as a duplicate id in one table, and changes nothing. Create,
Update, Delete, Assign, SetState, GrantAccess and ModifyAccess of activitypointer are refused (400) for every caller,
System Administrators included; reads, Associate and Disassociate are its only messages.
Two settings columns. systemuser.internalemailaddress (the address a user party gets) and
organization.maxuploadfilesize (the largest note body in bytes, default 5242880, 0 to 131072000). An organization created
before these tables gets them, their relationships and the defaults at startup; a second start changes nothing.
Regarding and notes: HasActivities and HasNotes
Two table settings decide which tables activities and notes can point to:
HasActivities: the table's records can be the regarding record of activities. Its table becomes a target ofregardingobjectidonactivitypointerand on every activity table.HasNotes: the table's records can have notes. Its table becomes a target ofannotation.objectid.
account and contact have both. The four standard activity tables have HasNotes. Every other seeded table has
neither. You set them with CreateEntity (the EntityMetadata properties, or CreateEntityRequest.HasActivities/
HasNotes), UpdateEntity, PUT EntityDefinitions(...), a solution import (HasRelatedActivities/HasRelatedNotes),
xrmd table create --notes --activities, xrmd table set, a schema file (hasNotes, hasActivities) and the web admin.
- They are one-way. False to true works; true to false is a 400. A solution import of
0on a table that is on keeps it on and logs a warning. HasActivitiesis refused (400) on activity tables,activitypointer,activityparty,annotationand intersect tables.HasNotesis refused onactivityparty,annotationand intersect tables. Organization-owned tables may have both.- A false
CreateEntityRequest.HasActivities/HasNotesparameter is ignored: the SDK always sends both, so only true asks for something.
Each pair of target table and activity table gets a saved relationship with the Dataverse name: Account_Emails,
Account_Tasks, Account_Phonecalls, Account_Appointments and Account_ActivityPointers for account (and Contact_…
for contact), {table}_{collection} for any other table, where {collection} is the activity's collection schema name
(Emails, …; for a custom activity its CollectionSchemaName, else its SchemaName plus s). The navigation property is
regardingobjectid_{table} on activitypointer and regardingobjectid_{table}_{activity} on an activity table. For notes:
Account_Annotation, Contact_Annotation, Email_Annotation and so on for the seeded tables, {table}_Annotations for a
custom table, navigation objectid_{table}. These names, apart from the account ones, are from memory (checklist 2a). You
cannot create, update or delete these relationships, nor change the targets of the lookups yourself (400): the server
keeps them in step with the settings.
Deleting. Deleting a record deletes its activities and notes (Delete Cascade), and with them the activities' pointer and party rows and the notes' files. A regarding, note or other multi-table lookup names the deleted record by id and table, so the Delete of contact X neither deletes the activities and notes of an account that has the same id nor clears a Customer lookup that names that account (RemoveLink); Restrict counts only that table's records too. Deleting a table is refused (400) while any activity is regarding one of its records or any note is attached to one; delete or move them first. The same check runs in a managed solution uninstall, dry run included, so the uninstall is refused before anything changes. The uninstall checks the caller's privileges first: a caller who may not uninstall the solution gets 403 and never this refusal, which names the counts of activities and notes.
Custom activities
A custom activity is a table created with IsActivity true. It gets the common columns, the party lists from, to,
cc, bcc, requiredattendees, optionalattendees, organizer, customers and resources (the names are from memory,
checklist 1b), the states Open (status 1), Completed (2) and Canceled (3), and its relationships (regarding, notes,
parties, currency). Rules, each a 400 when broken:
- the primary column has the SchemaName
Subject(logical namesubject, without a publisher prefix); HasActivitiesis false andHasNotestrue (both are set when absent; an explicitHasNotesfalse is refused, except in a solution import, where a0is turned into true with a warning);- the owner is a user (
OwnershipTypeUserOwned or absent): organization- and team-owned activities are refused; ActivityTypeMaskis 0 or 1 (default 1) and cannot change afterwards;IsActivitycannot change either;IsMailMergeEnabledfalse andIsAvailableOfflinetrue are the only accepted values.
Ways to create one:
// SDK
service.Execute(new CreateEntityRequest
{
Entity = new EntityMetadata
{
SchemaName = "xv_Sms", LogicalName = "xv_sms", IsActivity = true, OwnershipType = OwnershipTypes.UserOwned,
DisplayName = new Label("Text message", 1033), DisplayCollectionName = new Label("Text messages", 1033),
},
PrimaryAttribute = new StringAttributeMetadata
{
SchemaName = "Subject", LogicalName = "subject", MaxLength = 200, DisplayName = new Label("Subject", 1033),
},
});
# CLI (primary column subject, user owned, notes on)
xrmd table create xv_sms --label "Text message" --plural-label "Text messages" --activity
# schema file, applied with xrmd apply
tables:
- name: xv_sms
label: Text message
isActivity: true
primaryName: { name: subject }
On the Web API, POST EntityDefinitions with "IsActivity": true and a primary attribute with "SchemaName": "Subject";
in the web admin, the Activity table switch of the create-table dialog. A solution whose customizations.xml has
<IsActivity>1</IsActivity> creates the table the same way (see Solutions).
Deleting a custom activity table deletes its pointer and party rows with it, and removes it from the targets of the lookups. The shared Activity privileges stay (see Security).
Party lists
A party list column holds the participants of one kind (to, cc, …). It has no column of its own in SQL: each
participant is a row of activityparty with the activity's id, the participant (partyid, an account, contact or
systemuser), the address used (addressused, up to 320 characters) and the participation type (participationtypemask):
| Type | Meaning | Type | Meaning |
|---|---|---|---|
| 1 | Sender (from) |
8 | Regarding (kept by the server) |
| 2 | To recipient (to) |
9 | Owner (kept by the server) |
| 3 | CC recipient (cc) |
10 | Resource (resources) |
| 4 | BCC recipient (bcc) |
11 | Customer (customers) |
| 5 | Required attendee (requiredattendees) |
12 | Chat participant (refused) |
| 6 | Optional attendee (optionalattendees) |
13 | Related (related) |
| 7 | Organizer (organizer) |
SDK. A party list is an EntityCollection of activityparty entities, each with partyid (an EntityReference) or,
on email only, addressused alone (an address that names no record):
var email = new Entity("email")
{
["subject"] = "Quarterly review",
["regardingobjectid"] = new EntityReference("account", accountId),
["to"] = new EntityCollection(new[]
{
new Entity("activityparty") { ["partyid"] = new EntityReference("contact", adaId) },
new Entity("activityparty") { ["addressused"] = "someone@example.com" },
}),
};
var id = service.Create(email);
A read (Retrieve, RetrieveMultiple, every page) returns each party list asked for as an EntityCollection whose
entities carry activitypartyid, activityid, partyid (with the record's name), participationtypemask, addressused
and ispartydeleted. A party list with no rows is left out of the record, as an empty column is.
Web API. Party lists are not properties of the entity type. Use the collection navigation
{activity}_activity_parties (email_activity_parties, xv_sms_activity_parties) in the body and in $expand:
POST /api/data/v9.2/emails
{
"subject": "Quarterly review",
"regardingobjectid_account_email@odata.bind": "/accounts(<accountid>)",
"email_activity_parties": [
{ "participationtypemask": 2, "partyid_contact@odata.bind": "/contacts(<contactid>)" },
{ "participationtypemask": 2, "addressused": "someone@example.com" },
{ "participationtypemask": 1, "partyid_systemuser@odata.bind": "/systemusers(<userid>)" }
]
}
GET /api/data/v9.2/emails(<id>)?$select=subject&$expand=email_activity_parties($select=participationtypemask,addressused,_partyid_value)
Each item takes participationtypemask (required), one partyid_{table}@odata.bind and addressused; any other property
is a 400. Inside the $expand, only $select is accepted. $select of a party list column ($select=to) is a 400. Any
other collection-valued navigation in a POST or PATCH body (a generic deep insert) is a 400.
Rules on writes.
- On Create, each party list sent is written as rows of its type. On Update, a party list sent replaces the rows of its
type only; the other types stay. In the SDK, an empty collection or null clears the type. On a Web API PATCH, the types
present in
{activity}_activity_partiesreplace theirs, and an empty array changes nothing. fromandorganizertake at most one party. Types 8 and 9 are written by the server and refused when sent. A type the table does not have, and type 12, are refused. The same party twice in one list gives two rows.partyidmust name an existing account, contact or systemuser (404ObjectDoesNotExistwhen missing; another table is a 400). The caller must be able to read the party record (403 otherwise); Append or AppendTo on it is not needed.- An empty
addressusedis filled from the party:emailaddress1of an account or contact,internalemailaddressof a user. - The server keeps one Owner row (type 9, the activity's owner, user or team) and, when there is a regarding record, one
Regarding row (type 8) up to date on Create, Update and Assign. These two rows have no
addressused. - Deleting an activity deletes its party rows. Deleting a party record (an account or contact) keeps its rows and sets their
ispartydeletedto true.
activityparty itself is read-only: Create, Update and Delete of its rows are 400, through the SDK and the Web API.
Query it with RetrieveMultiple or link to it from an activity (activityparty.activityid) to filter by participant:
a party list column cannot be used in a condition, an order, a link or an aggregate (400), nor as a column of a link-entity.
A party row is readable when its activity is; no privilege on activityparty is needed.
State and status
| Table | Open (0) | Completed (1) | Canceled (2) | Scheduled (3) |
|---|---|---|---|---|
email |
1 Draft, 8 Failed | 2 Completed, 3 Sent, 4 Received, 6 Pending Send, 7 Sending | 5 Canceled | |
task |
2 Not Started, 3 In Progress, 4 Waiting on someone else, 7 Deferred | 5 Completed | 6 Canceled | |
phonecall |
1 Open | 2 Made, 4 Received | 3 Canceled | |
appointment |
1 Free, 2 Tentative | 3 Completed | 4 Canceled | 5 Busy, 6 Out of Office |
| custom activity | 1 Open | 2 Completed | 3 Canceled |
The default status of each state is the first one listed. A new activity starts in state 0 with that state's default status (a task is Not Started). A status must belong to its state (400 otherwise).
- Closed activities are read-only. An Update of an activity in state 1 or 2 is a 400 (
InvalidArgument) unless it only changesstatecodeandstatuscode(a SetState). Reopen it first (state 0). Party lists and file uploads count as changes. Assign is the exception: a closed activity can be assigned. - Moving an activity to state 1 or 2 sets
actualendto now when it is empty. An activity created directly in state 1 or 2 is accepted and getsactualendthe same way. - SetState arrives as an Update, as for every table (see Plug-ins).
activitypointer.statuscodekeeps the activity's own value; its formatted value comes from the option set of the activity's table (task status 3 shows "In Progress").
SendEmail
SendEmail marks an email as sent. xrmverse has no email provider before plan 4.1: no email leaves the server.
- SDK:
SendEmailRequest { EmailId, IssueSend, TrackingToken }, responseSubject. - Web API:
POST emails(<id>)/Microsoft.Dynamics.CRM.SendEmailwith{ "IssueSend": false, "TrackingToken": "…" }, answer{ "Subject": "…" }. The action is declared in$metadata.
What it does, in one transaction through the normal Update path (the pointer is updated and the change is audited as an Update):
- the caller needs Write on the email;
- the email must be open (Draft or Failed), else 400; it needs at least one party in
to,ccorbcc, else 400; - the sender must be the caller: an empty
fromis filled with the caller's ownsystemuser, and afromnaming another user, a contact, an account or an address is a 400 (sending on behalf of someone else is not modelled yet); IssueSendfalse: state Completed, status 3 Sent,sentonnow;IssueSendtrue: status 6 Pending Send and an emptysenton(a Failed email'ssentonfrom an earlier attempt is cleared); the server logs one information line, and the email stays Pending Send, because nothing sends it until plan 4.1;directioncodebecomes true, andtrackingtokenis stored when one is given (an empty one counts as not given);IssueSendis required (400 when missing or not a Boolean), and so isEmailId; an unknown email is 404.
Plug-in steps register on SendEmail with the filter email and get pre and post images of the email (see
Plug-ins). The Update steps of email do not fire for a send.
Notes and attachments
A note is a row of annotation: subject, notetext (up to 100000 characters), objectid with objecttypecode (the record
it belongs to, any table with HasNotes), and an optional file: documentbody (base64), filename, mimetype, and the
read-only filesize and isdocument. On the Web API the record is bound with objectid_{table}@odata.bind.
service.Create(new Entity("annotation")
{
["subject"] = "Contract",
["objectid"] = new EntityReference("account", accountId),
["filename"] = "contract.pdf",
["documentbody"] = Convert.ToBase64String(File.ReadAllBytes("contract.pdf")),
});
- The body is never stored in SQL. It is kept in the file store as a
fileattachmentrow plus a blob, so backups, the cleanup sweep and the deletion queue cover it. A new body replaces the old one (the old blob is deleted after the commit, the new one if the transaction rolls back). Deleting a note, or the record it belongs to, deletes its body. - Size. A body larger than
organization.maxuploadfilesize(5 MB by default) is refused with0x80044a02(AttachmentInvalidFileSize), before it is decoded. Text that is not base64 is a 400. - What the server sets. With a body:
filesize(bytes),isdocumenttrue, andmimetypefrom the file name's extension when you give none (application/octet-streamfor an unknown extension). Without a body, or afterdocumentbodyis set to null or empty:filesize0 andisdocumentfalse;filenameandmimetypestay. A client'sisdocumentis ignored. - Reads.
Retrieve,RetrieveMultiple, all columns, link-entities, plug-in images and the Web API return the base64 text. Only the notes a query returns get a body, so a body is never readable where its note is not.documentbodycannot be filtered, sorted, aliased, joined on or aggregated (400). - Read cap. One query reads at most
Files:MaxNoteBodyBytesPerReadbytes of bodies (64 MB by default, see Configuration). A page whose bodies add up to more is refused with a 400 that namesdocumentbody, before any body is read: leavedocumentbodyout of the columns, or read fewer notes per page. InitializeAnnotationBlocksUploadandInitializeAnnotationBlocksDownload(chunked transfer) are refused, so a body is written and read in one piece. The file messages (InitializeFileBlocksDownload,DeleteFile) refuse a note body with a 400 that says to use an Update.
Security
- Shared privileges. Every activity table, custom ones included, and
activitypointeruse one privilege set:prvCreateActivity,prvReadActivity,prvWriteActivity,prvDeleteActivity,prvAppendActivity,prvAppendToActivity,prvAssignActivity,prvShareActivity. Notes useprvCreateNote…prvShareNote. There are no per-table privileges such asprvReadEmail(per-table privileges are issue #66).activitypartyhas no privileges. - Basic User holds the eight Activity and the eight Note privileges at Basic (User) depth, in new organizations and, through a one-time top-up at startup, in existing ones.
- Reading through the pointer. A pointer row carries the owner columns of its activity, so owner depth, business units
and hierarchy security apply with
prvReadActivity. A share or an access team on an activity opens its pointer row too (matched by id and by the activity's type). A table left out of hierarchy security is left out through the pointer as well. - Shares are per table. Sharing, revoking or deleting an activity reads and changes only the shares of that activity,
never those of a record of another table that has the same id (see Security; migration
0011_share_object_type.sqlmakes the share index per table on an existing database). - Party rows are readable exactly when their activity is. Writing a party needs Read on the party record (see above).
- Field security. The common activity columns of every activity table, and every column of
activitypointer, cannot be secured (400FieldNotSecurable): the pointer copy would show a secured value. Neither canannotation.documentbody. Your own columns on an activity table can be secured as usual. - Auditing. Activities and notes are audited like any table. Party list changes and note bodies are not written to the
audit; the pointer and party writes are not audited on their own.
SendEmailis audited as an Update of the email. - Change tracking can be switched on for the activity tables and
annotation(a new body moves the note's version), but not foractivitypointeroractivityparty(400).
What is refused
These messages are answered with a 400 that says why (through the SDK, SOAP and the Web API, unbound or bound), and no
custom API or custom action can take their names: Book, Reschedule, AddRecurrence, DeliverPromoteEmail,
DeliverIncomingEmail, CheckIncomingEmail, CheckPromoteEmail, BackgroundSendEmail, SendEmailFromTemplate,
SendTemplate, InstantiateTemplate, GetTrackingTokenEmail, Rollup, Restore, AppendRelatedParty,
InitializeAnnotationBlocksUpload, InitializeAnnotationBlocksDownload, and the queue messages AddToQueue,
PickFromQueue, ReleaseToQueue, RemoveFromQueue, RouteTo, RetrieveUserQueues (these names are from memory and
arrive with plan 4.1). An sdkmessage row with one of these names does not make it run. These names are also reserved as
entity set names, so a new table, or a table in a solution import, whose entity set is named Rollup, Book and so on is a
400.
The tables activitymimeattachment, letter, fax, recurringappointmentmaster, socialactivity, queue, queueitem,
template, serviceappointment and campaignactivity are not seeded: they give the usual "entity not found" 400.
Surfaces
- CLI.
xrmd table create --activity,--notesand--activities,xrmd table set <table> --notes|--activities, andxrmd table describeshows the three settings (see CLI).data exportanddev seedskip party list columns with a notice, anddata importof a file that names one exits 2. - Schema files.
isActivity,hasNotes,hasActivities(see CLI). - Web admin. The Activity table switch when creating a table, the Notes and Activities switches on a table's page, party lists shown read-only on a record form (see Web admin).
- MCP. The record tools work on activities and notes; party lists are left out of the records they return.
- Solutions. See Solutions.
Known limits
- Stored twice. The common columns,
descriptionincluded, are kept in the activity's table and inactivitypointer. Asubjectlonger than 400 characters (possible onemail) is cut to 400 in the pointer, and adescriptionlonger than 2000 characters (possible onemail, whosedescriptionis far longer) is cut to 2000, the pointer's declared length; what Dataverse's pointer holds in both cases is on the checklist. - Not modelled. Team-owned and organization-owned custom activities; the party list
partnersand any other party column not listed above;knowledgearticleandqueuerecords as parties; an address without a record outsideemail; email attachments (activitymimeattachment); recurring appointments; queues; any standard column outside the lists above.activitypartyhas astatecodeandstatuscodelike every xrmverse table, which Dataverse's does not. - No email is sent.
SendEmailwithIssueSendtrue leaves the email Pending Send until plan 4.1. The sender must be the caller (no send-as, no queue). The plug-in message is namedSendEmail; whether Dataverse names itSendfor plug-ins is on the checklist. TheUpdatesteps ofemaildo not fire on a send, and the send is audited as an Update. - Party reads and writes. Writing a party needs Read on the party record (stricter than Dataverse may be). A reader of an
activity sees each party's name and address even without Read on the party record. An empty party list is left out of a
read. A party list cannot be a link-entity column. The Owner and Regarding rows have no
addressused. The Owner row of a team-owned activity has noassociatednavigationpropertyannotation on the Web API (teamis not apartyidtarget). A Web API PATCH cannot clear a party type (an empty array changes nothing): use the SDK. $expand. The per-target navigations (regardingobjectid_{table},regardingobjectid_{table}_{activity},activityid_{activity},partyid_{table}) expand one level, with no nested$expand. Other multi-target lookups (Customer, Owner) still cannot be expanded.fileattachmentlists itsobjectidnavigation in$metadatafor its first target only, soobjectid_annotationis declared only whereannotationis that target (a new organization).- Notes. A body is at most
maxuploadfilesizebytes (up to 131,072,000, which is 125 MiB). That is a different limit from the 128 MB (134,217,728 bytes) a single request may carry: the body travels base64-encoded, about a third larger, so a body near the maximum does not fit in one request, and there is no chunked transfer. One query reads at mostFiles:MaxNoteBodyBytesPerReadbytes of bodies.GET annotations(<id>)/documentbodyis a 400 (the property routes serve file and image columns only). Plug-in images and all-columns reads read the body from the store each time. A body deleted by another request between the row read and the blob read gives 404. - Field security. The common activity columns and
annotation.documentbodycannot be secured. - Audit and change tracking. Party list changes and note bodies are not audited;
activitypointerandactivitypartycannot be tracked. - Deleting a table with notes, or with activities regarding its records, is refused (no cascade), also in a managed uninstall and its dry run.
- Cascades. Assign, Share, Unshare, Reparent and Merge of a regarding or notes relationship are stored, not enforced (the general gap of Solutions).
- Refused names. A bound refused action (
POST {set}({id})/Microsoft.Dynamics.CRM.Book) answers 400 for any entity set segment, an unknown one included, where 404 would be expected. - Error codes. Every refusal of this page is
InvalidArgument(0x80040203, 400) with xrmverse's own text, except a missing party or regarding record (4040x80040217), a body over the limit (0x80044a02) and the security errors. The checklist records what Dataverse answers. - From memory. The object type codes, the custom activity party lists and states, the relationship names other than
account's and the parties', the Basic User depths, the range ofmaxuploadfilesizeand several behaviours above are not verified on a real Dataverse: see the checklist.