Workflow Hooks Reference
Developer guide for the Enezaframework workflow engine. Part of the Workflow series.
Table of Contents
- Event Reference
- WorkflowHookContext Reference
- Filter Semantics
- Ordering and De-duplication
- Settings
- Failure History Rows
- check_workflow_hooks
- WorkflowHookRegistry API
- Troubleshooting
- Related Guides
- A Note on Testing
Event Reference
WorkflowHookEvent (enezaworkflowengine/models/choices.py) lists the valid event keys.
| Event | Kind | Emitted from | Context highlights |
|---|---|---|---|
submitted |
both | services/workflow_submission.py submit_workflow |
actor_id, to_state (initial state for state-based), step_name |
step_entered |
approval | services/approval_step.py enter_approval_step |
step_name, metadata.approver_ids |
approved |
approval | services/step_handlers.py ApprovalStepHandler._on_approved |
actor_id, step_name |
rejected |
approval | services/step_handlers.py ApprovalStepHandler._on_rejected |
actor_id, step_name, comment |
changes_requested |
approval | services/step_handlers.py ApprovalStepHandler._on_rejected |
actor_id, step_name, comment |
state_entered |
state | services/state_workflow.py start_state_workflow, _move_branch_to_state, _fire_fork |
to_state, from_state, trigger, metadata.branch_id |
state_changed |
state | services/state_workflow.py transition_branch |
from_state, to_state, comment, metadata.to_state_type |
completed |
both | state: _finalize_if_done; approval: EndStepHandler.handle, StepDispatcher._finalize_workflow, process_end_step, complete_workflow |
metadata.template_name |
cancelled |
both | state: cancel_state_workflow; approval: cancel_workflow |
actor_id, comment, metadata.reason |
Notes:
state_enteredcarriestrigger=start,transition,forkorjoin.state_changedfires exactly once per successfultransition_branchcall.to_stateis the resolved landing state code, or''when the branch landed on a FORK, JOIN or END node — readmetadata.to_state_typeto tell which.- Blocked transitions emit nothing. A rejected/changes-requested step emits only its own event, never
completedorcancelled.
WorkflowHookContext Reference
WorkflowHookContext (enezaworkflowengine/services/workflow_hooks.py) is a frozen, JSON-serializable
dataclass passed to every handler.
| Field | Type | Notes |
|---|---|---|
event |
str | One of the WorkflowHookEvent values. |
workflowname |
str | WorkflowTemplate.workflowname, e.g. IncidentWorkflow. |
workflow_kind |
str | approval or state_based. |
workflow_instance_id |
str | UUID as text. |
document_model |
str | app_label.ModelName, e.g. frameworkapp.IncidentReport. |
document_id |
str | Primary key as text. |
actor_id |
str | User id who caused the event, or ''. |
from_state |
str | Previous state code, or ''. |
to_state |
str | Landing state code, or ''. |
step_name |
str | Step/state display name. |
comment |
str | Transition/approval comment, or ''. |
trigger |
str | start/transition/fork/join for state_entered, else ''. |
metadata |
dict | Event-specific extras. |
Methods (return None if the row no longer exists):
| Method | Returns |
|---|---|
get_document() |
The workflow's document via document_model + document_id. |
get_document_id() |
The document's primary key — the live pk (int/UUID) when the row exists, otherwise the raw document_id string, or None. |
get_workflow_instance() |
The WorkflowInstance. |
get_actor() |
The user via get_user_model(). |
To identify the record the workflow is attached to, read document_model and document_id directly
(no query), or call get_document_id() for a native-typed pk:
def on_submitted(context):
model = context.document_model # 'frameworkapp.IncidentReport'
pk = context.document_id # '42' (no query)
pk = context.get_document_id() # 42 (int/UUID; queries the row)
to_dict() / from_dict() convert to and from the primitive payload used by the async task.
Filter Semantics
A dict entry's filters are compared as strings against the matching context field:
'state_entered': [
{'handler': 'myapp.hooks.on_closed', 'to_state': 'closed'},
{'handler': 'myapp.hooks.on_reopen', 'from_state': 'closed', 'to_state': 'investigation'},
]
- Every present, non-empty filter must match exactly (logical AND).
- Absent or empty filters match anything.
- Valid filter names:
from_state,to_state,step_name,document_model. - Unknown keys are ignored.
Ordering and De-duplication
Handlers run sequentially, in this order:
- the
'*'workflow bucket, in list order, - the matching
workflownamebucket, in list order, - runtime hooks added with
register(), in registration order.
An identical (handler, filters) pair is executed once per event, even if declared twice.
Settings
| Setting | Default | Purpose |
|---|---|---|
MY_PROJECT_WORKFLOWS_CONFIG |
framework.workflowconfig.WorkflowTypeConfig (framework/settings.py:32) |
Dotted path to the class holding WORKFLOW_HOOKS. |
WORKFLOW_HOOKS_ASYNC |
unset (False) |
True enqueues hooks through django_tasks (requires python manage.py db_worker); falls back to inline if enqueueing fails. |
WORKFLOW_HOOKS_MAX_DEPTH |
10 |
Re-entrancy guard; hooks beyond this nesting depth are skipped with a warning. |
Failure History Rows
A hook that raises is logged and written to the document's workflow history:
| Field | Value |
|---|---|
action_type |
hook (WorkflowActionType.HOOK) |
step_name |
context.step_name |
message |
Hook '<dotted path>' failed for event '<event>': <error> |
metadata |
hook, event, error_type, error, from_state, to_state |
Successes are not recorded in history, to keep the timeline readable. The history templates render the
unknown type through the generic get_action_type_display fallback.
check_workflow_hooks
Validate the registry without running a workflow:
python manage.py check_workflow_hooks
python manage.py check_workflow_hooks --workflowname IncidentWorkflow
python manage.py check_workflow_hooks --json
The report prints MY_PROJECT_WORKFLOWS_CONFIG, the raw WORKFLOW_HOOKS mapping, the valid events,
and one row per entry with its status:
| Status | Meaning |
|---|---|
resolved |
Dotted path imports to a callable. |
unresolvable |
Import failed or the target is not callable. |
unknown_event |
The inner dict key is not a WorkflowHookEvent. |
invalid_bucket |
The workflowname value is not a dict. |
invalid_entry |
The entry is neither a string, a callable, nor a handler dict. |
The command exits non-zero when any entry is not resolved, so it can gate CI.
WorkflowHookRegistry API
from enezaworkflowengine.services.workflow_hooks import WorkflowHookRegistry
WorkflowHookRegistry.register('InvoiceWorkflow', 'approved', my_callable)
WorkflowHookRegistry.register('*', 'completed', my_callable, document_model='app.Model')
register(workflowname, event, handler, **filters)— adds a runtime handler for apps that cannot edit the project config;workflowname='*'matches every workflow. RaisesTypeErrorwhen the handler is not callable.iter_handlers(workflowname, event, context=None)— yields the resolved callables in execution order.describe()— the flat diagnostic view used by the command.
Runtime hooks live in process memory and are lost on restart; prefer the config registry for anything permanent.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Hooks never run | WORKFLOW_HOOKS empty, wrong MY_PROJECT_WORKFLOWS_CONFIG path, or workflowname mismatch |
python manage.py check_workflow_hooks --json |
| A single hook never runs | Filter value does not match (values are strings), or the dotted path is wrong | Check the entry's status and filters in the command output |
| Approval hooks never run but state hooks do | Approval events fire in background tasks; no worker running | Start python manage.py db_worker |
| Hooks do not run in tests | transaction.on_commit callbacks are not executed inside a wrapping TestCase transaction |
Use django.test.TestCase.captureOnCommitCallbacks(), or TransactionTestCase |
| Hooks run twice | Handler registered in both '*' and the workflowname bucket, or duplicate declarations |
Remove one declaration; de-duplication only catches identical (handler, filters) pairs |
Transition succeeds but a hook history row appears |
A handler raised after commit | Read metadata.error; hooks never fail the workflow by design |
| Recursive hook storm | A hook triggers more workflow work | Lower WORKFLOW_HOOKS_MAX_DEPTH or guard the hook by state |
| Async hooks never run | WORKFLOW_HOOKS_ASYNC = True without a running db_worker |
Start the worker, or unset the setting |
Related Guides
- Workflow Hooks — concepts and quick start.
- Workflow Project Configuration — the
workflowconfig/package pattern. - State-Based Workflow Reference — the state engine service API.
- 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.