by Martin Laukkanen | Jul 26, 2018 | Flow, How to, Workflow
Flow is a great (one day) replacement for SharePoint Designer workflows, however at the moment the Project Online connectors are still in “preview” and as such as woefully inadequate. In my view the biggest omission is the inability to set custom fields, among many other failings this one is pretty much always step one or two on any workflow I have ever needed to create for Project Server.
You may have seen some MS demos where they show a quick easy way to circumvent this by using the HTTP action, unfortunately I’ve yet to find any online examples of that so I’m going to write one myself. This article focuses on Enterprise Resource custom fields but applies equally well to Task custom fields. However, the process is actually simpler for Project level custom fields and Paul Mather has blogged on the topic before here. Although he doesn’t cover Flow in that article, you can convert his JavaScript code to a JSON body and use it as as below. There’s also this StackExchange post that covers Project fields using a different method.
Updating Enterprise Resource fields via REST
It turns out this is not supported!
Please correct me if I’m wrong here, I’ve spent many hours looking for a solution for this from MS to no avail!
Well for me I figure if MS released an API 5 years ago and never got around to finishing it then supported or not I need a way to do my job. Fortunately it is possible via CSOM/JSOM to do this, and by understanding how all the client side API’s work you will see that it is possible and (arguably) fully supported to do this via REST using the internal methods used by CSOM/JSOM.
Updated Enterprise Resource default fields via REST
Before we look at custom fields lets cover how to set the default fields such as; Group, Email, etc. These can be set via the normal REST endpoint, and frankly this is how it also *should* be possible to set custom fields.
Assuming the resource exists and you want to update it by GUID, the following HTTP PATCH request will do it:
PATCH /sites/pwa/_api/ProjectServer/EnterpriseResources('[guid]') HTTP/1.1
Host: contoso.sharepoint.com
Accept: application/json; odata=verbose
Content-Type: application/json; odata=verbose
{
"__metadata": {
"type": "PS.EnterpriseResource"
},
"Group": "Test group",
"Email": "[email protected]"
}
That’s a raw HTTP request from PostMan, the important bits are the JSON and the PATCH type of request, yes a standard POST or PUT request will not work. In that JSON body of the request you can list multiple custom fields as long as you adhere to correct JSON syntax, for the full list of field names, browse to the endpoint in your browser: /sites/pwa/_api/ProjectServer/EnterpriseResources.
However if you look at the endpoints you’ll see the custom field values referenced by internal name, e.g.: Custom_x005f_000039b78bbe4ceb82c4fa8c0c400284
Unfortunately it is not possible to add to your JSON the CF value, like: “Custom_x005f_000039b78bbe4ceb82c4fa8c0c400284″: “Text value for field” as a result we have to work a bit harder.
Enter the ProcessQuery endpoint
(If you’re not interested in the why, but just the how to do this, you may want to skip to the next section!)
If you’re familiar with Fiddler traces of any of the SharePoint client side object models then you will have seen that all of the API calls made are translated into HTTP calls to an internal endpoint: _vti_bin/client.svc/ProcessQuery while not documented anywhere I have found, you can see that basically this is where the ClientContext.ExecuteQueryAsync(…) call from either CSOM or JSOM is performing the requested operation.
The ProcessQuery endpoint accepts a HTTP POST request with a body that looks something like this:
<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library">
<Actions>
<Method Name="SetCustomFieldValue" Id="9" ObjectPathId="6">
<Parameters>
<Parameter Type="String">Custom_f825c154928ae81180da00155df8aa23</Parameter>
<Parameter Type="String">text field value</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="10" ObjectPathId="6" />
</Actions>
<ObjectPaths>
<Method Id="6" ParentId="4" Name="GetById">
<Parameters>
<Parameter Type="String">5e658680-838a-e811-80df-00155df8b01b</Parameter>
</Parameters>
</Method>
<Property Id="4" ParentId="0" Name="EnterpriseResources" />
<Constructor Id="0" TypeId="{3a609e5f-e2a1-497c-87a2-e5e8a781c096}" />
</ObjectPaths>
</Request>
That happens to be the query syntax to set a single text custom field for a resource. If you read through the content you can see (in order of Id properties) what is happening, in short the above translates to the following (pseudo) CSOM code:
var res = EnterpriseResources.GetById([guid]);
res.SetCustomFieldValue("Custom_f825...", "text field value");
EnterpriseResources.Update();
Pretty cool huh? :)
It get’s better, as if you trace in fiddler some code that does multiple things, like for example setting 3 or 4 custom field values, you will see that they are all batched into a single POST query just as you’d expect when using ExecuteQueryAsync in your code. The change in the method above is simply additional <Action> elements in the XML, one for each call to SetCustomFieldValue (note: incrementing the Id property is also necessary).
The remainder of that XML deserves a short explanation, ignoring the XML schema stuff the only other variable defined in the above example is a GUID: {3a609e5f-e2a1-497c-87a2-e5e8a781c096}. This GUID refers to the ServerTypeId that is defined in the Microsoft.ProjectServer.Client.dll file specifically the PS.ProjectServer constructor, and importantly “EnterpriseResources” is a public property of that class. So with that in mind you could re-use this GUID and make calls to any other public property to construct the call you need.
I prefer to cheat, and simply write a small snippet of JSOM (or CSOM) code that makes the call I want in REST, then use fiddler to get the XML body created when I make that call.
Calling a Project Online API from Flow
Now that we know what the HTTP request needs to look like we can start building a flow. Not so long ago this was much more difficult as we had to worry about OAuth2 and AzureAD apps and such, but now thankfully MS have added a new action to the SharePoint actions list which we are going to use:
I’ve created a new flow with a Project Online trigger “When a new resource is created”, and then added the above HTTP request action.
You need to populate the following values:
- Site Address: URL to your PWA
- Method: POST (for custom fields, or if out of the box fields PATCH)
- Uri: _vti_bin/client.svc/ProcessQuery
- Headers:
- Accept: application/json; odata=verbose
- Content-Type: text/xml
- Body: [Our XML body from the previous step]
Note: The header for Content-Type must be “text/xml” to match the content, if you were updating a default field using the JSON body from the beginning of this article, this would also be set to “application/json; odata=verbose”.
Thanks to the SharePoint HTTP action we don’t have to worry about authentication here, so actually that is it! Well almost.
Parameterizing the Flow action
Adding some variables to the flow allows us to parameterize the action based on whatever logic you want, once that’s done replace the values in the XML like this:
Important: Notice I have stripped the XML of tabs, spaces and line endings! This is required, if you don’t do that you will get an Unknown Error on larger query bodies.
Done.
What about default fields?
For completeness if you wanted to update the internal Group field for a resource, the http request action would look like this:
Note the resource GUID is in the URI and both the Method and Headers are different!
What about Lookup Table Values?
Lookup table value updates via REST have the same requirements as updating those values via JSOM (or CSOM), see my earlier post – Updating Lookup Table values using JSOM – for more on that.
In summary though, we need to first identify the lookup entry internal name, something like “Entry_412bb4bd5661e711a989000d3a28f1ff” then we need to pass that to the request as an array.
The request body now will look like this:
<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library">
<Actions>
<Method Name="SetCustomFieldValue" Id="13" ObjectPathId="8">
<Parameters>
<Parameter Type="String">Custom_f825c154928ae81180da00155df8aa23</Parameter>
<Parameter Type="String">text field value</Parameter>
</Parameters>
</Method>
<Method Name="SetCustomFieldValue" Id="14" ObjectPathId="8">
<Parameters>
<Parameter Type="String">Custom_000039b78bbe4ceb82c4fa8c0c400284</Parameter>
<Parameter Type="Array">
<Object Type="String">Entry_412bb4bd5661e711a989000d3a28f1ff</Object>
</Parameter>
</Parameters>
</Method>
<Method Name="Update" Id="15" ObjectPathId="6" />
</Actions>
<ObjectPaths>
<Property Id="6" ParentId="0" Name="EnterpriseResources" />
<Method Id="8" ParentId="6" Name="GetById">
<Parameters>
<Parameter Type="String">0bd67eed-538e-e811-aadb-000d3a28f1ff</Parameter>
</Parameters>
</Method>
<Constructor Id="0" TypeId="{3a609e5f-e2a1-497c-87a2-e5e8a781c096}" />
</ObjectPaths>
</Request>
I’ve left in the previous example to demonstrate how to set two custom fields at once, but the important part here is lines 9 – 16 which include the additional syntax you will need to set an array type value. Once again passing the value as an array is required for both single value lookups as well as multi-value.
Final words
This ProcessQuery endpoint is not publicly documented by Microsoft but a long time ago I discussed this with some people from the product group and their response was that while not officially supported, it is supported in the sense that it will not change until JSOM and CSOM are both disabled, as that’s how they internally work.
So use it with the usual warnings, but biggest of all: I take no responsibility or offer no support for how you use the above. Feel free to ask a question in the comments below, if I can help I will, but no guarantees.