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.
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",
"[email protected]": "msdyn_projects(projectGuidHere)",
"[email protected]": "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.
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.
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",
"[email protected]": "msdyn_projects(projectGuidHere)",
"msdyn_projecttaskid": "taskGuidHere"
},
"OperationSetId": "operationSetIdHere"
}
“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
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)
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).
Hello Antti,
Very helpful post.
Happy New Year!
My flow run Successfully but Task is not appearing on Project form Task Tab any idea?