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

  1. Submit Fails or No Instance Is Created
  2. A Branch Exists but Nobody Can Move It
  3. Saved Assignees Are Not Live
  4. "Move to State" Is Not Visible
  5. Threshold State Does Not Move
  6. State Assignment Notification Is Missing
  7. Condition Node Does Not Route
  8. Join Never Activates / Branch Appears Stuck
  9. Node Placement Is Not Remembered / Graph Is a Column
  10. State Choices Are Missing in the Designer
  11. Cannot Delete a Template Version
  12. Diagnostic Cookbook
  13. See Also
  14. 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:

  1. WorkflowMixin.can_submit_to_workflow refuses submission when the model lacks workflowstate, workflow_template_name is unset, no active template resolves, an active instance already exists, or workflowstate is non-empty (enezaframework/views/mixins.py:1229-1241).
  2. submit_workflow re-checks the field, resolves the active template, and rejects a second active instance (services/workflow_submission.py:58-90).
  3. start_state_workflow refuses to start when START is missing/points nowhere or when the initial state has no effective assignee (services/state_workflow.py:212-222).

Diagnostic.

text
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 workflowstate field 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.

text
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.

text
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_users with the matching --version (or without --version to 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-74 renders an action with modal: True as an hx-get targeting #statetransitionmodal-body and calls window.enezaStateTransitionModal.open().
  • components/layouts/default.html:166-192 defines window.enezaStateTransitionModal and opens the modal on htmx:afterSwap when the swapped element is #statetransitionmodal-body.
  • static/eneza_workflow_engine/js/workflow_actions.js intercepts form submits whose URL contains /transition/, posts them with fetch, 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 #statetransitionmodal and window.enezaStateTransitionModal; a bare partial page (no layout) has no modal host.
  • In the browser console, confirm window.enezaStateTransitionModal is defined and that the swapped element id is exactly statetransitionmodal-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 Threshold in the designer and the frozen eligible count on the step instance. Deactivating a user/group member does not lower the denominator.
  • Check the votes: WorkflowApproverAction rows for the current step include target_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_code on the state or the workflow-level state_assignment template. 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_assignment is enabled and the originator is not already an assignee.
  • Check the WorkflowSideEffect ledger: notifications are queued after commit and retried there.

Condition Node Does Not Route

  • The node must have a SavedQuery and exactly one true plus one false outgoing transition.
  • Check condition_failure_behavior; fail_escalate records a recoverable error, while fail_closed/fail_open route false/true respectively.
  • Look for the PRECONDITION history 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.

text
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_data snapshot.
  • Avoid editing StepTemplate rows 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.

text
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-template state_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:

text
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-instances to 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

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.