D365 Project Operations, Project for the web: Calling Schedule APIs from Power Automate without custom connectors

Tested on Project Operations on Dataverse version 4.12.0.152 (July 2021 update), Project for the web solution version 1.0.12.148

The last time I wrote about Schedule APIs, custom connectors were the only way to call some of the APIs from a cloud flow. This was due to a Power Automate related bug, where some of the APIs (for example msdyn_PssCreateV1) didn’t work when they were called with the Perform an unbound action action in Power Automate. As of ca. August 2021, the bug with Power Automate seems to have been resolved. This simplifies a low-code approach to customizing and extending Dynamics 365 Project Operations and Project for the web immensely. If you’ve read my previous post – and you should before reading this one – you’ll know that while I’m all about low-code, I think the Schedule APIs are a capability that should be used with C# in a production implementation. Why? Well, C# is a lot faster to write than playing around with the different actions and JSON. With plug-ins and Azure Functions we can also create synchronous processes and leverage the event framework.

Using the Perform an unbound action action to call Schedule APIs

The way schedulable tables are created or updated works the same way whether custom connectors or native actions are used. The process and the JSON schemas are exactly the same and when the APIs are called, it’s still necessary to know some basics of JSON to build the required schema. Using the native Perform and unbound action action does require a hack to use it. Check out MVP Linn Zaw Win’s post here, and search for trim. You’ll learn about the trim expression and about using a custom value for an action, so that the Action paramaters multiline textbox can be populated with JSON. Image 1 below shows what the unbound action msdyn_PssCreateV1 looks like in Power Automate with and without a trim expression.

Image 1. Action msdyn_PssCreateV1 with and without a trim expression.

Using flow to create rows in schedulable tables

Let’s go through a simple flow that can used to create rows in schedulable tables. While the examples in my previous Schedule API post were more focused on a plausible business scenario, this approach and flow are only for demonstrating how unbound actions are used to call the Schedule APIs. I’ve used a manual trigger that takes a project’s and a project bucket’s GUIDs, and a project task’s subject as inputs. This way you can have a project row open in you browser tab to watch what happens when a task is created and then updated.

Let’s look at the flow itself. I have some preliminary steps that prep dates for start/finish columns msdyn_start / msdyn_finish so that all dates that are being used fall on weekdays. I’m not covering these initial steps in this blog post as you can simply create a calendar template that has work hours for Monday to Sunday. I just wanted to make it the hard way and play around with some date/time expressions.

The first step is to create an OperationSet by calling the msdyn_CreateOperationSetV1 action. The OperationSetId can then be extracted with the following expression:
outputs('Call_msdyn_CreateOperationSetV1')?['body/OperationSetId']

After an OperationSet has been created, the msdyn_PssCreateV1 action is called to create a project task. The JSON schema is slightly different depending on whether you’re creating a project task in Project Operations or vanilla Project for the web. In ProjOps, the msdyn_LinkStatus property is required and its value has to be 192350000. Why? I don’t know but I’m trying to figure it out. If you’re using vanilla P4W, that property can’t be used or the action will fail.

The JSON schema I’ve used for ProjOps is immediately below this paragraph. Remember to escape the @ with an @ or create a compose action for @odata.type and use the compose’s output as dynamic content in the Action parameters textbox. Start/finish dates might require some trial and error. As I’m in UTC +03 and have my time zone set to GMT +2 in personal settings, project task start needs to be set to 06:00:00 hundred hours and project task finish to 14:00:00 hundred hours. This way the database values in Dataverse will be correct and the created project task will have a duration of 1 day. Task are hard coded from 9 AM/09:00 to 5 PM/17:00, which equates to 6 AM/06:00 to 2 PM/14:00 in the database. The date/time format used should be yyyy-MM-ddTHH:mm:ssZ.

{
    "Entity": {
      "msdyn_LinkStatus": 192350000,
      "msdyn_subject": "taskSubjectHere",
      "msdyn_start": "yyyy-MM-ddTHH:mm:ssZ",
      "msdyn_finish": "yyyy-MM-ddTHH:mm:ssZ",
      "@@odata.type": "Microsoft.Dynamics.CRM.msdyn_projecttask",
      "msdyn_project@odata.bind": "msdyn_projects(projectGuidHere)",
      "msdyn_projectbucket@odata.bind": "msdyn_projectbuckets(bucketGuidHere)"
    },
    "OperationSetId": "operationSetIdHere"
  }

The third and final mandatory action is executing the created OperationSet. It’s done by calling the msdyn_ExecuteOperationSetV1 action.

Image 2. Creating an OperationSet, calling msdyn_PssCreateV1, and executing the OperationSet.

Using flow to update rows in schedulable tables

The next section in the flow will update the finish date of the project task that was created. Keep the Tasks tab open on your project row in D365 while the flow is running. You’ll be able to see PSS create the project task and then update it as the flow progresses. The tricky thing is getting the GUID of the created project task from OperationSetResponse of the msdyn_PssCreateV1 action. The output isn’t very low-code friendly but the project task’s GUID can be extracted either by using xml and XPath or by doing a compose/parse/filter/compose. I try to avoid xml+XPath approaches so below are the steps required for extracting projecttaskid with four actions.

First, compose OperationSetResponse with the following expression:
outputs('Call_msdyn_PssCreateV1')?['body/OperationSetResponse']

Next, parse the JSON using the following schema:

{
    "type": "object",
    "properties": {
        "<OperationSetResponses>k__BackingField": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "Key": {
                        "type": "string"
                    },
                    "Value": {
                        "type": "string"
                    }
                },
                "required": [
                    "Key",
                    "Value"
                ]
            }
        }
    }
}

The third step is to filter the following array by looking for a Key that equals recordId:
body('Parse_JSON')?['<OperationSetResponses>k__BackingField']

The recoredId can then be composed with a last expression:
last(body('Filter_array')).value

Throw in a delay of 20-30 seconds so that you have time to navigate to the Tasks tab to witness the results.

Image 3. Getting the project task’s GUID (recordId) from msdyn_PssCreateV1’s output.

The update part is pretty much a rinse and repeat of the create steps. Create an OperationSet, call msdyn_PssUpdateV1, and execute the OperationSet. The JSON for the update is naturally different than for the create, and the update is done with the msdyn_PssUpdateV1 action. The JSON I’ve used is:

{
    "Entity": {
      "msdyn_subject": "taskSubjectHere-updated",
      "msdyn_finish": "newFinishDateHere",
      "@odata.type": "Microsoft.Dynamics.CRM.msdyn_projecttask",
      "msdyn_project@odata.bind": "msdyn_projects(projectGuidHere)",
      "msdyn_projecttaskid": "taskGuidHere"
    },
    "OperationSetId": "operationSetIdHere"
  }
Image 4. Updating the project task that was previously created with msdyn_PssUpdateV1.
Disclaimer:
All my blog posts reflect my personal opinions and findings unless otherwise stated.

3 thoughts on “D365 Project Operations, Project for the web: Calling Schedule APIs from Power Automate without custom connectors”

  1. “message”: “An error occurred while validating input parameters: Microsoft.OData.ODataException: Bad Request – Error in query syntax.\r\n at Microsoft.OData.UriParser.ODataPathParser.ExtractSegmentIdentifierAndParenthesisExpression(String segmentText, String& identifier, String& parenthesisExpression)\r\n at Microsoft.OData.UriParser.ODataPathParser.CreateFirstSegment(String segmentText)\r\n at Microsoft.OData.UriParser.ODataPathParser.ParsePath(ICollection`1 segments)\r\n at Microsoft.OData.UriParser.ODataPathFactory.BindPath(ICollection`1 segments, ODataUriParserConfiguration configuration)\r\n at Microsoft.OData.UriParser.ODataUriParser.Initialize()\r\n at Microsoft.Crm.Extensibility.OData.CrmEdmEntityReference.CreateCrmEdmEntityReference(Uri link, IEdmModel edmModel, CrmODataExecutionContext context, EntitySetSegment& entitySetSegment)\r\n at Microsoft.Crm.Extensibility.ODataV4.ODataParameterReaderExtensions.TryReadFlowPrimitiveData(ODataParameterReader oDataParameterReader, ODataDeserializerContext readContext, CrmEdmEntityObject& result)\r\n at Microsoft.Crm.Extensibility.ODataV4.ODataParameterReaderExtensions.TryReadFlowPrimitive(ODataParameterReader oDataParameterReader, ODataDeserializerContext readContext, CrmEdmEntityObject& result)\r\n at Microsoft.Crm.Extensibility.ODataV4.CrmODataActionPayloadDeserializer.Read(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext)\r\n at System.Web.OData.Formatter.ODataMediaTypeFormatter.ReadFromStream(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)”

    Getting this Error

  2. Hi Antti,
    Thanks for this post! It’s really helpful. But I’m getting an error at the “Call_msdyn_PssCreateV1” action and here is the error-
    An error occurred while validating input parameters: Microsoft.OData.ODataException: Bad Request – Error in query syntax.\r\n at Microsoft.OData.UriParser.ODataPathParser.ExtractSegmentIdentifierAndParenthesisExpression(String segmentText, String& identifier, String& parenthesisExpression)\r\n at Microsoft.OData.UriParser.ODataPathParser.CreateFirstSegment(String segmentText)\r\n at Microsoft.OData.UriParser.ODataPathParser.ParsePath(ICollection`1 segments)\r\n at Microsoft.OData.UriParser.ODataPathFactory.BindPath(ICollection`1 segments, ODataUriParserConfiguration configuration)\r\n at Microsoft.OData.UriParser.ODataUriParser.Initialize()\r\n at Microsoft.Crm.Extensibility.OData.CrmEdmEntityReference.CreateCrmEdmEntityReference(Uri link, IEdmModel edmModel, CrmODataExecutionContext context, EntitySetSegment& entitySetSegment)\r\n at Microsoft.Crm.Extensibility.ODataV4.ODataParameterReaderExtensions.TryReadFlowPrimitiveData(ODataParameterReader oDataParameterReader, ODataDeserializerContext readContext, CrmEdmEntityObject& result)\r\n at Microsoft.Crm.Extensibility.ODataV4.ODataParameterReaderExtensions.TryReadFlowPrimitive(ODataParameterReader oDataParameterReader, ODataDeserializerContext readContext, CrmEdmEntityObject& result)\r\n at Microsoft.Crm.Extensibility.ODataV4.CrmODataActionPayloadDeserializer.Read(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext)\r\n at System.Web.OData.Formatter.ODataMediaTypeFormatter.ReadFromStream(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)

    1. It could be that the fix to Power Automate hasn’t rolled to all stations yet. The JSON should work both with the trim function and without it as well (latest development).

Leave a Reply

Your email address will not be published. Required fields are marked *