State-Based Workflow Troubleshooting
Diagnosing state-based (
workflow_kind='state_based') workflow problems in Enezaframework. Each entry follows symptom → cause → diagnostic → fix. Part of the Workflow series. Entry point: State-Based Workflows.
Table of Contents
- Submit Fails or No Instance Is Created
- A Branch Exists but Nobody Can Move It
- Saved Assignees Are Not Live
- "Move to State" Is Not Visible
- Threshold State Does Not Move
- State Assignment Notification Is Missing
- Condition Node Does Not Route
- Join Never Activates / Branch Appears Stuck
- Node Placement Is Not Remembered / Graph Is a Column
- State Choices Are Missing in the Designer
- Cannot Delete a Template Version
- Diagnostic Cookbook
- See Also
- A Note on Testing
Submit Fails or No Instance Is Created
Symptom. Pressing Submit to Workflow returns an error such as "Document already has an
active workflow", "No active workflow template found", "does not have a 'workflowstate' field",
or "The initial state '...' has no effective assignees". Alternatively the action is absent
entirely and no WorkflowInstance row is created.
Cause. Submission is gated at two levels:
WorkflowMixin.can_submit_to_workflowrefuses submission when the model lacksworkflowstate,workflow_template_nameis unset, no active template resolves, an active instance already exists, orworkflowstateis non-empty (enezaframework/views/mixins.py:1229-1241).submit_workflowre-checks the field, resolves the active template, and rejects a second active instance (services/workflow_submission.py:58-90).start_state_workflowrefuses to start when START is missing/points nowhere or when the initial state has no effective assignee (services/state_workflow.py:212-222).
Diagnostic.
python manage.py workflow_instance_status --workflowname IncidentWorkflow
python manage.py check_workflow_state_choices --workflowname IncidentWorkflow
Check the model has workflowstate and no active instance exists; check a template for the
workflowname is is_active=True; check the first state's assignees resolve to active users.
Fix.
- Add the
workflowstatefield to the model (see the quick start in State-Based Workflows). - Activate the intended template version.
- Close/reject the previous instance, or create a new document if one is still active.
- Assign at least one active user (or a group with active members) to the initial state, then re-submit.
A Branch Exists but Nobody Can Move It
Symptom. The history panel shows an active branch, but nobody sees a Move to State item, or the user sees it and the move returns "You are not allowed to transition state '...'".
Cause. user_can_transition has no permission fallback (services/state_workflow.py:188-201):
a user must be in state_assignees(state). Assignees are the active users in assignee_users plus
the active members of assignee_groups. A state with assignees that resolve to nobody (inactive
users, or groups with no active members) counts as unassigned by has_effective_assignees
(:157-177).
Diagnostic.
python manage.py workflow_instance_status --user <username>
The command prints each branch's resolved assignees and can '<user>' move it: True/False. If the
state is unassigned it prints assignees: NONE. workflowstate history also records an explicit
warning entry when a branch enters a state nobody can move
(_warn_if_no_assignees, services/state_workflow.py:419-448) with metadata
warning='no_effective_assignees'.
Fix.
- Activate the template version that carries the assignees (see the next entry), or
python manage.py assign_state_users --workflowname IncidentWorkflow --username <user>against the active version. - Make the listed users/members active, or replace inactive users, then re-check with
workflow_instance_status --user. - Note the initial state is special: an unassigned initial state blocks submission outright, whereas an unassigned later state leaves the branch stuck (recorded in history).
Saved Assignees Are Not Live
Symptom. Assignees were set in the designer and saved, but no one can move the document; or the change only appears for documents created after a certain point.
Cause. Every designer save clones the template into a new inactive version
(clone_workflow_template, services/workflow_versioning.py:25-38) and the active version stays
active until explicitly activated. Assignees on the inactive version do nothing at runtime. In
addition, an in-flight document is pinned to the version active at submit time, because
WorkflowInstance.template is a PROTECT FK resolved at submit (workflow_submission.py:74-77).
Diagnostic.
python manage.py check_workflow_state_choices --workflowname IncidentWorkflow
python manage.py workflow_instance_status --workflowname IncidentWorkflow
The first shows each version's is_active/is_latest; the second shows each instance's template
version and its branch assignees.
Fix.
- Activate the version you saved the assignees on (this deactivates the other versions of the same
workflowname). - For already-submitted documents, activate the version and create a new document — a pinned instance keeps its original version's graph and assignees and cannot be re-submitted while active.
- To change assignees on the already-pinned active version, use
assign_state_userswith the matching--version(or without--versionto target the active version).
"Move to State" Is Not Visible
Symptom. The Workflow menu shows View History but no move item, or the item is there but the modal does not open.
Cause and diagnostic by sub-case:
| Sub-case | Cause | Check |
|---|---|---|
| No move item at all | The user is not an assignee of any active branch's state, or state_status != 'active'. |
workflow_instance_status --user <user>; owner get_workflow_menu_items requires state_status == 'active' (mixins.py:1350). |
Item shows Move to -> ... instead of a state name |
The branch has more than one allowed transition, so the picker is required. | get_available_transitions(branch.current_state) in the shell. |
| Item click does nothing | The picker modal never opens. | See below. |
| Picker opens but shows "You are not allowed to move this document" | can_transition is false for this user/state. |
user_can_transition(current_state, request.user, document) in StateWorkflowTransitionPickerView (views/workflow_api.py:992). |
| Move returns an error modal | The transition threw a WorkflowTransitionError. |
workflow_task_status; server logs; the error message is surfaced by workflow_actions.js. |
The modal wiring spans three files:
navactions.html:68-74renders an action withmodal: Trueas anhx-gettargeting#statetransitionmodal-bodyand callswindow.enezaStateTransitionModal.open().components/layouts/default.html:166-192defineswindow.enezaStateTransitionModaland opens the modal onhtmx:afterSwapwhen the swapped element is#statetransitionmodal-body.static/eneza_workflow_engine/js/workflow_actions.jsintercepts form submits whose URL contains/transition/, posts them withfetch, then closes the state modal and shows the result.
Fix.
- Assign the user to the branch's state on the active version.
- Ensure the page includes the app layout that defines
#statetransitionmodalandwindow.enezaStateTransitionModal; a bare partial page (no layout) has no modal host. - In the browser console, confirm
window.enezaStateTransitionModalis defined and that the swapped element id is exactlystatetransitionmodal-body. - If a state genuinely has no outgoing transition, that is a template error — fix the graph (rule
S8) and re-activate.
Threshold State Does Not Move
A majority/all state waits until one destination reaches the required approval count.
- Check the state's
Approval Thresholdin the designer and the frozen eligible count on the step instance. Deactivating a user/group member does not lower the denominator. - Check the votes:
WorkflowApproverActionrows for the current step includetarget_state. Votes for different destinations are counted separately, so split votes can stall the state. - Reassign a pending approver or cancel the workflow if the threshold can no longer be reached.
- States with majority/all are rejected at activation if they have automatic outgoing transitions that would bypass voting; use a CONDITION node before the state instead.
State Assignment Notification Is Missing
- Confirm the state has at least one effective assignee. Group members are expanded when the notification is queued.
- If an email is expected, set
state_assignment_template_codeon the state or the workflow-levelstate_assignmenttemplate. The template must be linked to the workflow template and be active with a published version; otherwise the notification is in-app only. - In-flight instances are pinned to their template version. When that version has no assignment template, the runtime falls back to the currently active version's template for the same state code. Instances already sitting in a state do not re-send their assignment notification when a new version is activated; create a new instance or re-enter the state to test the email.
- The originator is notified only when
notify_originator_on_assignmentis enabled and the originator is not already an assignee. - Check the
WorkflowSideEffectledger: notifications are queued after commit and retried there.
Condition Node Does Not Route
- The node must have a
SavedQueryand exactly one true plus one false outgoing transition. - Check
condition_failure_behavior;fail_escalaterecords a recoverable error, whilefail_closed/fail_openroute false/true respectively. - Look for the
PRECONDITIONhistory entry, which records the query result, chosen target, and any evaluation error.
Join Never Activates / Branch Appears Stuck
Symptom. A fork spawned branches, but the join never activates, or the instance shows as
active with no way to progress.
Cause. A join activates only when no active branch remains in its fork_scope
(_arrive_at_join, services/state_workflow.py:535-554). This fails when a sibling branch is still
active (including sitting in a state nobody can move, or waiting at a fork that was never fired), or
when a branch can never reach the join or an END node. The validator's deadlock rules (S13, S14)
catch the static cases before activation; they cannot catch a runtime branch that is merely stuck on
assignees.
Diagnostic.
python manage.py workflow_instance_status --workflowname IncidentWorkflow --limit 50
Look at each branch's fork_scope and status. Sibling branches share a fork_scope; the join
cannot fire while any sibling with the same UUID is still active. The command warns when an
instance is active but has no active branches.
Fix.
- Unblock the stuck sibling: assign an active user to its state and move it (or let it reach END).
- If a branch ended at a terminal instead of the join, that is allowed — the join still fires once the scope has no active branch.
- If the graph itself is deadlocked, re-open the designer, correct the fork/join matching, save and re-activate. Remember a new version only affects new documents.
Node Placement Is Not Remembered / Graph Is a Column
Symptom. The designer shows every node stacked in a vertical column at x=360 instead of the
saved layout, or positions reset after an unrelated edit.
Cause. The saved layout is only trusted while it still matches the persisted graph. The load
view uses design_data only when _design_data_matches_steps returns True
(views/worflow_design.py:480, rule at :281-316); otherwise it rebuilds from the DB with
_statetemplates_to_graph, whose fallback layout is x=360, y=60 + idx*170 (:242-254). The
snapshot is invalidated by a node count change, a connection count change, a step_type change, or
a changed state_code — for example a step edited directly through the admin.
Diagnostic. Inspect WorkflowTemplate.design_data for the loaded version. If nodes /
connections counts differ from the step/transition counts, or the state-code sets differ, the
snapshot is stale. The load view also reports is_latest/is_active, so confirm you loaded the
version you edited (?version=<n>).
Fix.
- Re-save from the designer once the graph is correct; the save writes a fresh
design_datasnapshot. - Avoid editing
StepTemplaterows for a state-based template through the admin — that drifts the graph away from the snapshot. - Note that Save to Database creates a new version: after saving and reloading, make sure you are looking at that new version.
State Choices Are Missing in the Designer
Symptom. The state dropdown is empty, shows the wrong vocabulary, or the designer/validator
reports that the choices must be registered (S7).
Cause. The vocabulary resolves in the order registry → per-template state_choices_path →
auto-discovery (services/state_choices.py:164-178). Problems come from a wrong
MY_PROJECT_WORKFLOWS_CONFIG path, a registry key that does not match the exact workflowname, an
unimportable state_choices_path, or several models declaring workflowstate choices with
different sets (auto-discovery then picks one deterministically and logs a warning,
:89-103).
Diagnostic.
python manage.py check_workflow_state_choices --workflowname IncidentWorkflow
python manage.py check_workflow_state_choices --json
The report prints MY_PROJECT_WORKFLOWS_CONFIG, the effective WORKFLOW_STATE_CHOICES, every
model with a workflowstate field, the discovery result, and per-template registry_path /
explicit_path / choices.
Fix.
- Correct the dotted path in
settings.MY_PROJECT_WORKFLOWS_CONFIG. - Make the registry key exactly equal to
WorkflowTemplate.workflowname. - Fix the class path in
WORKFLOW_STATE_CHOICES(or the per-templatestate_choices_path). - When several documents declare different vocabularies, register the mapping explicitly to disambiguate instead of relying on auto-discovery.
Cannot Delete a Template Version
Symptom. Deleting a template version from the admin or with a raw delete() raises a
ProtectedError, or the admin refuses with "this version has submitted workflow documents".
Cause. Runtime models protect the graph: WorkflowInstance.template, StepTemplate.step_template
and StepTemplate.workflowtemplate use PROTECT, and WorkflowStepInstance.step_template is
PROTECT. The admin delete view explicitly refuses when instances exist
(enezaadminstrator/views/workflowtemplates.py:174-179) and otherwise deletes transitions and steps
before the template (:181-194).
Diagnostic. Count the instances referencing the template:
python manage.py workflow_instance_status --workflowname IncidentWorkflow
python manage.py delete_workflow_template --workflowname IncidentWorkflow
The command prints the version and instance counts and asks for confirmation unless --yes is
passed.
Fix.
- Use
python manage.py delete_workflow_template --workflowname <name> --yes, which deletes instances first (cascading to branches, step instances and transactions), then transitions, steps and the template. - Use
--template-version <n>to remove a single version, or--keep-instancesto fail fast instead of deleting runtime data.
Diagnostic Cookbook
| Goal | Command |
|---|---|
| See instances, branches, assignees and whether a user can move each state | python manage.py workflow_instance_status --workflowname IncidentWorkflow --user admin |
Inspect recent background task results (e.g. apply_document_state) |
python manage.py workflow_task_status --all --limit 50 |
| Inspect state-choice resolution as JSON | python manage.py check_workflow_state_choices --json |
| Add/repair assignees on the active version | python manage.py assign_state_users --workflowname IncidentWorkflow --username admin |
| Build (and optionally activate) a known-good sample | python manage.py create_sample_state_workflow --reset --activate |
| Remove a template and its runtime data | python manage.py delete_workflow_template --workflowname IncidentWorkflow --yes |
Reading the history itself is usually the fastest path: open View History on the document. The
state panel lists the active branches with their effective assignees
(workflow_instance/partials/history_shell.html:34-50), and the transaction list shows each move,
fork spawn, join activation and any no_effective_assignees warning.
See Also
- State-Based Workflows — overview and quick start.
- State-Based Workflow Architecture — engine internals, fork/join semantics and the designer pipeline.
- State-Based Workflow Reference — rules, APIs, field meanings and management commands.
- Workflow — Developer Index
A Note on Testing
Per project rules, the developer or agent does not run or claim test results. The user is solely responsible for all testing and validation of the output described in this guide.