Use Project schedule APIs to perform operations with Scheduling entities

Applies To: Project Operations for resource/non-stocked based scenarios, Lite deployment - deal to proforma invoicing.

Scheduling entities

Project schedule APIs provide the ability to perform create, update, and delete operations with Scheduling entities. These entities are managed through the Scheduling engine in Project for the web. Create, update, and delete operations with Scheduling entities were restricted in earlier Dynamics 365 Project Operations releases.

The following table provides a full list of the Project schedule entities.

Entity name Entity logical name
Project msdyn_project
Project Task msdyn_projecttask
Project Task Dependency msdyn_projecttaskdependency
Resource Assignment msdyn_resourceassignment
Project Bucket msdyn_projectbucket
Project Team Member msdyn_projectteam
Project Checklists msdyn_projectchecklist
Project Label msdyn_projectlabel
Project Task to Label msdyn_projecttasktolabel
Project Sprint msdyn_projectsprint

OperationSet

OperationSet is a unit-of-work pattern that can be used when several schedule impacting requests must be processed within a transaction.

Project schedule APIs

The following is a list of current Project schedule APIs.

API Description
msdyn_CreateProjectV1 This API is used to create a project. The project and default project bucket are created immediately. Project creation can also be done by adding a row to the project table using standard Dataverse APIs. This process doesn't create a default bucket for the project but may have better performance.
msdyn_CreateTeamMemberV1 This API is used to create a project team member. The team member record is created immediately. Team Member creation can also be done by adding a row to the Project Team Member table using standard Dataverse APIs.
msdyn_CreateOperationSetV1 This API is used to schedule several requests that must be performed within a transaction.
msdyn_PssCreateV1 This API is used to create an entity. The entity can be any of the Project scheduling entities that support the create operation.
msdyn_PssCreateV2 This API is used to create an entity. It works like msdyn_PssCreateV1, but multiple entities can be created in one action.
msdyn_PssUpdateV1 This API is used to update an entity. The entity can be any of the Project scheduling entities that support the update operation.
msdyn_PssUpdateV2 This API is used to updated entities. It works like msdyn_PssUpdateV1, but multiple entities can be updated in one action.
msdyn_PssDeleteV1 This API is used to delete an entity. The entity can be any of the Project scheduling entities that support the delete operation.
msdyn_PssDeleteV2 This API is used to delete entities. It works like msdyn_PssDeleteV1, but multiple entities can be deleted in one action.
msdyn_ExecuteOperationSetV1 This API is used to execute all the operations within the given operation set.
msdyn_PssUpdateResourceAssignmentV1 This API is used to update a Resource Assignment planned work contour.

Using Project schedule APIs with OperationSet

Because records are created immediately for both CreateProjectV1 and CreateTeamMemberV1, these APIs can't be used directly in the OperationSet. However, you can use them to create the required records, create an OperationSet, and then use the precreated records in the OperationSet.

Supported operations

Scheduling entity Create Update Delete Important considerations
Project task Yes Yes Yes The Progress, EffortCompleted, and EffortRemaining fields can be edited in Project for the Web, but they can't be edited in Project Operations.
Project task dependency Yes No Yes Project task dependency records aren't updated. Instead, an old record can be deleted, and a new record can be created.
Resource assignment Yes Yes* Yes Operations with the following fields aren't supported: BookableResourceID, Effort, EffortCompleted, EffortRemaining, and PlannedWork.
Project bucket Yes Yes Yes The default bucket is created by using the CreateProjectV1 API. Support for creating and deleting project buckets was added in Update Release 16.
Project team member Yes Yes Yes For the create operation, use the CreateTeamMemberV1 API.
Project Yes Yes No Operations with the following fields aren't supported: StateCode, BulkGenerationStatus, GlobalRevisionToken, CalendarID, Effort, EffortCompleted, EffortRemaining, Progress, Finish, TaskEarliestStart, and Duration.
Project Checklists Yes Yes Yes
Project Label No Yes No Label names can be changed. This feature is only available for Project for the Web. Labels are created the first you open a project.
Project Task to Label Yes No Yes This feature is only available for Project for the Web.
Project Sprint Yes Yes Yes The Start field must have a date earlier than the Finish field. Sprints for the same project can't overlap each other. This feature is only available for Project for the Web.
Project Goal Yes Yes Yes Operations with the following fields aren't supported: DescriptionPlainText, TaskDisplayOrder
Project Task to Goal Yes No Yes Operations with the following fields aren't supported: TaskDisplayOrder

