Reconcile & Post Automation for Utility Network Edits
Reconciling and posting by hand works until an estate has more than a handful of open versions, at which point it stops happening reliably and starts happening in bursts before deadlines. That is the worst possible cadence: the conflicts are large, the person resolving them is under time pressure, and the post lands without validation because validation is what gets dropped when the window is short. Automating the mechanical half — reconcile, classify, validate, prove — while leaving the judgement half to people turns versioning from a periodic ordeal into a nightly non-event. This guide builds that job, and it deliberately refuses to post anything whose conflicts were not automatically resolvable, following the conflict taxonomy set out in branch versioning and conflict resolution.
Environment Prerequisites
- ArcGIS Pro 3.2+ or ArcGIS Enterprise 11.2+ with branch versioning enabled on the utility
network’s feature dataset, and
arcpyimportable from Python 3.11. - Two portal identities: a reconcile account with edit rights and no post rights, and a post service account with post rights. Running both halves as one identity removes the control this job exists to provide.
- A version inventory — the list of open versions with their owner, creation time and work order — readable from the versioning service so the job knows what it is reconciling.
- A per-field ownership map declaring which system owns which attribute, used to decide the only conflict class the job may resolve unattended.
- A validated baseline: the default version’s topology clean at the start of the run, so any dirty area found after a reconcile belongs to that reconcile.
- A defined post window that does not overlap the reconcile schedule, because a reconcile against a default version that is being posted to fails intermittently and looks like a database fault.
- An append-only audit table with insert rights for both accounts and no update rights for anyone.
Schema-Aware Validation Protocol — Run Before the Job
- Confirm the default version is clean before the run. Dirty areas inherited from the previous night make every validation result in this run ambiguous.
- Confirm no version is older than the policy allows. A version months past its expected lifetime should be escalated rather than reconciled quietly; it is a process failure, and automating around it hides the failure.
- Verify the ownership map covers every field the job may auto-resolve. A field with no declared owner must never be auto-merged, because the default in that case is last-writer-wins under a different name.
- Check that the post window is clear. Query for in-progress posts before starting; a reconcile that races a post produces errors that will be misattributed to the network.
- Confirm the confirming-trace origins are defined. For versions that touched connectivity, the job needs to know which controller or device to trace from, or the proof step silently degrades into no proof at all.
Minimal Reproducible Implementation
The job below reconciles each open version, classifies its conflicts, validates the topology, runs the confirming trace where connectivity moved, and returns a per-version verdict. It never posts; it returns the set of versions that are safe to post, and a separate call under the post account acts on that set.
from __future__ import annotations
import logging
from dataclasses import dataclass, field
import arcpy
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
LOG = logging.getLogger("reconcile-post")
AUTO_RESOLVABLE = {"ATTRIBUTE"} # everything else is held for a person
@dataclass
class VersionVerdict:
name: str
conflicts: dict[str, int] = field(default_factory=dict)
resolved: int = 0
dirty_after: bool = True
trace_ok: bool | None = None
problems: list[str] = field(default_factory=list)
@property
def postable(self) -> bool:
held = {k: v for k, v in self.conflicts.items() if k not in AUTO_RESOLVABLE and v}
return (not self.problems and not held and not self.dirty_after
and self.trace_ok is not False)
def _classify_conflicts(un_path: str, version: str) -> dict[str, int]:
"""Count conflicts by class for a reconciled version.
The conflict table names the row and the kind of difference; this collapses it into
the four classes the policy distinguishes, so the verdict is readable.
"""
counts = {"ATTRIBUTE": 0, "GEOMETRY": 0, "DELETE_UPDATE": 0, "ASSOCIATION": 0}
table = f"{un_path}_Conflicts"
with arcpy.da.SearchCursor(table, ["VERSIONNAME", "CONFLICTTYPE", "ISASSOCIATION"]) as cur:
for name, kind, is_assoc in cur:
if name != version:
continue
if is_assoc:
counts["ASSOCIATION"] += 1
elif kind in counts:
counts[kind] += 1
else:
counts["ATTRIBUTE"] += 1
return counts
def reconcile_version(
un_path: str,
workspace: str,
version: str,
trace_origin: str | None = None,
) -> VersionVerdict:
"""Reconcile one version, validate it, and decide whether it is safe to post."""
verdict = VersionVerdict(name=version)
try:
arcpy.management.ReconcileVersions(
input_database=workspace,
reconcile_mode="ALL_VERSIONS",
target_version="sde.DEFAULT",
edit_versions=[version],
acquire_locks="LOCK_ACQUIRED",
abort_if_conflicts="NO_ABORT",
conflict_definition="BY_ATTRIBUTE",
conflict_resolution="FAVOR_TARGET_VERSION",
with_post="NO_POST",
)
except arcpy.ExecuteError as exc:
verdict.problems.append(f"reconcile: {exc}")
return verdict
verdict.conflicts = _classify_conflicts(un_path, version)
verdict.resolved = verdict.conflicts.get("ATTRIBUTE", 0)
try:
arcpy.un.ValidateNetworkTopology(in_utility_network=un_path)
except arcpy.ExecuteError as exc:
verdict.problems.append(f"validate: {exc}")
return verdict
with arcpy.da.SearchCursor(f"{un_path}_DirtyAreas", ["OBJECTID"]) as cur:
verdict.dirty_after = any(True for _ in cur)
if trace_origin:
try:
arcpy.un.Trace(in_utility_network=un_path, trace_type="CONNECTED",
starting_points=trace_origin)
verdict.trace_ok = True
except arcpy.ExecuteError as exc:
verdict.trace_ok = False
verdict.problems.append(f"confirming trace: {exc}")
LOG.info("%s: conflicts=%s dirty=%s trace=%s postable=%s", version,
verdict.conflicts, verdict.dirty_after, verdict.trace_ok, verdict.postable)
return verdict
def nightly_run(un_path: str, workspace: str,
versions: dict[str, str | None]) -> list[VersionVerdict]:
"""Reconcile every open version and return the verdicts. Posts nothing."""
return [reconcile_version(un_path, workspace, name, origin)
for name, origin in versions.items()]
Two design choices carry the safety. The job returns verdicts rather than acting on them, so the
posting step is a separate call that can run under a different identity. And postable is
conservative by construction: an unclassified conflict falls outside AUTO_RESOLVABLE and blocks
the post rather than being waved through.
Production Deployment Pattern
- Schedule the reconcile nightly, outside the post window. Overlapping the two produces intermittent failures that get attributed to the database rather than to the schedule.
- Post from a separate service under a gate. Feed the postable set to a second job running as the post account. That account should have no ability to edit, so it cannot resolve a conflict on its way past one.
- Escalate versions that fail two nights running. A version that cannot reconcile cleanly twice is a person’s decision waiting to happen; leaving it in the nightly loop turns it into background noise.
- Apply bounded retry on lock acquisition only. Retrying a reconcile that failed on a conflict achieves nothing; retrying one that failed on a transient lock is worthwhile.
- Delete the version after a successful post. A posted version left open reconciles forever and slows every subsequent run.
- Persist the verdict per version. Conflicts by class, resolutions applied, validation result, trace outcome, the accounts involved and the timestamps form the record that shows a publication was gated rather than merely successful.
Conclusion
A nightly reconcile with a conservative post gate converts the riskiest routine operation in a versioned estate into something boring. The mechanical work — merging, classifying, validating, proving — runs unattended, and the only human attention required is on the conflicts that genuinely need a decision. Splitting the reconcile and post identities keeps the audit trail meaningful, and deleting versions after posting keeps tomorrow’s run as cheap as today’s. The conflicts that this job holds back are the subject of the next step: deciding them consistently rather than case by case, which is what resolving branch version conflicts sets out.
Related
- Up to the parent topic: Branch Versioning & Conflict Resolution
- Up to the section: Topology & Tracing Workflows
- Resolving Branch Version Conflicts in Utility Networks
- Version vs Snapshot Isolation for Batch Jobs
- Incremental Topology Rebuild After Field Edits
For authoritative reference, consult the ArcGIS Pro branch versioning documentation and the Python logging facility.