Summary
Every workflow activity write re-mints PersistentId as a fresh GUID, on both engines. Nothing ever reads the stored value back, and the semantic model (sdk/workflows) has no field for it — so it cannot be preserved even in principle.
Found while fixing #944. That fix stops REPLACE ACTIVITY renaming a same-name replacement, but a no-op replace still rewrites the .mpr on every run, because the activity's PersistentId changes each time.
Evidence
mdl/backend/modelsdk/workflow_write.go:
// addFreshPersistentID emits PersistentId as a fresh binary-UUID value (the
// legacy serializer writes a new GUID on every save).
func addFreshPersistentID(g *element.Base) {
addIDRef(g, "PersistentId", model.ID(mmpr.GenerateID()))
}
Called at 10 sites. sdk/mpr/writer_workflow.go does the same at 5 sites (idToBsonBinary(generateUUID())). Neither mdl/backend/modelsdk/workflow_read.go nor the legacy workflow parser reads PersistentId, and grep PersistentID sdk/workflows/workflow.go is empty.
Measured on a v1 fixture, a no-op REPLACE ACTIVITY "TaskB" WITH USER TASK "TaskB" '<same caption>' (with the #944 fix applied, so the name is stable):
| activity |
before |
after |
| Start |
52424976db5e |
52424976db5e |
| TaskA |
21e64da5ad34 |
21e64da5ad34 |
| TaskB (replaced) |
6cdbf22c1b6e |
5050e603455e |
| End |
32b15e3cc138 |
32b15e3cc138 |
Untouched siblings keep theirs — only the rebuilt activity churns. Three consecutive no-op runs each produced a different .mpr sha.
Why it matters
- Idempotence (ADR-0008). A script re-run against an in-sync project is supposed to leave the
.mpr byte-identical. Any workflow write breaks that, so every run shows up as a version-control change in Studio Pro.
- Possible runtime identity.
persistentId is Introduced: 10.21.0 (modelsdk/gen/workflows/version.go) — Mendix added a persistent identity to workflow elements at some point, which suggests it is meant to survive model edits. CLAUDE.md's rule for entities is explicit: "A GUID Is the Database's Identity — Never Mint One for an Existing Element."
This is NOT established. Mendix's workflow versioning docs never mention persistentId, and say activity matching is structural. The open question is whether the Workflow Versioning Conflict Detection uses it — if it does, mxcli re-minting it could turn an in-place edit into "current activity removed from an executing path", which that page does list as a conflict. Determine this before assuming either way; do not repeat #944's mistake of asserting a runtime consequence from the name of a field.
Suggested direction
- Establish empirically what
persistentId governs (a workflow app with an in-flight instance, edited and redeployed, is the honest test).
- If it must be preserved: carry it on
workflows.BaseWorkflowActivity, populate it on read in both engines, and reuse it on rebuild — or handle it at the codec layer the way canon.TransplantIDs carries element $IDs, which already solves the same shape of problem.
- A row in
canon.identityFields may be the right home; see ADR-0008 and TestFreshGUIDFieldsHaveAnIdentityDecision.
Summary
Every workflow activity write re-mints
PersistentIdas a fresh GUID, on both engines. Nothing ever reads the stored value back, and the semantic model (sdk/workflows) has no field for it — so it cannot be preserved even in principle.Found while fixing #944. That fix stops
REPLACE ACTIVITYrenaming a same-name replacement, but a no-op replace still rewrites the.mpron every run, because the activity'sPersistentIdchanges each time.Evidence
mdl/backend/modelsdk/workflow_write.go:Called at 10 sites.
sdk/mpr/writer_workflow.godoes the same at 5 sites (idToBsonBinary(generateUUID())). Neithermdl/backend/modelsdk/workflow_read.gonor the legacy workflow parser readsPersistentId, andgrep PersistentID sdk/workflows/workflow.gois empty.Measured on a v1 fixture, a no-op
REPLACE ACTIVITY "TaskB" WITH USER TASK "TaskB" '<same caption>'(with the #944 fix applied, so the name is stable):52424976db5e52424976db5e21e64da5ad3421e64da5ad346cdbf22c1b6e5050e603455e32b15e3cc13832b15e3cc138Untouched siblings keep theirs — only the rebuilt activity churns. Three consecutive no-op runs each produced a different
.mprsha.Why it matters
.mprbyte-identical. Any workflow write breaks that, so every run shows up as a version-control change in Studio Pro.persistentIdisIntroduced: 10.21.0(modelsdk/gen/workflows/version.go) — Mendix added a persistent identity to workflow elements at some point, which suggests it is meant to survive model edits. CLAUDE.md's rule for entities is explicit: "AGUIDIs the Database's Identity — Never Mint One for an Existing Element."This is NOT established. Mendix's workflow versioning docs never mention
persistentId, and say activity matching is structural. The open question is whether the Workflow Versioning Conflict Detection uses it — if it does, mxcli re-minting it could turn an in-place edit into "current activity removed from an executing path", which that page does list as a conflict. Determine this before assuming either way; do not repeat #944's mistake of asserting a runtime consequence from the name of a field.Suggested direction
persistentIdgoverns (a workflow app with an in-flight instance, edited and redeployed, is the honest test).workflows.BaseWorkflowActivity, populate it on read in both engines, and reuse it on rebuild — or handle it at the codec layer the waycanon.TransplantIDscarries element$IDs, which already solves the same shape of problem.canon.identityFieldsmay be the right home; see ADR-0008 andTestFreshGUIDFieldsHaveAnIdentityDecision.