xrmverse
Documentation menu

Activities and notes

← xrmverse documentation

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 of regardingobjectid on activitypointer and on every activity table.
  • HasNotes: the table's records can have notes. Its table becomes a target of annotation.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 0 on a table that is on keeps it on and logs a warning.
  • HasActivities is refused (400) on activity tables, activitypointer, activityparty, annotation and intersect tables. HasNotes is refused on activityparty, annotation and intersect tables. Organization-owned tables may have both.
  • A false CreateEntityRequest.HasActivities/HasNotes parameter 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 name subject, without a publisher prefix);
  • HasActivities is false and HasNotes true (both are set when absent; an explicit HasNotes false is refused, except in a solution import, where a 0 is turned into true with a warning);
  • the owner is a user (OwnershipType UserOwned or absent): organization- and team-owned activities are refused;
  • ActivityTypeMask is 0 or 1 (default 1) and cannot change afterwards; IsActivity cannot change either;
  • IsMailMergeEnabled false and IsAvailableOffline true 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_parties replace theirs, and an empty array changes nothing.
  • from and organizer take 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.
  • partyid must name an existing account, contact or systemuser (404 ObjectDoesNotExist when 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 addressused is filled from the party: emailaddress1 of an account or contact, internalemailaddress of 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 ispartydeleted to 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 changes statecode and statuscode (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 actualend to now when it is empty. An activity created directly in state 1 or 2 is accepted and gets actualend the same way.
  • SetState arrives as an Update, as for every table (see Plug-ins).
  • activitypointer.statuscode keeps 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 }, response Subject.
  • Web API: POST emails(<id>)/Microsoft.Dynamics.CRM.SendEmail with { "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, cc or bcc, else 400;
  • the sender must be the caller: an empty from is filled with the caller's own systemuser, and a from naming another user, a contact, an account or an address is a 400 (sending on behalf of someone else is not modelled yet);
  • IssueSend false: state Completed, status 3 Sent, senton now;
  • IssueSend true: status 6 Pending Send and an empty senton (a Failed email's senton from 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;
  • directioncode becomes true, and trackingtoken is stored when one is given (an empty one counts as not given);
  • IssueSend is required (400 when missing or not a Boolean), and so is EmailId; 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 fileattachment row 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 with 0x80044a02 (AttachmentInvalidFileSize), before it is decoded. Text that is not base64 is a 400.
  • What the server sets. With a body: filesize (bytes), isdocument true, and mimetype from the file name's extension when you give none (application/octet-stream for an unknown extension). Without a body, or after documentbody is set to null or empty: filesize 0 and isdocument false; filename and mimetype stay. A client's isdocument is 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. documentbody cannot be filtered, sorted, aliased, joined on or aggregated (400).
  • Read cap. One query reads at most Files:MaxNoteBodyBytesPerRead bytes of bodies (64 MB by default, see Configuration). A page whose bodies add up to more is refused with a 400 that names documentbody, before any body is read: leave documentbody out of the columns, or read fewer notes per page.
  • InitializeAnnotationBlocksUpload and InitializeAnnotationBlocksDownload (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 activitypointer use one privilege set: prvCreateActivity, prvReadActivity, prvWriteActivity, prvDeleteActivity, prvAppendActivity, prvAppendToActivity, prvAssignActivity, prvShareActivity. Notes use prvCreateNote … prvShareNote. There are no per-table privileges such as prvReadEmail (per-table privileges are issue #66). activityparty has 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.sql makes 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 (400 FieldNotSecurable): the pointer copy would show a secured value. Neither can annotation.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. SendEmail is 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 for activitypointer or activityparty (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, --notes and --activities, xrmd table set <table> --notes|--activities, and xrmd table describe shows the three settings (see CLI). data export and dev seed skip party list columns with a notice, and data import of 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, description included, are kept in the activity's table and in activitypointer. A subject longer than 400 characters (possible on email) is cut to 400 in the pointer, and a description longer than 2000 characters (possible on email, whose description is 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 partners and any other party column not listed above; knowledgearticle and queue records as parties; an address without a record outside email; email attachments (activitymimeattachment); recurring appointments; queues; any standard column outside the lists above. activityparty has a statecode and statuscode like every xrmverse table, which Dataverse's does not.
  • No email is sent. SendEmail with IssueSend true 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 named SendEmail; whether Dataverse names it Send for plug-ins is on the checklist. The Update steps of email do 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 no associatednavigationproperty annotation on the Web API (team is not a partyid target). 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. fileattachment lists its objectid navigation in $metadata for its first target only, so objectid_annotation is declared only where annotation is that target (a new organization).
  • Notes. A body is at most maxuploadfilesize bytes (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 most Files:MaxNoteBodyBytesPerRead bytes of bodies. GET annotations(<id>)/documentbody is 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.documentbody cannot be secured.
  • Audit and change tracking. Party list changes and note bodies are not audited; activitypointer and activityparty cannot 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 (404 0x80040217), 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 of maxuploadfilesize and several behaviours above are not verified on a real Dataverse: see the checklist.