Enforcing Lifecycle Transitions with Attribute Rules
A lifecycle state machine that lives in application code is a convention. It governs the edits that pass through that application and nothing else — not the migration script, not the mobile client, not the analyst with edit rights and a table view. Since every estate eventually acquires all three, the states drift: features appear as retired without ever having been active, abandoned mains regain an active status during a bulk update, and the status field becomes something nobody trusts enough to filter a trace on. Moving the guard into an attribute rule puts it in the geodatabase, where every edit path meets it. This guide covers what the rule should encode, how to deploy one onto a class that already contains violations, and what belongs outside the rule. It implements the machine defined in lifecycle state machines for utility assets.
Environment Prerequisites
- ArcGIS Pro 3.2+ with a Standard or Advanced licence, and a geodatabase that supports attribute rules on the target feature classes.
- The lifecycle domain loaded with exactly the permitted values, because a rule can only compare against values the field can hold.
- The legal transition map as data — the same adjacency the state machine uses — so the rule and the application agree by construction rather than by review.
- A measured violation count per class before deployment, since that number decides the rollout.
- An append-only audit table with insert rights for the rule’s execution context, so accepted transitions are recorded where they cannot be edited.
- A test geodatabase where the rule can be exercised against realistic edits before it reaches anyone’s working version.
Schema-Aware Validation Protocol — Run Before Deploying the Rule
- Count existing violations. Features already holding an impossible state, or pairs whose history implies an illegal transition, will meet the rule on their next edit. That count is the size of the clean-up.
- Confirm the domain matches the transition map. A value in the domain with no transitions defined is a state assets can enter and never leave.
- Check every edit path. Enumerate the applications, scripts and services that write the status field, and confirm each will surface the rule’s rejection message rather than swallowing it.
- Decide the cancelled-proposal case explicitly. A proposed asset that is never built is not retired; the estate needs either a cancelled state or a deletion policy, and the rule will force the decision.
- Test against a bulk edit. Attribute rules run per row, and a rule that is acceptable on one edit can make a ten-thousand-row update unusably slow.
Minimal Reproducible Implementation
The rule itself is a short expression evaluated by the geodatabase on every update. What follows is the deployment and the pre-flight measurement, which are the parts that take the time.
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("lifecycle-rule")
LEGAL = {
"PROPOSED": {"ACTIVE", "CANCELLED"},
"ACTIVE": {"ABANDONED", "RETIRED"},
"ABANDONED": {"RETIRED", "ACTIVE"},
"RETIRED": set(),
"CANCELLED": set(),
}
RULE_EXPRESSION = """
// Refuse any lifecycle transition the state machine does not permit.
var legal = {
'PROPOSED': ['ACTIVE', 'CANCELLED'],
'ACTIVE': ['ABANDONED', 'RETIRED'],
'ABANDONED': ['RETIRED', 'ACTIVE'],
'RETIRED': [],
'CANCELLED': []
};
var was = $originalfeature.LIFECYCLESTATUS;
var now = $feature.LIFECYCLESTATUS;
if (was == now) { return true; }
if (IsEmpty(was)) { return true; } // new feature: any initial state is allowed
var allowed = legal[was];
if (IsEmpty(allowed)) { return {'errorMessage': was + ' is terminal'}; }
if (IndexOf(allowed, now) == -1) {
return {'errorMessage': was + ' cannot transition to ' + now};
}
return true;
"""
@dataclass
class Violation:
global_id: str
from_state: str
to_state: str
@dataclass
class PreflightReport:
violations: list[Violation] = field(default_factory=list)
rows: int = 0
@property
def safe_to_enforce(self) -> bool:
return not self.violations
def preflight(feature_class: str, history_table: str) -> PreflightReport:
"""Count transitions already recorded that the rule would have refused.
The rule only governs future edits, so the question before deployment is how much
of the existing history it contradicts — which is what an editor will meet the next
time they touch one of those features.
"""
report = PreflightReport()
with arcpy.da.SearchCursor(history_table,
["GLOBALID", "PRIOR_STATUS", "NEW_STATUS"]) as cursor:
for global_id, prior, new in cursor:
report.rows += 1
if prior and new and new not in LEGAL.get(prior, set()) and prior != new:
report.violations.append(Violation(str(global_id), str(prior), str(new)))
LOG.info("%d historical transition(s), %d would be refused", report.rows,
len(report.violations))
return report
def deploy_rule(feature_class: str, name: str = "LifecycleTransitionGuard") -> None:
"""Attach the constraint rule to a feature class."""
arcpy.management.AddAttributeRule(
in_table=feature_class,
name=name,
type="CONSTRAINT",
script_expression=RULE_EXPRESSION,
is_editable="EDITABLE",
triggering_events="UPDATE",
error_number=51001,
error_message="Illegal lifecycle transition",
exclude_from_client_evaluation="INCLUDE",
)
LOG.info("attribute rule %s attached to %s", name, feature_class)
exclude_from_client_evaluation="INCLUDE" is the setting that matters. A rule excluded from client
evaluation runs only on the server, which means a disconnected editor discovers the rejection at
synchronisation rather than at the moment of the edit — long after the context that would let them
correct it.
What Belongs Outside the Rule
An attribute rule is the right place for the transition adjacency and the wrong place for everything that needs to look beyond the row being edited.
Cascade logic — refusing to retire a container while it still holds active contents — needs to query other features, which a constraint rule cannot do cheaply or reliably. That check belongs in the retirement workflow, where it already lives.
Authorisation — whether this editor may make this transition — is a permissions question, and encoding it as data in a rule expression produces a permissions model nobody can audit.
Side effects, such as writing an audit row, belong in a calculation rule or in the workflow rather than in the constraint, because a constraint that has side effects is a constraint that runs differently depending on whether the edit succeeds.
Production Deployment Pattern
- Measure, clean, warn, enforce. Never deploy a constraint onto a class with a violation backlog; the first person to meet it will be an editor doing unrelated work.
- Version the rule expression with the transition map. They are the same policy in two languages, and letting them drift reintroduces the problem the rule was meant to solve.
- Test the rejection message. Editors read it, and “Illegal lifecycle transition” is less useful than naming the states involved.
- Benchmark a bulk update. A per-row rule that costs a millisecond costs ten seconds on a ten-thousand-row edit, and that is the edit an operator will make during a migration.
- Keep the workflow checks. The rule is the floor, not the ceiling: cascade and authorisation checks still belong upstream.
- Audit rule rejections. A spike in rejections from one application usually means that application encodes an older version of the policy.
Conclusion
An attribute rule moves the lifecycle guard from a convention that governs one edit path into a constraint that governs all of them. Encoding the adjacency and nothing else keeps it fast and auditable; measuring and clearing the existing violations before enforcement keeps the rollout from landing on editors; and leaving cascade and authorisation upstream keeps each check where it can actually be evaluated. What results is a status field that a trace can filter on without hedging, which is the whole point of modelling lifecycle at all.
Related
- Up to the parent topic: Lifecycle State Machines for Utility Assets
- Up to the section: Asset Lifecycle & Maintenance Automation
- Modeling Asset Retirement Workflows in ArcGIS Pro
- Field Data Capture & Mobile Sync
- Resolving Branch Version Conflicts in Utility Networks
For authoritative reference, consult the ArcGIS Pro attribute rules documentation and the Arcade expression reference.