NEVER start building a workflow directly in the UI without designing the state machine first.
WRONG:
Open Workflow form → start adding states randomly → add transitions → debug
CORRECT:
1. Draw state diagram on paper/whiteboard
2. List all states with doc_status values
3. List all transitions with roles and conditions
4. Verify all paths have entry AND exit
5. Then implement
Why: Ad-hoc workflow design leads to dead-end states, missing transitions, and circular paths that trap documents.
NEVER set doc_status = 1 on an intermediate approval state.
# WRONG — once submitted, document cannot return to draft
{"state": "L1 Approved", "doc_status": "1"}, # Too early!
{"state": "L2 Approved", "doc_status": "1"},
# If L2 rejects, document is stuck — cannot go back to draft
# CORRECT — keep intermediate states at doc_status 0
{"state": "L1 Approved", "doc_status": "0"},
{"state": "L2 Approved", "doc_status": "0"},
{"state": "Final Approved", "doc_status": "1"}, # Only final state is submittedALWAYS provide a rejection/send-back path from every approval state.
# WRONG — no way to reject after reaching Pending Approval
transitions = [
{"state": "Draft", "action": "Submit", "next_state": "Pending Approval", ...},
{"state": "Pending Approval", "action": "Approve", "next_state": "Approved", ...},
]
# If the approver wants to reject, they have no action available
# CORRECT — add rejection path
transitions = [
{"state": "Draft", "action": "Submit", "next_state": "Pending Approval", ...},
{"state": "Pending Approval", "action": "Approve", "next_state": "Approved", ...},
{"state": "Pending Approval", "action": "Reject", "next_state": "Draft", ...},
]NEVER create transitions where the same role has multiple actions with overlapping conditions.
# WRONG — Manager sees both Approve options when amount is exactly 50000
{"state": "Pending", "action": "Approve", "next_state": "Approved",
"allowed": "Manager", "condition": "doc.amount <= 50000"},
{"state": "Pending", "action": "Approve", "next_state": "Escalated",
"allowed": "Manager", "condition": "doc.amount >= 50000"},
# amount == 50000 matches BOTH — user sees duplicate buttons
# CORRECT — mutually exclusive conditions
{"condition": "doc.amount <= 50000"},
{"condition": "doc.amount > 50000"},ALWAYS test rejection loops, self-approval blocking, edge-case conditions, and concurrent user scenarios.
WRONG: Test only: Draft → Approve → Submitted. Ship it.
CORRECT test plan:
□ Happy path: Draft → Pending → Approved → Submitted
□ Rejection loop: Draft → Pending → Rejected → Draft → Pending → Approved
□ Self-approval: Creator tries to approve their own doc (should fail)
□ Wrong role: User without approval role tries to approve (should see no button)
□ Condition boundaries: Test with exact boundary values
□ Concurrent: Two approvers act on same doc simultaneously
□ Existing documents: Check docs created before workflow activation
ALWAYS set allow_edit to restrict who can modify documents in each state.
# WRONG — anyone can edit in any state
{"state": "Pending Approval", "doc_status": "0"}, # No allow_edit set
# CORRECT — only the approver can edit during their review
{"state": "Pending Approval", "doc_status": "0", "allow_edit": "Approver Role"},Why: Without allow_edit, any user with DocType write permission can modify the document while it awaits approval.
NEVER activate a workflow on a DocType with existing documents without verifying state mapping.
# When activated, the engine runs update_default_workflow_status:
# - Docs with docstatus=0 get mapped to first state with doc_status=0
# - Docs with docstatus=1 get mapped to first state with doc_status=1
# - Docs with docstatus=2 get mapped to first state with doc_status=2
# WRONG — workflow has no state with doc_status=1, but submitted POs exist
# Submitted POs get empty workflow_state → stuck
# CORRECT — ensure a state exists for every docstatus your existing docs haveNEVER use a Workflow when a simple Select field would suffice.
WRONG: Single-user status tracking (Open → In Progress → Done)
→ No approval, no role restrictions, no conditions
→ A workflow adds unnecessary overhead
CORRECT use cases for Workflow:
✓ Multi-user approval chains
✓ Role-based state transitions
✓ Conditional routing by document values
✓ Self-approval prevention
✓ Controlling when docstatus changes
NEVER create cycles that have no terminal state reachable from the cycle.
# WRONG — infinite loop with no way to finish
{"state": "Review", "action": "Send Back", "next_state": "Revision"},
{"state": "Revision", "action": "Resubmit", "next_state": "Review"},
# No "Approve" transition → document loops forever
# CORRECT — cycle with exit
{"state": "Review", "action": "Send Back", "next_state": "Revision"},
{"state": "Review", "action": "Approve", "next_state": "Approved"}, # Exit!
{"state": "Revision", "action": "Resubmit", "next_state": "Review"},NEVER enable send_email_alert without configuring proper email templates.
# WRONG — emails enabled but no template
{"send_email_alert": 1}
# States have send_email=1 (default) but no next_action_email_template
# Users get generic/empty notifications
# CORRECT — configure templates
{"state": "Pending Approval", "send_email": 1,
"next_action_email_template": "Workflow Approval Request",
"message": "Document {{ doc.name }} requires your approval."}