Edit History
An Editor Client workflow, entitled Edit History, is available to users out-of-the-box (OOTB), and tracks the addition of new features, updates to existing features, and deletions. The benefit of the workflow button is that it offers an easily readable list of all changes made within each individual session. To learn more about the Edit History workflow, refer to the Edit History topic in the Editor XI Implementation Guide.
digraph workflow {
entry[
label = "entry"
se_type0 = SessionOperationTrigger
se_displayName0 = "Edit History"
se_sortOrder0 = 120
se_visibilityCondition0 = "(ctx, log) => {
return true;
}"
se_type1 = Action,
se_action1 = "(ctx, log) => {
if(ctx.Event.assignments != null)
{
ctx.State.Assignments = ctx.Event.assignments;
}
else
{
ctx.State.Assignments = new[] {ctx.Event.assignment};
}
}",
]
getAuditData[
se_executionOrder = ForEach
se_forEachEnumerable = Assignments
se_type0 = Api,
se_body0 = ""
se_method0 = "GET"
se_modifyRequest0 = "(request, ctx, log) => {
request.RequestUri = new Uri($\"api/v1/edithistory/auditdatasummary?uniqueFeaturesOnly=true&sessionId={ctx.State.__currentForEachItem.associatedSessionId}\", UriKind.RelativeOrAbsolute);
}",
se_processResponse0 = "async (response, ctx, log) => {
string layersString = ctx.State.Layers.ToString();
string summaryJsonString = await response.Content.ReadAsStringAsync();
JToken layersJson = JToken.Parse(layersString);
JToken summaryJson = JToken.Parse(summaryJsonString);
StringBuilder result = new StringBuilder();
foreach (var section in new[] { \"totalInserts\", \"totalUpdates\", \"totalDeletes\" })
{
result.AppendLine(\"**************************************************************\");result.AppendLine();
result.AppendLine($\"{section.Replace(\"total\", \"\")}:\");
result.AppendLine();
var entries = summaryJson[section];
if (entries == null || !entries.HasValues)
{
result.AppendLine(\"None\");
result.AppendLine();
continue;
}
foreach (var entry in entries.Children(var entry in entries.Children<JProperty>())
{
string[] parts = entry.Name.Split('_');
int count = (int)entry.Value;
if (parts.Length == 3 &&
int.TryParse(parts[0], out int layerId) &&
int.TryParse(parts[1], out int typeId) &&
int.TryParse(parts[2], out int codedValueCode))
{
var layer = layersJson[\"layers\"]?.FirstOrDefault(l => (int?)l[\"id\"] == layerId);
if (layer == null)
{
layer = layersJson[\"tables\"]?.FirstOrDefault(l => (int?)l[\"id\"] == layerId);
}
var layerName = layer?[\"name\"]?.ToString() ?? $\"Layer {layerId}\";
var type = layer?[\"types\"]?.FirstOrDefault(t => (int?)t[\"id\"] == typeId);
var typeName = type?[\"name\"]?.ToString() ?? $\"Type {typeId}\";
var codedValues = type?[\"domains\"]?[\"ASSETTYPE\"]?[\"codedValues\"];
var codedValue = codedValues?.FirstOrDefault(cv => (int?)cv[\"code\"] == codedValueCode);
var codedValueName = codedValue?[\"name\"]?.ToString() ?? $\"Code {codedValueCode}\";
result.AppendLine($\"{layerName}/{typeName}/{codedValueName}: {count}\");
}
else if (parts.Length == 3 &&
int.TryParse(parts[0], out int layerOnlyId) &&
string.IsNullOrEmpty(parts[1]) &&
string.IsNullOrEmpty(parts[2]))
{
var layer = layersJson[\"layers\"]?.FirstOrDefault(l => (int?)l[\"id\"] == layerOnlyId);
var layerName = layer?[\"name\"]?.ToString() ?? $\"Layer {layerOnlyId}\";
if (layerOnlyId == 500001)
{
layerName = \"Associations\";
}
result.AppendLine($\"{layerName}: {count}\");
}
else
{
result.AppendLine($\"Invalid key: {entry.Name}\");
}
result.AppendLine();
}
result.AppendLine();
result.AppendLine();
}
result.AppendLine(\"**************************************************************\");
ctx.State.AuditData = result;
}",
se_resultVariable0 = ""
se_service0 = "SessionManager"
se_uri0 = ""
]
getLayerData[
se_executionOrder = ForEach
se_forEachEnumerable = Assignments
se_type0 = GetCurrentOperationalMap,
se_resultVariable0 = "OpMap"
se_type1 = ArcGisApi,
se_configureRequest1 = "(request, ctx, log) => {
}",
se_determineUri1 = "(ctx, loger) => {
return new Uri($\"{ctx.State.OpMap.url}/layers?f=pjson\", UriKind.RelativeOrAbsolute);
}",
se_forceAuthentication1 = false,
se_method1 = "GET"
se_resultVariable1 = "Layers"
se_uri1 = ""
]
showData[
se_type0 = PromptUser,
se_promptCaption0 = "Edit History",
se_cardTemplate0 = "{
\"$schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\",
\"type\": \"AdaptiveCard\",
\"version\": \"1.3\",
\"body\": [
{
\"type\": \"TextBlock\",
\"style\":\"heading\",
\"text\": \"${auditData}\"
},
]
}",
se_determineCardData0 = "(ctx, log) => {
var test = new Dictionary<string,object>{{\"auditdata\",ctx.State.AuditData.ToString()}}
return JToken.FromObject(test);
}"
se_resultVariable0 = ""
]
entry->getLayerData->getAuditData->showData->exit
}
This workflow makes use of the AuditDataSummary endpoint, which can be modified using the following parameters:
-
user – oauth2|ESRI-Portal-YourPortalName|UserName (as an example)
-
startMoment – UNIX time in milliseconds
-
endMoment – UNIX time in milliseconds
-
layerId – number
-
assetGroup – number
-
assetType – number
-
sessionId – sessionID GUID
-
uniqueFeaturesOnly – determination factor
NOTE: This parameter determines if the service should return the number of changes performed OR the number of features that were changed. For example, you edited 5 breakers, and you edited the same breaker in multiple edit moments. As a result, although you edited 5 breakers, the number of changes can actually be greater, e.g., 10. If this is set totrue, the number of breakers is 5. If this is set tofalse, you see that breakers were edited 10 times.
The following is an example response for the AuditDataSummary endpoint:
{
"totalInserts": {
"0_204_232": 4,
"3_506_541": 1,
"9__": 6
},
"totalDeletes": {},
"totalUpdates": {
"3_506_541": 1,
"5_808_791": 2
}
}
The key format is LayerId-assetgroup-assettype – a conversion to names using the ArcGIS server endpoint (e.g., https://GISServerName.arcfmsolution.com/server/rest/services/FeatureServiceName/FeatureServer/layers?f=12345) would be necessary to have a nicer, human readable summary. An example of a conversion is available in the Edit History workflow.
The AuditData endpoint accepts the same parameters, with the exception of uniqueFeaturesOnly. It returns all the edit moments that were filtered based on the given parameters.
The following is an example response for the AuditData endpoint:
"editMoments": [
{
"description": "Autosaved Moment",
"moment": 1754044501687,
"user": oauth2|ESRI-Portal-YourPortalName|UserName,
"dateSaved": "2025-08-01T10:35:01.687+00:00",
"inserts": {
"9__": [
{
"id": 339718,
"lastUpdate": 1754051701000
}
]
},
"updates": {},
"deletes": {
"0_201_201": [
{
"id": 50208,
"lastUpdate": 1749044610000
}
]
}
},
{
"description": "Autosaved Moment",
"moment": 1754044671620,
"user": oauth2|ESRI-Portal-YourPortalName|UserName,
"dateSaved": "2025-08-01T10:37:51.62+00:00",
"inserts": {},
"updates": {
"0_207_261": [
{
"id": 34,
"lastUpdate": 1754051871000
}
]
},
"deletes": {}
},
]