* Resource assignment records aren't updated. Instead, the old record can be deleted, and a new record can be created. A separate API is provided to update Resource Assignment contours.

The ID property is optional. If the ID property is provided, the system tries to use it and throws an exception if it can't be used. If it isn't provided, the system generates it.

Limitations and known issues

The following list shows limitations and known issues:

  • Project Schedule APIs can only be used by Users with Microsoft Project License. They can't be used by:

    • Application users
    • System users
    • Integration users
    • Other users that don't have the required license
  • Each OperationSet can only have a maximum of 200 operations.

  • Each user can only have a maximum of 10 open OperationSets.

  • Each Update Resource Assignment Contour operation counts as a single operation.

  • Each list of updated contours can contain a maximum of 100 time slices.

  • OperationSet failure status and failure logs aren't currently available.

  • There's a maximum of 400 sprints per project.

  • Limits and boundaries on projects and tasks.

Error handling

  • To review errors generated from the Operation Sets, go to Settings > Schedule Integration > Operations Sets.
  • To review errors generated from the Project schedule Service, go to Settings > Schedule Integration > PSS Error Logs.

Editing Resource Assignment Contours

Unlike all other project scheduling APIs that update an entity, the resource assignment contour API is solely responsible for updates to a single field, msdyn_plannedwork, on a single entity, msydn_resourceassignment.

