State-Based Workflow Reference
API, schema and command reference for the state-based (
workflow_kind='state_based') workflow engine. Part of the Workflow series. Entry point: State-Based Workflows.
Table of Contents
- Choices
- Model Field Reference
- Service Reference
- Validation Rules S1–S20
design_dataJSON Schema- URLs and Views
WorkflowMixinReference- History Context
- Management Commands
- Settings and Migrations
- Error Types
- See Also
- A Note on Testing
Choices
All defined in enezaworkflowengine/models/choices.py.
WorkflowExecutionType — choices.py:5
| Value | Label | Meaning in a state-based graph |
|---|---|---|
start |
Start | Exactly one entry node (S2). |
approval |
Approval | Approval-engine only; invalid in a state graph (S1). |
condition |
condition | Approval-engine only; invalid in a state graph (S1). |
end |
End | Terminal sink; at least one required (S3). |
state |
State | A user-moveable state carrying a state_code and assignees. |
fork |
Fork | Splits a branch into parallel children. |
join |
Join | Waits for a fork scope and auto-continues. |
WorkflowKind — choices.py:14
| Value | Label | Engine |
|---|---|---|
approval |
Approval | workflow_template_validator + step_dispatcher (default). |
state_based |
State Based | state_template_validator + services/state_workflow.py. |
StateLifecycle — choices.py:18
Used by both WorkflowInstance.state_status and WorkflowBranch.status.
| Value | Label |
|---|---|
active |
Active |
completed |
Completed |
cancelled |
Cancelled |
WorkflowActionType — choices.py:23
creation, assignment, approval, precondition, notification, status_change,
resubmission, completion. State-based history uses creation, assignment, status_change,
completion and notification.
Backward-compatibility alias:
WorflowActionType = WorkflowActionType(choices.py:34).
Model Field Reference
WorkflowTemplate — models/workflowtemplate.py:15
| Field | Type | Default | Verbose name | Notes |
|---|---|---|---|---|
name |
CharField(255) |
— | Workflow Template Name | |
description |
TextField |
— | Workflow Template Description | blank=True. |
workflowname |
CharField(100), get_getworkflow_names_choices().choices |
— | Workflow Name | Registry key for state choices. |
workflow_kind |
CharField(20), WorkflowKind.choices |
approval |
Workflow Kind | Selects engine + validator. |
state_choices_path |
CharField(255) |
'' |
State Choices Path | blank=True; per-template override. |
is_active |
BooleanField |
False |
Is Active | Only one active version per workflowname. |
workflowmodule |
CharField(100), Modules.choices |
GL (Modules.GENERAL_LEDGER) |
Workflow Module | |
design_data |
JSONField |
{} |
Designer Data | Layout snapshot. |
version |
PositiveIntegerField |
1 |
Version | Unique with workflowname. |
is_latest |
BooleanField, indexed |
True |
Is Latest Version | Only one latest per workflowname. |
cloned_from |
self-FK, SET_NULL |
None |
Cloned From | related_name='branches'. |
StepTemplate — workflowtemplate.py:83
| Field | Type | Default | Verbose name | Notes |
|---|---|---|---|---|
workflowtemplate |
FK, PROTECT |
— | Workflow Template | related_name='steps'. |
order |
PositiveIntegerField |
— | Order | Model ordering = ['order']. |
name |
CharField(255) |
— | Step Name | |
step_type |
CharField(50), WorkflowExecutionType.choices |
None |
Step Type | null=True, blank=True. |
condition_expression |
FK SavedQuery, PROTECT |
None |
Condition Expression | Approval/condition nodes; state transition guards live on StateTransition. |
if_true / if_false |
self-FK, PROTECT |
None |
If True/False Go To | Approval-engine only. |
next_step |
self-FK, PROTECT |
None |
Next Step | Approval-engine only. |
approval_threshold |
CharField(50), ApprovalThreshold.choices |
None |
Approval Threshold | Approval steps and state stages; blank state value means single. |
state_assignment_template_code |
CharField(100) |
'' |
State Assignment Email Template Code | Per-state Email Studio override. |
notify_originator_on_assignment |
BooleanField |
False |
Notify Originator on State Assignment | Adds the initiator as a state assignment recipient. |
orig_notification_subject |
CharField(200) |
None |
Originator Notification Subject | |
originator_notification_message |
TextField |
None |
Originator Notification Message | |
app_notification_subject |
CharField(200) |
None |
Approver Notification Subject | |
approver_notification_message |
TextField |
None |
Approver Notification Message | |
notification_recipient |
CharField(50), NotificationRecipient.choices |
None |
Notification Recipient | |
condition_failure_behavior |
CharField(50), ConditionFailureBehavior.choices |
fail_closed |
Condition Failure Behavior | |
state_code |
CharField(100) |
'' |
State Code | blank=True; state nodes only. |
matched_join |
self-FK, SET_NULL |
None |
Matched Join | related_name='matched_fork'. |
assignee_users |
M2M User |
— | Assignee Users | blank=True. |
assignee_groups |
M2M Group |
— | Assignee Groups | blank=True. |
StateTransition — workflowtemplate.py:127
| Field | Type | Default | Verbose name | Notes |
|---|---|---|---|---|
workflowtemplate |
FK, CASCADE |
— | Workflow Template | related_name='state_transitions'. |
source_state |
FK StepTemplate, CASCADE |
— | Source State | related_name='outgoing_transitions'. |
target_state |
FK StepTemplate, CASCADE |
— | Target State | related_name='incoming_transitions'. |
label |
CharField(150) |
'' |
Transition Label | blank=True; overrides the display name. |
order |
PositiveIntegerField |
0 |
Order | ordering = ['order', 'id']. |
condition_expression |
FK SavedQuery, PROTECT |
None |
Transition Condition | State guard or automatic route. |
condition_mode |
CharField(20), TransitionConditionMode.choices |
manual |
Condition Mode | |
condition_result |
CharField(10), ConditionResult.choices |
'' |
Condition Result | true/false on condition-node edges. |
comment_required |
BooleanField |
False |
Comment Required |
Unique together: (workflowtemplate, source_state, target_state).
WorkflowInstance — workflowtemplate.py:165
| Field | Type | Default | Verbose name | Notes |
|---|---|---|---|---|
template |
FK, PROTECT |
— | Workflow Template | related_name='instances'. |
content_type |
FK ContentType, PROTECT |
— | Content Type | With object_id forms document. |
object_id |
TextField |
None |
— | Stored as str(pk). |
initiated_by |
FK User, PROTECT |
None |
Initiated By | Notification recipient. |
current_step |
FK StepTemplate, PROTECT |
None |
Current Step | Primary active state. |
current_step_order |
PositiveIntegerField |
1 |
Current Step Order | Approval engine. |
status |
CharField(30), ApprovalStatus.choices |
draft |
Status | |
state_status |
CharField(20), StateLifecycle.choices |
None |
State Status | Authoritative for state-based. |
is_active / is_completed |
BooleanField |
True / False |
Is Active / Is Completed | |
submitted_at / completed_at |
DateTimeField |
None |
Submitted At / Completed At | |
context |
JSONField |
{} |
Context | entity_id, document_id, amounts. |
WorkflowBranch — models/workflowinstance.py:70
| Field | Type | Default | Verbose name | Notes |
|---|---|---|---|---|
workflow_instance |
FK, CASCADE |
— | Workflow Instance | related_name='branches'. |
current_state |
FK StepTemplate, PROTECT |
None |
Current State | related_name='branches_here'. |
parent_branch |
self-FK, SET_NULL |
None |
Parent Branch | related_name='child_branches'. |
fork_scope |
UUIDField, indexed |
None |
Fork Scope | Shared by sibling branches. |
fork_node |
FK StepTemplate, SET_NULL |
None |
Fork Node | related_name='spawned_branches'. |
join_node |
FK StepTemplate, SET_NULL |
None |
Join Node | related_name='consumed_branches'. |
status |
CharField(20), StateLifecycle.choices |
active |
Branch Status | |
started_at / completed_at |
DateTimeField |
— / None |
Started At / Completed At | ordering = ['started_at', 'id']. |
WorkflowStepInstance.branch — workflowinstance.py:43
| Field | Type | Default | Verbose name |
|---|---|---|---|
branch |
FK WorkflowBranch, SET_NULL |
None |
Branch |
related_name='step_instances'; audit link from the executed step back to its token.
WorkflowApproverAction — workflowinstance.py:141
| Field | Type | Default | Notes |
|---|---|---|---|
step_instance |
FK, CASCADE |
— | related_name='approver_actions'. |
target_state |
FK StepTemplate, SET_NULL |
None |
State-threshold vote target; null for approval steps. |
approver |
FK User, PROTECT |
— | related_name='workflow_approvals'. |
action |
CharField(30) |
pending |
pending/approved/rejected/changes_requested/resubmitted. |
comment |
TextField |
'' |
|
acted_at |
DateTimeField |
None |
|
is_available |
BooleanField |
True |
Set False for non-acting assignees. |
Unique constraint unique_approver_per_step_instance on (step_instance, approver).
WorkflowTransaction — workflowinstance.py:197
| Field | Type | Default | Notes |
|---|---|---|---|
workflow_instance |
FK, CASCADE |
— | related_name='transactions'. |
step_instance |
FK, SET_NULL |
None |
|
action_type |
CharField(30), WorflowActionType.choices |
— | |
actor |
FK User, SET_NULL |
None |
Null when the actor has no PK. |
step_name |
CharField(255) |
'' |
|
message |
TextField |
— | Human-readable history line. |
metadata |
JSONField |
{} |
See the architecture guide's metadata list. |
Service Reference
enezaworkflowengine/services/state_workflow.py
| Function | Signature | Behaviour |
|---|---|---|
get_start_node |
(template) |
First step_type='start' step or None. |
get_end_node |
(template) |
First step_type='end' step ordered by order, id. |
get_initial_node |
(template) |
Target of START's first outgoing StateTransition, or None. |
get_initial_state |
(template) |
Backward-compatible alias for get_initial_node. |
get_available_transitions |
(state_template) |
Ordered list of outgoing StateTransition rows. |
state_approval_threshold |
(step_template) |
State threshold, defaulting blank to single. |
state_required_approvals |
(step_instance) |
Required votes from the frozen eligible count. |
approved_votes_for_target |
(step_instance, target_state) |
Approved vote count for one destination. |
transition_display_name |
(transition) |
User-facing name; resolves through forks/joins up to depth 3. |
get_active_branches |
(workflow_instance) |
Active branches ordered by started_at, id. |
get_branches_for_user |
(workflow_instance, user, document=None) |
Active branches whose current node is a state and where user_can_transition is true. |
state_assignees |
(state_template) |
set of active users from assignee_users plus active members of assignee_groups; empty set for None. |
has_effective_assignees |
(state_template) |
bool(state_assignees(...)). |
assignee_label |
(state_template) |
Sorted display names, or 'nobody'. |
user_can_transition |
(state_template, user, document=None) |
False for anonymous, empty assignees, or a non-assignee. No permission fallback. |
start_state_workflow |
(workflow_instance) |
Creates the initial branch; raises StateWorkflowError if START is missing or the initial state has no effective assignees. Returns the WorkflowBranch. |
transition_branch |
(workflow_instance, branch, target_state, user, comment='') |
Atomic move with locks and validation. Records a per-target state vote; moves only when the threshold is met. Raises WorkflowAlreadyCompletedError / StateWorkflowError. Returns the branch. |
cancel_state_workflow |
(workflow_instance, user=None, reason='') |
Cancels all active branches and the instance, clears the document state. No-op if already not active. Returns the instance. Not exposed in the UI. |
sync_document_state |
(workflow_instance, final_code=None) |
Resolves the document state code, queues the write, updates current_step, returns the code or ''. |
transition_display_name rules (:94-127): a state is named by its label or node name; a
transition into a fork is named after the states the fork activates; a transition into a condition
is named after its true/false destinations; a transition into a join is named after the node the
join continues to; structural names are never shown. Depth is capped at 3, and duplicate names are
collapsed.
enezaworkflowengine/services/state_choices.py — State Choices Resolution
| Function | Signature | Behaviour |
|---|---|---|
resolve_state_choices |
(state_choices_path) |
import_string → [(value, label)]; [] on missing path/bad import/no choices. |
state_choice_values |
(state_choices_path) |
Set of values from resolve_state_choices. |
discover_document_state_choices |
() |
Scans installed models for a workflowstate field with choices; returns the shared set, or a deterministic single set (sorted label) when several differ, logging a warning. |
configured_registry |
() |
Reads WorkflowTypeConfig.WORKFLOW_STATE_CHOICES (or a plain dict) via settings.MY_PROJECT_WORKFLOWS_CONFIG. {} when unset/unresolvable. |
configured_state_choices_path |
(workflowname) |
Registry entry for the name, else ''. |
choices_for_workflowname |
(workflowname) |
Registry path; if empty and the registry has exactly one entry, that entry is used as a fallback. |
explicit_state_choices_path |
(template) |
Registry path for template.workflowname, else template.state_choices_path, else ''. |
template_state_choices |
(template) |
Precedence: registry → state_choices_path → auto-discovery. |
Precedence chain, summarised:
flowchart LR
A["WORKFLOW_STATE_CHOICES<br/>registry entry for workflowname"] -->|found| R["(value, label) list"]
A -->|not found| B["WorkflowTemplate.state_choices_path"]
B -->|found| R
B -->|not found| C["discover_document_state_choices()<br/>any workflowstate field choices"]
C --> ROnly explicit_state_choices_path is used by the validator for the S7 check
(state_template_validator.py:238-244); the designer uses template_state_choices.
enezaworkflowengine/services/workflow_submission.py
| Function | Signature | Behaviour |
|---|---|---|
submit_workflow |
(document, user, template=None, workflow_name=None) |
Atomic. Rejects an existing active instance; resolves the active template by workflow_name; requires workflowstate for state-based; creates the instance and starts the state engine. Raises WorkflowSubmissionError. |
enezaworkflowengine/services/workflow_versioning.py
| Function | Signature | Behaviour |
|---|---|---|
clone_workflow_template |
(source_template) |
Atomic. Creates version = max + 1, is_active=False, is_latest=True; copies kind, state_choices_path, design_data, all steps (with state_code and assignees), remapped FKs and all StateTransition rows. Returns the new template. |
enezaworkflowengine/services/workflow_template_validator.py
| Function | Signature | Behaviour |
|---|---|---|
validate_workflow_template |
(template) |
Approval rules R1–R21. Returns list[str] of errors. |
validate_template |
(template) |
Dispatches to validate_state_workflow_template when workflow_kind == 'state_based', else validate_workflow_template. |
Validation Rules S1–S20
Source: enezaworkflowengine/services/state_template_validator.py. validate_state_workflow_template
returns a list[str]; activation runs it through validate_template and refuses to activate when
the list is non-empty.
| Rule | Line range | Requirement | Message prefix |
|---|---|---|---|
S1 |
175-190 |
Every node's step_type is one of start/state/condition/fork/join/end, and every transition connects nodes of the same template. |
S1: Node ... / S1: Transition ... |
S2 |
192-204 |
Exactly one START node; it has no incoming transitions, no state code and no assignees. | S2: ... / S6: / S9: for the code/assignee checks |
S3 |
206-215 |
At least one END node; END nodes have no outgoing transitions, no state code and no assignees. | S3: ... |
S4 |
217-231 |
START has exactly one outgoing transition and it targets a state or condition. | S4: START node ... |
S5 |
233-235 |
The template has at least one state (or, when there are no nodes at all, :162-163 returns S5). |
S5: ... |
S6 |
237-266 |
Every state has a non-empty state_code, unique per template (compared case-insensitively); conditions, forks, joins, START and END have none. |
S6: State ... / S6: Node ... |
S7 |
238-259 |
When the template declares state choices (via explicit_state_choices_path), every state code belongs to them; an unresolvable path is itself an error. |
S7: ... |
S8 |
268-271 |
Every non-END state/join has at least one outgoing transition. | S8: Node ... |
S9 |
299-322 |
A fork has ≥ 2 outgoing transitions targeting states, conditions or forks, no assignees, and a matched_join pointing at a join of the template. |
S9: Fork ... |
S10 |
340-359 |
A join has ≥ 2 incoming transitions, is matched by a fork, has exactly one outgoing transition, and does not continue into another join. | S10: Join ... |
S11 |
324-338, 361-369 |
Matching is bijective: a join is matched by at most one fork and every fork's join is a join of this template. | S11: ... |
S12 |
371-443 |
Fork scopes are properly nested or disjoint; no edge crosses scopes except into the matching join or an END; entering a scope is only allowed via the fork node itself. | S12: ... |
S13 |
445-457 |
Every branch leaving a fork can reach its matching join or an END node. | S13: Branch from fork ... |
S14 |
458-462 |
The matching join is reachable from at least one branch of its fork. | S14: Join ... |
S15 |
464-470 |
Every node is reachable from START; no orphan fork/join. | S15: ... |
S16 |
273-297 |
Every state has at least one effective assignee (active user, or group with active members); conditions, forks and joins have none. A state with only inactive users or empty groups is an error. | S16: ... |
S17 |
453-483 |
Transition guards remain state-only; condition-node edges are unconditional and carry a true/false result. | S17: ... |
S18 |
282-322 |
A condition node has a query, no assignees, no state code, and exactly one true plus one false outgoing transition. | S18: ... |
S19 |
324-339 |
condition_result is set on condition edges and blank on every other edge; condition targets are state/condition/fork/join/end. |
S19: ... |
S20 |
422-451 |
A majority/all state has at least one manual transition and no automatic transition that would bypass voting. | S20: ... |
Notes:
S6also tightensS2/S3: a START or END node carrying a state code is reported underS6.S9also covers START/END assignees: any of them having assignees is reported underS9.S16is evaluated withstate_assignees(), so "listed but inactive" does not satisfy it.- Rule numbers in the reference docstring (
:13-49) are authoritative; a few checks are physically grouped by node kind rather than by rule number.
design_data JSON Schema
WorkflowTemplate.design_data stores the last designer snapshot. The save view writes exactly what
the designer sent; the load view enriches it before returning it.
Stored shape
{
"nodes": [
{
"id": "step-1",
"type": "state",
"x": 360,
"y": 60,
"name": "Reported",
"zIndex": 1,
"stateCode": "reported",
"approvalThreshold": "majority",
"stateAssignmentTemplateCode": "STATE_ASSIGNED",
"notifyOriginatorOnAssignment": true,
"assigneeUserIds": [1, 2],
"assigneeGroupIds": [3]
}
],
"connections": [
{
"id": "conn-1",
"source": "step-1",
"target": "step-2",
"type": "transition",
"label": "Triage"
},
{
"id": "conn-2",
"source": "condition-1",
"target": "step-3",
"type": "true",
"conditionResult": "true"
}
]
}
nodes[] keys
| Key | Type | Present on | Meaning |
|---|---|---|---|
id |
string | all | Designer id, step-<n> when rebuilt from the DB. |
type |
string | all | One of state, condition, fork, join, start, end. |
x, y |
number | all | Canvas position. Fallback layout uses x=360, y=60 + idx*170. |
name |
string | all | Node display name. |
zIndex |
number | all | Stacking order, idx + 1. |
stateCode |
string | state |
The state code; empty string when unset. |
approvalThreshold |
string | state |
single, majority, or all; blank loads as single. |
stateAssignmentTemplateCode |
string | state |
Per-state assignment Email Studio code. |
notifyOriginatorOnAssignment |
bool | state |
Also notify the workflow originator. |
conditionExpressionId |
string | condition |
SavedQuery PK. |
assigneeUserIds |
int[] | state, join |
User PKs (returns [] when none). |
assigneeGroupIds |
int[] | state, join |
Group PKs. |
connections[] keys
| Key | Type | Meaning |
|---|---|---|
id |
string | conn-<n>. |
source |
string | Source node id. |
target |
string | Target node id. |
type |
string | transition, or true/false for edges leaving a condition node. |
label |
string | Transition label; truncated to 150 chars on save. |
conditionExpressionId |
string | Optional state-transition guard. |
conditionMode |
string | manual or automatic for state transitions. |
conditionResult |
string | true/false for condition-node edges. |
commentRequired |
bool | Transition comment requirement. |
Loader enrichment
WorkflowDesignerLoadView (views/worflow_design.py:484-492) adds:
| Key | Source |
|---|---|
workflow_kind |
WorkflowTemplate.workflow_kind |
state_choices |
template_state_choices(template) |
state_choices_path |
explicit_state_choices_path(template) |
version |
WorkflowTemplate.version |
is_latest |
WorkflowTemplate.is_latest |
is_active |
WorkflowTemplate.is_active |
template_id |
str(WorkflowTemplate.id) |
cloned_from_version |
cloned_from.version, only when cloned_from is set |
The stored snapshot is bypassed when _design_data_matches_steps finds it stale
(worflow_design.py:281-316); see the architecture guide's
Designer Load/Save Pipeline.
URLs and Views
Registered in enezaworkflowengine/urls.py (namespaced enezaworkflowengine).
| Name | Path | View | Methods |
|---|---|---|---|
workflow_state_transition_picker |
workflow/<uuid:instance_id>/transition/<uuid:branch_id>/ (urls.py:79) |
StateWorkflowTransitionPickerView (views/workflow_api.py:973) |
GET |
workflow_state_transition |
workflow/<uuid:instance_id>/transition/<uuid:branch_id>/move/ (urls.py:80) |
StateWorkflowTransitionView (views/workflow_api.py:1021) |
POST |
Also relevant: workflow_history_ui (workflow/<uuid:instance_id>/history/ui/), used by the
View History menu item.
workflow_state_transition_picker (GET)
- Unauthenticated → JSON
{'error': 'Authentication required'}, status401. - Unknown instance/branch →
404(get_object_or_404). - Optional query parameter
list_urlis passed through to the template. - Renders
workflow_instance/partials/state_transition_form.htmlwith: workflow_instance,branch,current_statetransitions(rawStateTransitionrows)transition_options—[{'id': target.id, 'name': transition_display_name(t), 'is_end': <bool>}]list_url,can_transition(user_can_transition(current_state, request.user, document))- The form posts
target_state,commentandlist_urltoworkflow_state_transition.
workflow_state_transition (POST)
Request fields: target_state (UUID, required), comment (optional), list_url (optional). The
view is csrf_exempt at the dispatch level; the picker's form still includes {% csrf_token %}.
Success responses by request shape:
| Condition | Response |
|---|---|
X-Requested-With: XMLHttpRequest |
JsonResponse({'success': True, 'workflow_instance_id', 'status', 'message', 'list_url'}) |
HX-Request header |
Empty 200 with HX-Reswap: none and HX-Trigger = {"workflowResult": {"title": "Success", "message", "variant": "success", "list_url"}} |
| Otherwise | eneza_workflow_engine/workflow_result.html with message, list_url, title, variant |
Error handling:
- Missing/invalid
target_state→ JSON400. WorkflowTransitionErrorsubclass → statuse.http_status; JSON{'error': str(e)}or an HTMXHX-Triggerpayload withvariant: "error"whenHX-Requestis set.- Any other exception → JSON
{'error': 'An unexpected error occurred'}, status500(logged with traceback).
WorkflowMixin Reference
Source: enezaframework/views/mixins.py.
| Member | Line | Behaviour |
|---|---|---|
workflow_template_name |
:979 |
Class attribute the child view must set. |
has_workflow_state_field() |
:1087-1103 |
True when self.model has a workflowstate field. |
get_workflow_kind() |
:1105-1115 |
workflow_kind of the active template, or None. |
get_document_workflow_state() |
:1117-1126 |
Raw object.workflowstate or ''. |
get_transitionable_branches() |
:1128-1148 |
get_branches_for_user(instance, request.user, self.object); [] when anonymous or no active instance. |
can_submit_to_workflow() |
:1221-1266 |
State-based gate at :1229-1241. |
get_workflow_menu_items() |
:1268-1381 |
State branch at :1342-1381. |
State-based can_submit_to_workflow gate — :1229-1241
Returns (False, reason) in order when: the model lacks workflowstate; workflow_template_name
is unset; no active template resolves; an active instance already exists; workflowstate is
non-empty. Returns (True, '') otherwise. Note that the approval-engine draft-status requirement
does not apply.
State-based get_workflow_menu_items — :1342-1381
- Always appends View History →
workflow_history_ui. - When
workflow_instance.state_status == 'active', iteratesget_transitionable_branches()and appends one item per branch: -name:Move to -> <transition_display_name(t)>when the state has exactly one allowed transition, otherwiseMove to -> .... -url:workflow_state_transition_pickerwith(instance.id, branch.id). -method:GET,modal:True,hx_vals:{"list_url": "<app_name>:<list_view_name>"}. - Returns after the state branch; the approval action items are not produced for state-based documents.
History Context
WorkflowHistoryMainView (views/workflow_api.py:665-705) adds to the template context:
| Key | Type | Value |
|---|---|---|
is_state_based |
bool | template.workflow_kind == WorkflowKind.STATE_BASED |
active_branches |
list | get_active_branches(instance); each branch is annotated with effective_assignees (sorted active users) and assigned_groups. |
state_summary |
str | Sorted, comma-joined unique names of the active branches' current states. |
state_status_display |
str | instance.get_state_status_display() or ''. |
history_shell.html renders state_summary (falling back to state_status_display, then
status_display) and, when is_state_based and active_branches, lists every active branch with its
effective_assignees (workflow_instance/partials/history_shell.html:10-50).
Management Commands
All under enezaworkflowengine/management/commands/.
create_sample_state_workflow
Builds a ready-to-edit state-based template with a Fork/Join (START → reported → triage → assessment → fork → investigation/containment → join → notification → remediation → validation → closed/cancelled → END).
| Argument | Default | Meaning |
|---|---|---|
--workflowname |
IncidentWorkflow |
Workflow name for the sample. |
--reset |
off | Delete all existing versions of the name (globally unique) first. |
--entity |
first Entity |
Entity by id, slug, name or company code. |
--activate |
off | Validate and activate the created template. |
--assignee |
first active superuser | Repeatable username assigned to every state. |
python manage.py create_sample_state_workflow
python manage.py create_sample_state_workflow --workflowname IncidentWorkflow --reset
python manage.py create_sample_state_workflow --entity ACME --activate --assignee admin
assign_state_users
Adds (or replaces) users/groups on every state of a template. The quickest way to make a workflow
testable.
| Argument | Default | Meaning |
|---|---|---|
--workflowname |
required | Target workflow name. |
--username |
— | Repeatable username. |
--group |
— | Repeatable group name. |
--version |
active, else latest | Target one template version. |
--replace |
off | Replace assignees instead of adding. |
python manage.py assign_state_users --workflowname IncidentWorkflow --username admin
python manage.py assign_state_users --workflowname IncidentWorkflow --group Managers --replace
check_workflow_state_choices
Reports how the designer resolves state choices: the settings path, the registry, models with a
workflowstate field, the discovery result, and per-template registry_path / explicit_path /
choices.
| Argument | Default | Meaning |
|---|---|---|
--workflowname |
all | Limit the report to one name. |
--json |
off | Emit the report as JSON. |
python manage.py check_workflow_state_choices --workflowname IncidentWorkflow
python manage.py check_workflow_state_choices --json
workflow_instance_status
Prints instances with their kind, state_status, document workflowstate, every branch (id, status,
state, fork_scope), the resolved assignees and — with --user — whether that user can move each
state. Warns when an active instance has no active branches.
| Argument | Default | Meaning |
|---|---|---|
--workflowname |
all | Filter by template.workflowname. |
--user |
— | Username to test user_can_transition against. |
--limit |
20 |
Maximum instances. |
python manage.py workflow_instance_status --workflowname IncidentWorkflow --user admin
workflow_task_status
Shows recent django_tasks DBTaskResult rows (workflow tasks only unless --all), including
status, queue, args and the last traceback line for failures, plus a status count.
| Argument | Default | Meaning |
|---|---|---|
--limit |
25 |
Maximum rows. |
--all |
off | Include every module, not just workflow. |
python manage.py workflow_task_status --all --limit 50
delete_workflow_template
Deletes a template (all versions by default) and everything attached to it in the correct order:
instances (cascading to branches/steps/transactions) → transitions → steps → template. PROTECT
FKs make a plain delete fail when runtime data exists.
| Argument | Default | Meaning |
|---|---|---|
--workflowname |
required | Target workflow name. |
--template-version |
all | Delete only this version. |
--keep-instances |
off | Do not delete instances; fails if any reference the template. |
--yes |
off | Skip the interactive confirmation. |
python manage.py delete_workflow_template --workflowname IncidentWorkflow --yes
Settings and Migrations
Settings
| Setting | Default | Effect |
|---|---|---|
WORKFLOW_STATE_ASYNC |
unset (False) |
False: apply_document_state runs inline. True: enqueued to the database task backend on commit; requires python manage.py db_worker. Read in tasks/workflow_tasks.py:728. |
MY_PROJECT_WORKFLOWS_CONFIG |
framework.workflowconfig.WorkflowTypeConfig (framework/settings.py:32) |
Dotted path to the project config class holding WORKFLOW_STATE_CHOICES (and the type registry). |
MY_PROJECT_WORKFLOWS_NAME |
framework.config.workflownames (framework/settings.py:31) |
Dotted path to the workflowname choices class. |
Migrations
| Migration | Adds |
|---|---|
enezaworkflowengine/migrations/0004_state_based_workflow.py |
State workflow models/fields (kind, branches, state fields). |
enezaworkflowengine/migrations/0005_state_choices_and_start_end.py |
state_choices_path and the START/END additions. |
frameworkapp/migrations/0012_incidentreport.py |
The IncidentReport test document. |
frameworkapp/migrations/0013_incidentreport_workflowstate_choices.py |
IncidentReport.workflowstate choices. |
Error Types
| Class | Module | code |
http_status |
Raised when |
|---|---|---|---|---|
WorkflowTransitionError |
services/workflow_errors.py:1 |
TRANSITION_ERROR |
500 |
Base class; __init__(message, step_instance_id=None, event=None, original=None). |
StateWorkflowError |
services/state_workflow.py:40 |
STATE_WORKFLOW_ERROR |
400 |
Invalid branch state, disallowed edge, non-assignee, bad target type, missing START/initial assignees. |
WorkflowAlreadyCompletedError |
services/workflow_errors.py:57 |
WORKFLOW_COMPLETED |
409 |
Transition attempted on a non-active instance. |
WorkflowSubmissionError |
services/workflow_submission.py:33 |
— (plain Exception) |
— | Already-active instance, no active template, missing workflowstate/Approvalstatus. |
Approval-engine siblings (StepHandlerNotFoundError, ConditionEvaluationError,
ApproverAssignmentError, StepLockedError, WorkflowNotActiveError) share the same base class
and are not raised by the state engine.
See Also
- State-Based Workflows — overview and quick start.
- State-Based Workflow Architecture — components, lifecycle and internals.
- State-Based Workflow Troubleshooting — symptom → cause → diagnostic → fix.
- 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.