Workflow Assignments, Group Approvals and Recovery
This document describes the assignment, delegation, group approval, asynchronous execution, and recovery behavior added to both workflow engines in enezaworkflowengine.
1. Assignment strategies
Every StepTemplate (approval and state) has exactly one assignment_strategy:
user— assigns the selectedassignee_users; groups must not be configured.group— assigns every active member of the selectedassignee_groups; users must not be configured.hr_hierarchy— resolves one assignee from the initiator's HR reporting chain athr_level; users and groups must not be configured.
The designer presents these as three mutually exclusive choices and clears the other sources when the choice changes. Template validation rejects any step that mixes sources (for example users plus groups). direct remains only as a legacy value for templates saved before the three-mode split; the designer flags it and asks the user to choose user or group. New steps default to user.
HR hierarchy rules
- The hierarchy root is the
Employeelinked toWorkflowInstance.initiated_by. - If the initiator has no linked employee, the operation stops and the workflow enters a recoverable error. An administrator must link the employee and resume.
- Level N counts filled manager positions above the employee, skipping vacant positions. - Level 1 = first occupied manager position. - Level 2 = next occupied manager position above that.
- If the chain ends before level N, the operation stops with a recoverable error.
- A filled manager position whose employee has no linked active user is an error; the engine never falls back to an unrelated user.
- The full inspected chain, level, resolved employee, and resolved user are stored in
HR_ASSIGNMENThistory.
2. Temporary delegation
Users configure WorkflowDelegation rules from /workflow/delegations/:
- delegate (must be active, not the same user);
- start and end timestamps (timezone-aware, inclusive boundaries);
- all workflows, or a selected list of workflow names;
- active flag and notes.
At assignment resolution, the engine checks the owner's active rule and creates the effective assignment for the delegate. The original assignee is preserved in WorkflowApproverAction.original_approver, and the rule is recorded in delegation_rule.
Only one hop is followed. Self-delegation, overlapping active rules for the same owner, and direct two-node loops are rejected at save time. Delegation is distinct from administrative reassignment: the user owns the rule and the engine applies/expires it.
Expiry
expire_due_delegations runs:
- every 60 seconds from
run_periodic; - lazily before assignment resolution when due rules exist;
- manually through
python manage.py expire_workflow_delegations.
When a rule expires:
- the delegate's pending action is marked
expired/unavailable; - a pending action is returned to the active original assignee;
- completed decisions are unchanged;
- if the original assignee is inactive, a recoverable assignment error is recorded for an administrator.
3. User groups and approval policies
Groups reuse Django auth.Group. WorkflowGroupProfile adds a description and active flag. Membership changes are recorded in WorkflowGroupMembershipEvent with actor, source, and timestamp.
Group administration is available at /workflow/groups/ and requires action.enezaworkflowengine.workflow_group_admin.
Policy mapping
single_approver= Any one — first valid approval completes the step.majority_approver= Majority —(eligible_count // 2) + 1approvals.all_approver= All — every eligible approver must approve.
The same policies apply to state-based STATE nodes. Each assignee votes by selecting a transition;
votes are counted per destination and the branch moves when one destination reaches the threshold.
For majority/all, split votes can stall the state until an administrator reassigns or cancels. A
blank state threshold behaves as single_approver.
At step entry the engine resolves effective approvers through the shared assignment resolver and freezes:
eligible_approver_count;assignment_snapshot, including group ids, original/effective users, policy, and capture time.
Duplicate decisions by the same user are prevented by the unique action row and a row lock.
Rejection behavior
StepTemplate.rejection_behavior controls rejection:
policy_default- any one: first decision wins, so a rejection rejects the workflow;
- majority: rejection fails the workflow only when the remaining eligible approvers cannot reach the threshold;
- all: any rejection fails the workflow.
fail_workflow— first rejection rejects the workflow.continue_step— rejection is recorded and the step remains open while the policy is satisfiable. It is invalid for the ALL policy.
Deactivating a group, removing a member, or deactivating a user marks their pending action unavailable but does not change the frozen denominator. The threshold is never silently lowered. If ALL becomes unsatisfiable, an administrator must reassign or replace the approver.
4. Comments
Comments are optional unless StepTemplate.comment_required (approval) or StateTransition.comment_required (state move) is enabled.
Each comment is stored as an immutable WorkflowComment linked to:
- the workflow instance;
- step/branch where applicable;
- the approver action and/or history transaction;
- actor, action, timestamp, and metadata.
WorkflowApproverAction.comment is still populated for backward compatibility. Historical comments are never overwritten. The history UI shows comments inline and they remain after reassignment.
5. Administrative reassignment
POST /workflow/<instance_id>/reassign/ (and services.reassignment.reassign_workflow_task) requires:
action.enezaworkflowengine.workflow_operationsor superuser;- an active, current task;
- a pending assignment for the selected current assignee;
- an active replacement user;
- a reason.
The operation:
- marks the original action unavailable but keeps it auditable;
- creates a pending action for the replacement with
assignment_source=reassignment; - preserves the workflow instance, current step, status, approval progress, comments, and completed decisions;
- does not change
eligible_approver_count; - records
REASSIGNMENThistory and a comment with the reason; - notifies the replacement.
Historical approvals and comments are never deleted or modified.
6. Asynchronous state execution
State workflow submission and manual transitions no longer run the engine in the request thread.
- Submission creates a
WorkflowExecutionRequest(operation=start_state)and enqueues it after commit. - A manual transition validates authorization, edge, comment requirement, and guard condition, then creates a
WorkflowExecutionRequest(operation=state_transition). process_workflow_execution_requestperforms the state change, assignment resolution, automatic routing, and history updates.- Requests are idempotent by unique key and status. Duplicate delivery returns the stored result.
dispatch_pending_workflow_requestsruns every 60 seconds as a durable outbox sweeper, covering enqueue failures.- Notifications, document-state writes, and hooks pass through
WorkflowSideEffectso duplicate delivery after success is a no-op.
Deployment requires the existing db_worker and run_periodic processes. Scheduling alone is not treated as execution.
7. Error recovery
Failures are recorded in WorkflowError with:
- workflow instance, step, branch, execution request;
- failed operation and stage;
- message, exception type, traceback;
- attempt, task id, correlation id;
- recovery status, retryability, retry payload;
- timestamp via
created_at.
The workflow is marked is_in_error=True and normal actions are blocked.
Resume
POST /workflow/<instance_id>/resume/ requires action.enezaworkflowengine.workflow_operations or superuser and a reason.
Resume:
- finds the latest open error;
- validates that the workflow is still in error and its current step/history match the error context;
- retries only the failed operation (state request, approval enter/action, condition, notification, hook, document state, or delegation expiry);
- records
WorkflowRecoveryAttemptandRESUMEhistory with administrator, reason, attempt, and result; - clears the error only on success.
A failed resume keeps the workflow in error. Resume never selects an arbitrary target or bypasses approval requirements.
8. History and auditability
WorkflowTransaction now records actor_type (user/system/task), correlation_id, task_id, and an optional WorkflowError. New action types cover delegation, delegation expiry, reassignment, HR assignment, errors, resume, task execution, and comments.
Historical records are preserved when templates, group membership, or HR reporting relationships change. simple_history remains enabled for all new and changed models.
9. RBAC and deployment
New action codes:
action.enezaworkflowengine.workflow_operations— reassignment, resume, error queue.action.enezaworkflowengine.workflow_group_admin— group administration.
Delegation self-service uses the existing workflow action.
After deployment:
python manage.py migrate
python manage.py rbac_sync
python manage.py db_worker
python manage.py run_periodic
Ensure HR initiators have linked Employee records with active primary positions before enabling HR-hierarchy assignment.