Skip to content

Workflow activity PersistentId is re-minted on every write (never preserved, never read back) #949

Description

@ako

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

  1. 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.
  2. 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

  1. Establish empirically what persistentId governs (a workflow app with an in-flight instance, edited and redeployed, is the honest test).
  2. 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.
  3. A row in canon.identityFields may be the right home; see ADR-0008 and TestFreshGUIDFieldsHaveAnIdentityDecision.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions