When working with historical or test data in Dynamics 365 Field Service, we often come across the need to clean up old Work Orders and their related records. While it might seem straightforward to just delete the Work Order, there are quite a few interdependent entities and plugin validations that can make this task tricky.
We wanted to delete all msdyn_workorder records created before a certain date along with their related child records such as:
Work Order Products
Work Order Services
Work Order Incidents
Bookable Resource Bookings
Time Entries, Service Tasks, Resolutions, etc.
The Field Service solution has many plugins that enforce business rules during deletion.
If a Work Order is in a Posted state (msdyn_systemstatus = 690970004), attempting to delete any of its child records will throw errors like:
Failed to delete msdyn_workorderproduct {GUID}: A record can’t be deleted if the work order has been posted.
So to delete the Posted work order, we had to update its status to Cancelled.
If the Work Order has any associated Bookable Resource Booking records, deletion is blocked:
Work order can’t be deleted because there are associated Resource Booking records. Please delete these records first.
And while deleting Bookings, we might hit even deeper integration issues, like this one from a virtual entity plugin
This error originates from the FnO Virtual Entity plugin and usually appears if the user linked to the Booking doesn’t exist in the FnO system — a hard blocker for environments with dual-write enabled.
Also, we had to temporarily change the Delete action to Remove Link from Restrict for the relationship between msdyn_workorder and msdyn_actual_workorder.
Here’s a high-level overview of what we did:
Fetched work orders based on our criteria. Loop through each work order:
Revert the status if it is Posted (so it becomes deletable)
Delete all child records in a proper order
Delete the Work Order itself
Below is the sample code we used for deleting the work orders and its associated records successfully.
public void DeleteAllWorkOrders()
{
QueryExpression query = new QueryExpression("msdyn_workorder");
query.ColumnSet = new ColumnSet("msdyn_workorderid", "msdyn_name", "createdon", "msdyn_systemstatus");
query.Criteria = new FilterExpression();
query.Criteria.Conditions.Add(
new ConditionExpression("createdon", ConditionOperator.LessThan, new DateTime(2025, 1, 1))
);
query.Orders.Add(new OrderExpression("createdon", OrderType.Descending));
var workOrders = _service.RetrieveMultiple(query).Entities;
Console.WriteLine($"Found {workOrders.Count} work orders.");
try
{
foreach (var wo in workOrders)
{
var woId = wo.Id;
var systemStatusCode = wo.Contains("msdyn_systemstatus") ? ((OptionSetValue)wo["msdyn_systemstatus"]).Value : 0;
try
{
//// 1. If Posted revert status
if (systemStatusCode == 690970004) // Posted
{
var revert = new Entity("msdyn_workorder", woId)
{
["msdyn_systemstatus"] = new OptionSetValue(690970005) // Canceled
};
_service.Update(revert);
Console.WriteLine("Reverted system status to Cancelled.");
}
// 2. Delete dependencies
DeleteRelatedRecords("msdyn_workorderservice", "msdyn_workorder", woId);
DeleteRelatedRecords("msdyn_workorderproduct", "msdyn_workorder", woId);
DeleteRelatedRecords("msdyn_workorderservicetask", "msdyn_workorder", woId);
DeleteRelatedRecords("msdyn_workorderincident", "msdyn_workorder", woId);
DeleteRelatedRecords("msdyn_workorderresolution", "msdyn_workorder", woId);
DeleteRelatedRecords("bookableresourcebooking", "msdyn_workorder", woId);
DeleteRelatedRecords("msdyn_timeentry", "msdyn_workorder", woId);
// 3. Delete the Work Order
_service.Delete("msdyn_workorder", woId);
Console.WriteLine($"Deleted Work Order: {woId}\n");
}
catch (Exception ex)
{
Console.WriteLine($"Error processing Work Order {woId}: {ex.Message}");
Console.WriteLine("Continuing with the next Work Order...\n");
}
}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.WriteLine("All Work Orders cleaned up.");
Console.WriteLine("Press any key to continue !");
}
private void DeleteRelatedRecords(string entityName, string lookupField, Guid workOrderId)
{
var query = new QueryExpression(entityName)
{
ColumnSet = new ColumnSet(false),
Criteria =
{
Conditions =
{
new ConditionExpression(lookupField, ConditionOperator.Equal, workOrderId)
}
}
};
var records = _service.RetrieveMultiple(query).Entities;
foreach (var record in records)
{
try
{
_service.Delete(entityName, record.Id);
Console.WriteLine($"Deleted {entityName}: {record.Id}");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to delete {entityName} {record.Id}: {ex.Message}");
}
}
}
Recently while trying to set Bookable Resource Booking record’s status as completed or trying to change Settings >> General >> Field Service Settings >> Time Entry, we were getting below error
We can see the Dynamics 365 Project Service Installation Failed and on retrying the installation it was giving below error.
The fix that worked here was to install the Project Operations.
Below we can see the resource cell template (or view) applied that defines the images, values, and fields displayed for the resource in the Schedule Board.
Now suppose we want to show the Account (custom field) value also, that would make it easy for the Dispatcher to schedule them from within the Schedule board.
For this, we need to select Board Settings for the Schedule Board.
Navigate to the Other section within the Board Settings.
We’d first start by adding/defining a new Resource Cell Template and a new Retrieve resource query template.
We have added below Div tag below to show the account name.
Save this new template
Next, add the below attribute tag for the account field in the Fetch XML for it to retrieve the value of the account, here name property holds the schema name of the field.
Select Save as new to add the new template.
On refreshing the Schedule Board we can see the Account value added to the view, however, it shows the Guid of the account record.
To get the label /name, edit the Resource Cell Template and add the below UFX Bag (UFX directives for querying the data) to fetch the name of the account.
Update the Sample Resource Query, Save the changes, and refresh the schedule board.
We can see the Guid replaced by the Account name there.
Recently we found that in one of the environments, the Date Window Start / End Dates was not showing up on the Work Order Forms, unlike the other environments.
Check the forms below
Well, the show and hide of the Date Window Start / End Date is controlled from Field Service Settings >> Fields Service >> Agreement
Setting Pre/Post Booking Flexibility Date Field Population to Populate Date Window Start / Date Window End will unhide the fields on the form.
Below is the corresponding out-of-the-box script that shows/hides the fields.
Also worth noting is that these fields are deprecated, as Microsoft recommends using Time From Promised and Time To Promised fields instead to define the date window in which a job is performed.
When setting up an agreement, you can control how work order scheduling works with pre-booking and post-booking flexibility. These fields define a window of time for scheduling each work order.
There are two options for how this window is used:
Date Window Start/End: The agreement populates these fields with the scheduling window. This makes the “Date Window Start/End” section visible on the work order itself.
Time From/To Promised: The agreement doesn’t directly affect these fields, but the scheduling window is considered during the scheduling process. “Date Window Start/End” will be hidden in this case.
This setting essentially determines where the scheduling window information is stored and displayed for work orders generated from this agreement.
Recently in one of our test environments, the out the box flow – “Deserialization of Inspection Definition” wasn’t getting triggered even when it was in the On state.
This was because the “CallbackRegistrationExpanderFilter” operation was failing with the below error.
Unexpected failure during ValidateUserAccessCached. Ex: System.ServiceModel.FaultException`1[Microsoft.Xrm.Sdk.OrganizationServiceFault]: The specified user(Id) is disabled. Consider enabling this user. Additional Details: User IsDisabled=True, IsLicensed=True. ImpersonatingSystemUserId. (Fault Detail is equal to Exception details:
ErrorCode: 0x80040225
Message: The specified user(Id = xxxx-xxxx-xxxx-xxx-xxxxxx) is disabled. Consider enabling this user. Additional Details: User IsDisabled=True, IsLicensed=True. ImpersonatingSystemUserId=xxxx-xxxx-xxxx-xxx-xxxxxx)
This was because the flow’s corresponding Callback Registration record’s Owner was a user whose account was now disabled in CRM.
So the option to fix was to Turn off and Turn On the flow, which will delete the old callback registration record and will create a new one with the user who is turning off / on the flow. Updating the owner of the flow will not work here, as it won’t update the existing callback registration record.
Also we had another observation, if a callback registration is owned by a user who is a proper / enabled user in CRM, simply turning off and on the flow using a different user account will not delete the existing callback registration record and create a new one with that particular user as the owner. In this case we will have to explicitly delete the existing callback registration record.
We might get the below message/notification on opening the Project form –
Dates and values are displayed in your time zone. Because the project could take place in a different time zone, the project schedule may appear incorrect.
This message will appear if the Project’s timezone differs from the logged-in user’s timezone.
The project’s timezone is defined in the Calendar
For the user, it is defined in the user settings – Personalization Settings
Let us set it to be the same time zone as the project.
We will not get the notification now for that specific project.
If we check in detail, we can see below out-of-the-box method validateUserAndProjectTimezone getting fired on the onLoad of Project form, which fetches and compares the Timezone associated with the Project and Logged In User and shows the notification.