Restart with useLatestDefinitions=true does not re-seed workflow.variables from the new definition, so ${workflow.variables.*} resolves to null
Describe the bug
Summary
WorkflowExecutor.restart(workflowId, useLatestDefinitions=true) replaces the running instance's WorkflowDef with the latest registered version, but never re-seeds the instance's variables map from that definition. The instance keeps whatever variables it was created with.
If the original definition had no variables block and the new one does, every ${workflow.variables.*} reference in the new definition's task templates resolves to the literal string null at task-execution time.
There is no error at restart time. The workflow restarts successfully (HTTP 204) and only fails later, when a task that depends on a variable executes — typically with a confusing downstream error rather than anything pointing at variable resolution.
Environment
- Observed on a 3.30.2 build.
- Code path is unchanged on
mainas of96dfed1(postv3.32.4/v3.33.0-rc3), so current releases are affected. - Persistence: Postgres (the full
WorkflowModel, includingvariablesandworkflowDefinition, is serialized and read back verbatim, so the inconsistency is visible in the stored record).
Steps to reproduce
1. Register v1 — no variables, hardcoded URI
curl -X POST localhost:8080/api/metadata/workflow \
-H 'Content-Type: application/json' -d '{
"name": "restart_variables_repro",
"version": 1,
"schemaVersion": 2,
"restartable": true,
"ownerEmail": "[email protected]",
"tasks": [{
"name": "call_service",
"taskReferenceName": "call_service",
"type": "HTTP",
"inputParameters": {
"http_request": { "uri": "http://127.0.0.1:9/health", "method": "GET" }
}
}]
}'2. Start it and let it fail (port 9 refuses connections, so the workflow reaches FAILED, which is the terminal state restart requires)
curl -X POST localhost:8080/api/workflow/restart_variables_repro \
-H 'Content-Type: application/json' -d '{}'3. Register v2 — adds variables and references it
curl -X PUT localhost:8080/api/metadata/workflow \
-H 'Content-Type: application/json' -d '[{
"name": "restart_variables_repro",
"version": 2,
"schemaVersion": 2,
"restartable": true,
"ownerEmail": "[email protected]",
"variables": { "serviceBaseUrl": "https://api.example.com" },
"tasks": [{
"name": "call_service",
"taskReferenceName": "call_service",
"type": "HTTP",
"inputParameters": {
"http_request": {
"uri": "${workflow.variables.serviceBaseUrl}/health",
"method": "GET"
}
}
}]
}'4. Restart the v1 instance against the latest definition
curl -X POST "localhost:8080/api/workflow/{workflowId}/restart?useLatestDefinitions=true"5. Inspect the execution
curl -s "localhost:8080/api/workflow/{workflowId}" | jq '{
variables,
defVariables: .workflowDefinition.variables,
uri: .tasks[0].inputData.http_request.uri
}'Actual behaviour
{
"variables": {},
"defVariables": { "serviceBaseUrl": "https://api.example.com" },
"uri": "null/health"
}The stored record is self-contradictory: the embedded workflowDefinition carries the variables, while the instance's own variables is empty. The task fails with something like Target host is not specified, which gives no hint that a workflow variable failed to resolve.
Expected behaviour
Restarting against the latest definition should adopt that definition's variables, so uri resolves to https://api.example.com/health.
Root cause
Variable references are resolved against the instance's variables, not the definition's — ParametersUtils.java#L125:
workflowParams.put("variables", workflow.getVariables());That map is seeded in exactly one place, the fresh-start path — WorkflowExecutorOps.java#L2897, inside createWorkflowModel:
workflow.setVariables(workflowDefinition.getVariables());The restart path swaps the definition and stops there — WorkflowExecutorOps.java#L218:
if (useLatestDefinitions) {
workflowDef = metadataDAO.getLatestWorkflowDef(workflow.getWorkflowName())
.orElseThrow(...);
workflow.setWorkflowDefinition(workflowDef); // <-- no setVariables
workflowDef = metadataMapperService.populateTaskDefinitions(workflowDef);
}setVariables appears exactly once in the whole file, and a repo-wide search finds no other production call site that re-seeds an existing instance's variables.
Only restart(useLatestDefinitions=true) can produce the mismatch, since it is the only path that swaps a live instance's definition. retry and rerun keep the original definition, so their variables stay consistent with it.
One consequence worth noting: because restart reuses the same workflowId and rewrites the stored record, the affected instance becomes permanently stuck. Its persisted definition is now v2 while its variables stays empty, so a subsequent plain restart (useLatestDefinitions=false) reuses v2 and fails identically.
Suggested fix
if (useLatestDefinitions) {
workflowDef =
metadataDAO
.getLatestWorkflowDef(workflow.getWorkflowName())
.orElseThrow(
() ->
new NotFoundException(
"Unable to find latest definition for %s",
workflowId));
workflow.setWorkflowDefinition(workflowDef);
+ workflow.setVariables(workflowDef.getVariables());
workflowDef = metadataMapperService.populateTaskDefinitions(workflowDef);
}Open design question
Should the reset be unconditional, i.e. applied on every restart rather than only when useLatestDefinitions is true?
Restart already clears all task history (workflow.getTasks().clear(), plus resetWorkflow removing the workflow and task rows and dropping the instance from the index), so the run genuinely starts from scratch. Under that reading, any variable state accumulated by SET_VARIABLE tasks in the previous run is stale and should be discarded in favour of the definition's initial values.
As it stands, that stale state survives a restart even when the definition is unchanged — which looks like a smaller, related bug in the same method. Resetting unconditionally would fix both, and it matches fresh-start semantics. I've kept the patch above minimal and scoped to the reported bug; happy to widen it if maintainers prefer.
Source: conductor-oss/conductor