Given schedule mode is:

  • fixed units.
  • The project calendar is from 9:00 to 5:00 PM (Pacific Time) Monday, Tuesday, Thursday, and Friday. (There's no work on Wednesdays.)
  • The resource calendar is from 9:00 AM to 1:00 PM (Pacific Time) Monday through Friday.

This assignment is for one week, four hours a day because the resource calendar is from 9:00 AM to 1:00 PM (Pacific Time), or four hours a day.

  Task Start Date End Date Quantity 6/13/2022 6/14/2022 6/15/2022 6/16/2022 6/17/2022
9-1 worker T1 6/13/2022 6/17/2022 20 4 4 4 4 4

For example, if you want the worker to only work three hours each day this week and allow for one hour for other tasks.

UpdatedContours sample payload

[{

"minutes":900.0,

"start":"2022-06-13T00:00:00-07:00",

"end":"2022-06-18T00:00:00-07:00"

}]

This is the assignment after the Update Contour Schedule API is run.

  Task Start Date End Date Quantity 6/13/2022 6/14/2022 6/15/2022 6/16/2022 6/17/2022
9-1 worker T1 6/13/2022 6/17/2022 15 3 3 3 3 3

Sample scenario

In this scenario, you create a project, a team member, four tasks, and two resource assignments. Next, you update one task, update the project, update a resource assignment contour, delete one task, delete one resource assignment, and create a task dependency.

Entity project = CreateProject();
project.Id = CallCreateProjectAction(project);
var projectReference = project.ToEntityReference();

var teamMember = new Entity("msdyn_projectteam", Guid.NewGuid());
teamMember["msdyn_name"] = $"TM {DateTime.Now.ToShortTimeString()}";
teamMember["msdyn_project"] = projectReference;
var createTeamMemberResponse = CallCreateTeamMemberAction(teamMember);

var description = $"My demo {DateTime.Now.ToShortTimeString()}";
var operationSetId = CallCreateOperationSetAction(project.Id, description);

var task1 = GetTask("1WW", projectReference);
var task2 = GetTask("2XX", projectReference, task1.ToEntityReference());
var task3 = GetTask("3YY", projectReference);
var task4 = GetTask("4ZZ", projectReference);

var assignment1 = GetResourceAssignment("R1", teamMember, task2, project);
var assignment2 = GetResourceAssignment("R2", teamMember, task3, project);

var task1Response = CallPssCreateAction(task1, operationSetId);
var task2Response = CallPssCreateAction(task2, operationSetId);
var task3Response = CallPssCreateAction(task3, operationSetId);
var task4Response = CallPssCreateAction(task4, operationSetId);

var assignment1Response = CallPssCreateAction(assignment1, operationSetId);
var assignment2Response = CallPssCreateAction(assignment2, operationSetId);

task2["msdyn_subject"] = "Updated Task";
var task2UpdateResponse = CallPssUpdateAction(task2, operationSetId);

project["msdyn_subject"] = $"Proj update {DateTime.Now.ToShortTimeString()}";
var projectUpdateResponse = CallPssUpdateAction(project, operationSetId);

List<UpdatedContour> updatedContours = new List<UpdatedContour>(); 
UpdatedContour updatedContour = new UpdatedContour(); 
updatedContour.Start = DateTime.UtcNow.Date; 
updatedContour.End = DateTime.UtcNow.Date.AddDays(1); 
updatedContour.Minutes = 120; 
updatedContours.Add(updatedContour); 

String serializedUpdate = JsonConvert.SerializeObject(updatedContours); 
var updateContoursResponse = CallPssUpdateContourAction(assignment1.Id, serializedUpdate, operationSetId); 

var task4DeleteResponse = CallPssDeleteAction(task4.Id.ToString(), task4.LogicalName, operationSetId);

var assignment2DeleteResponse = CallPssDeleteAction(assignment2.Id.ToString(), assignment2.LogicalName, operationSetId);

var dependency1 = GetTaskDependency(project, task2, task3);
var dependency1Response = CallPssCreateAction(dependency1, operationSetId);

CallExecuteOperationSetAction(operationSetId);
Console.WriteLine("Done....");

Additional samples

#region Call actions --- Sample code ----

/// <summary>
/// Calls the action to create an operationSet
/// </summary>
/// <param name="projectId">project id for the operations to be included in this operationSet</param>
/// <param name="description">description of this operationSet</param>
/// <returns>operationSet id</returns>
private string CallCreateOperationSetAction(Guid projectId, string description)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_CreateOperationSetV1");
    operationSetRequest["ProjectId"] = projectId.ToString();
    operationSetRequest["Description"] = description;
    OrganizationResponse response = organizationService.Execute(operationSetRequest);
    return response["OperationSetId"].ToString();
}

/// <summary>
/// Calls the action to create an entity
/// </summary>
/// <param name="entity">Scheduling entity</param>
/// <param name="operationSetId">operationSet id</param>
/// <returns>OperationSetResponse</returns>

private OperationSetResponse CallPssCreateAction(Entity entity, string operationSetId)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_PssCreateV1");
    operationSetRequest["Entity"] = entity;
    operationSetRequest["OperationSetId"] = operationSetId;
    return GetOperationSetResponseFromOrgResponse(organizationService.Execute(operationSetRequest));
}

/// <summary>
/// Calls the action to update an entity
/// </summary>
/// <param name="entity">Scheduling entity</param>
/// <param name="operationSetId">operationSet Id</param>
/// <returns>OperationSetResponse</returns>
private OperationSetResponse CallPssUpdateAction(Entity entity, string operationSetId)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_PssUpdateV1");
    operationSetRequest["Entity"] = entity;
    operationSetRequest["OperationSetId"] = operationSetId;
    return GetOperationSetResponseFromOrgResponse(organizationService.Execute(operationSetRequest));
}

/// <summary>
/// Calls the action to update an entity
/// </summary>
/// <param name="recordId">Id of the record to be deleted</param>
/// <param name="entityLogicalName">Entity logical name of the record</param>
/// <param name="operationSetId">OperationSet Id</param>
/// <returns>OperationSetResponse</returns>
private OperationSetResponse CallPssDeleteAction(string recordId, string entityLogicalName, string operationSetId)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_PssDeleteV1");
    operationSetRequest["RecordId"] = recordId;
    operationSetRequest["EntityLogicalName"] = entityLogicalName;
    operationSetRequest["OperationSetId"] = operationSetId;
    return GetOperationSetResponseFromOrgResponse(organizationService.Execute(operationSetRequest));
}

/// <summary> 
/// Calls the action to update a Resource Assignment contour
/// </summary> 
/// <param name="resourceAssignmentId">Id of the resource assignment to be updated</param> 
/// <param name="serializedUpdates">JSON formatted contour updates</param>
/// <param name="operationSetId">operationSet id</param> 
/// <returns>OperationSetResponse</returns> 
private OperationSetResponse CallPssUpdateContourAction(string resourceAssignmentId, string serializedUpdates string operationSetId) 
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_PssUpdateResourceAssignmentContourV1"); 
    operationSetRequest["ResourceAssignmentId"] = resourceAssignmentId; 
    operationSetRequest["UpdatedContours"] = serializedUpdates; 
    operationSetRequest["OperationSetId"] = operationSetId; 
    return GetOperationSetResponseFromOrgResponse(OrganizationService.Execute(operationSetRequest)); 
} 

/// <summary>
/// Calls the action to execute requests in an operationSet
/// </summary>
/// <param name="operationSetId">operationSet id</param>
/// <returns>OperationSetResponse</returns>
private OperationSetResponse CallExecuteOperationSetAction(string operationSetId)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_ExecuteOperationSetV1");
    operationSetRequest["OperationSetId"] = operationSetId;
    return GetOperationSetResponseFromOrgResponse(organizationService.Execute(operationSetRequest));
}

/// <summary>
/// This can be used to abandon an operationSet that is no longer needed
/// </summary>
/// <param name="operationSetId">operationSet id</param>
/// <returns>OperationSetResponse</returns>
protected OperationSetResponse CallAbandonOperationSetAction(Guid operationSetId)
{
    OrganizationRequest operationSetRequest = new OrganizationRequest("msdyn_AbandonOperationSetV1");
    operationSetRequest["OperationSetId"] = operationSetId.ToString();
    return GetOperationSetResponseFromOrgResponse(organizationService.Execute(operationSetRequest));
}


/// <summary>
/// Calls the action to create a new project
/// </summary>
/// <param name="project">Project</param>
/// <returns>project Id</returns>
private Guid CallCreateProjectAction(Entity project)
{
    OrganizationRequest createProjectRequest = new OrganizationRequest("msdyn_CreateProjectV1");
    createProjectRequest["Project"] = project;
    OrganizationResponse response = organizationService.Execute(createProjectRequest);
    var projectId = Guid.Parse((string)response["ProjectId"]);
    return projectId;
}

/// <summary>
/// Calls the action to create a new project team member
/// </summary>
/// <param name="teamMember">Project team member</param>
/// <returns>project team member Id</returns>
private string CallCreateTeamMemberAction(Entity teamMember)
{
    OrganizationRequest request = new OrganizationRequest("msdyn_CreateTeamMemberV1");
    request["TeamMember"] = teamMember;
    OrganizationResponse response = organizationService.Execute(request);
    return (string)response["TeamMemberId"];
}

private OperationSetResponse GetOperationSetResponseFromOrgResponse(OrganizationResponse orgResponse)
{
    return JsonConvert.DeserializeObject<OperationSetResponse>((string)orgResponse.Results["OperationSetResponse"]);
}

private EntityCollection GetDefaultBucket(EntityReference projectReference)
{
    var columnsToFetch = new ColumnSet("msdyn_project", "msdyn_name");
    var getDefaultBucket = new QueryExpression("msdyn_projectbucket")
    {
        ColumnSet = columnsToFetch,
        Criteria =
        {
            Conditions =
            {
                new ConditionExpression("msdyn_project", ConditionOperator.Equal, projectReference.Id),
                new ConditionExpression("msdyn_name", ConditionOperator.Equal, "Bucket 1")
            }
        }
    };

    return organizationService.RetrieveMultiple(getDefaultBucket);
}

private Entity GetBucket(EntityReference projectReference)
{
    var bucketCollection = GetDefaultBucket(projectReference);
    if (bucketCollection.Entities.Count > 0)
    {
        return bucketCollection[0].ToEntity<Entity>();
    }

    throw new Exception($"Please open project with id {projectReference.Id} in the Dynamics UI and navigate to the Tasks tab");
}

private Entity CreateProject()
{
    var project = new Entity("msdyn_project", Guid.NewGuid());
    project["msdyn_subject"] = $"Proj {DateTime.Now.ToShortTimeString()}";

    return project;
}



private Entity GetTask(string name, EntityReference projectReference, EntityReference parentReference = null)
{
    var task = new Entity("msdyn_projecttask", Guid.NewGuid());
    task["msdyn_project"] = projectReference;
    task["msdyn_subject"] = name;
    task["msdyn_effort"] = 4d;
    task["msdyn_scheduledstart"] = DateTime.Today;
    task["msdyn_scheduledend"] = DateTime.Today.AddDays(5);
    task["msdyn_start"] = DateTime.Now.AddDays(1);
    task["msdyn_projectbucket"] = GetBucket(projectReference).ToEntityReference();
    task["msdyn_LinkStatus"] = new OptionSetValue(192350000);

    //Custom field handling
    /*
    task["new_custom1"] = "Just my test";
    task["new_age"] = 98;
    task["new_amount"] = 591.34m;
    task["new_isready"] = new OptionSetValue(100000000);
    */

    if (parentReference == null)
    {
        task["msdyn_outlinelevel"] = 1;
    }
    else
    {
        task["msdyn_parenttask"] = parentReference;
    }

    return task;
}

private Entity GetResourceAssignment(string name, Entity teamMember, Entity task, Entity project)
{
    var assignment = new Entity("msdyn_resourceassignment", Guid.NewGuid());
    assignment["msdyn_projectteamid"] = teamMember.ToEntityReference();
    assignment["msdyn_taskid"] = task.ToEntityReference();
    assignment["msdyn_projectid"] = project.ToEntityReference();
    assignment["msdyn_name"] = name;
   
    return assignment;
}

protected Entity GetTaskDependency(Entity project, Entity predecessor, Entity successor)
{
    var taskDependency = new Entity("msdyn_projecttaskdependency", Guid.NewGuid());
    taskDependency["msdyn_project"] = project.ToEntityReference();
    taskDependency["msdyn_predecessortask"] = predecessor.ToEntityReference();
    taskDependency["msdyn_successortask"] = successor.ToEntityReference();
    taskDependency["msdyn_linktype"] = new OptionSetValue(192350000);

    return taskDependency;
}

#endregion


#region OperationSetResponse DataContract --- Sample code ----

[DataContract]
public class OperationSetResponse
{
[DataMember(Name = "operationSetId")]
public Guid OperationSetId { get; set; }

[DataMember(Name = "operationSetDetailId")]
public Guid OperationSetDetailId { get; set; }

[DataMember(Name = "operationType")]
public string OperationType { get; set; }

[DataMember(Name = "recordId")]
public string RecordId { get; set; }

[DataMember(Name = "correlationId")]
public string CorrelationId { get; set; }
}

#endregion

#region UpdatedContour DataContract --- Sample code ---- 

[DataContract] 
public class UpdatedContour 
{ 
[DataMember(Name = "start")] 
public DateTime Start { get; set; } 

[DataMember(Name = "end")] 
public DateTime End { get; set; } 

[DataMember(Name = "minutes")] 
public decimal Minutes { get; set; } 
} 

#endregion