Frappe_Claude_Skill_Package frappe-core-workflow
git clone https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package
T=$(mktemp -d) && git clone --depth=1 https://github.com/OpenAEC-Foundation/Frappe_Claude_Skill_Package "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/source/core/frappe-core-workflow" ~/.claude/skills/openaec-foundation-frappe-claude-skill-package-frappe-core-workflow && rm -rf "$T"
skills/source/core/frappe-core-workflow/SKILL.mdWorkflow Engine
The Frappe Workflow engine is a state machine that controls document lifecycle through configurable states, transitions, and role-based permissions. It governs when and how documents change status, who can perform actions, and what side effects occur on each transition.
Quick Reference
Workflow DocType → Defines the state machine for a specific DocType ├── states (child table) → Workflow Document State rows │ ├── state → Link to Workflow State │ ├── doc_status → 0 (Draft), 1 (Submitted), 2 (Cancelled) │ ├── allow_edit → Role that can edit in this state │ ├── update_field → Field to update when entering state │ ├── update_value → Value to set (literal or expression) │ └── next_action_email_template → Email Template link └── transitions (child table) → Workflow Transition rows ├── state → Source state (Link to Workflow State) ├── action → Link to Workflow Action Master ├── next_state → Target state (Link to Workflow State) ├── allowed → Role that can perform this action ├── allow_self_approval → Check (default: 1) ├── condition → Python expression (optional) └── transition_tasks → Link to Workflow Transition Tasks
Key Fields on Workflow DocType
| Field | Type | Purpose |
|---|---|---|
| Data | Unique identifier |
| Link → DocType | Target DocType |
| Check | Only ONE workflow per DocType can be active |
| Data | Default: |
| Check | Prevent workflow from overriding list view status |
| Check | Email notifications with next possible actions |
How the Engine Works
1. Activation and Field Creation
When a Workflow is saved with
is_active = 1:
- All other workflows for the same DocType are deactivated automatically
- A hidden Custom Field (
, defaultworkflow_state_field
) is created on the target DocType if it does not existworkflow_state - The field is type
toLink
, withWorkflow State
,hidden=1
,allow_on_submit=1no_copy=1 - Existing documents with empty workflow state get their state set based on their current
docstatus
2. State Resolution
Every document under a workflow has a
workflow_state field. The engine resolves available transitions by:
- Reading current
from the documentworkflow_state - Filtering
whereworkflow.transitionstransition.state == current_state - Filtering by user roles:
transition.allowed in frappe.get_roles() - Evaluating
viatransition.condition
(if set)frappe.safe_eval() - Returning matching transitions as available actions
3. Applying a Transition
When
apply_workflow(doc, action) is called:
- Load document from DB (fresh read)
- Get available transitions for current user
- Find transition matching the requested
action - Check self-approval: blocked if
AND user is document ownerallow_self_approval=0 - Set
toworkflow_state_fieldtransition.next_state - If
is set on the target state, update that fieldupdate_field - Execute transition tasks (sync first, then async via
)frappe.enqueue - Handle docstatus change based on source/target state
valuesdoc_status - Save/Submit/Cancel document accordingly
- Add workflow comment
Workflow and DocStatus Interaction
CRITICAL: The workflow engine controls docstatus transitions. You NEVER call
doc.submit() or doc.cancel() directly on a workflow-controlled document. The workflow does it.
DocStatus Transition Rules
| Source doc_status | Target doc_status | Engine Action | Valid? |
|---|---|---|---|
| 0 (Draft) | 0 (Draft) | | YES |
| 0 (Draft) | 1 (Submitted) | | YES |
| 1 (Submitted) | 1 (Submitted) | | YES |
| 1 (Submitted) | 2 (Cancelled) | | YES |
| 2 (Cancelled) | ANY | BLOCKED | NO |
| 1 (Submitted) | 0 (Draft) | BLOCKED | NO |
| 0 (Draft) | 2 (Cancelled) | BLOCKED | NO |
ALWAYS define your states so that docstatus only moves forward: 0→0, 0→1, 1→1, 1→2. NEVER create a transition from a cancelled state or from submitted back to draft.
Non-Submittable DocTypes
If the target DocType is NOT submittable, ALL states MUST have
doc_status = 0. The engine validates this and throws an error if any state has doc_status = 1 or 2 on a non-submittable DocType.
Workflow States
Workflow State is a separate DocType used as a master list. Each state has:
| Field | Purpose |
|---|---|
| Display name of the state |
| CSS class for badge display (Primary, Success, Warning, Danger, Info, Inverse) |
| Font Awesome icon class |
State Row Fields (Workflow Document State)
| Field | Purpose |
|---|---|
| Link to Workflow State |
| Select: 0, 1, or 2 |
| Link to Role — ONLY this role can edit the document in this state |
| Field to update when document enters this state |
| Value to set (string or Python expression if ) |
| Check — optional states are skipped in |
| Check (default 1) — send email notification on entering this state |
| Link to Email Template |
| Text message for the email notification |
Workflow Transitions
Each transition row defines one possible action:
| Field | Purpose |
|---|---|
| Source state (MUST exist in states table) |
| Link to Workflow Action Master (e.g., "Approve", "Reject", "Review") |
| Target state (MUST exist in states table) |
| Link to Role — ONLY users with this role see this action |
| Check (default 1) — if 0, document owner cannot perform this action |
| Python expression evaluated with |
| Link to Workflow Transition Tasks (v15+) |
Condition Expressions
Conditions are Python expressions evaluated in a sandboxed environment. Available globals:
# Available in condition expressions: frappe.db.get_value(doctype, name, fieldname) frappe.db.get_list(doctype, filters, fields) frappe.session.user frappe.session.roles # NOT available — use frappe.get_roles() outside conditions frappe.utils.now_datetime() frappe.utils.add_to_date(date, **kwargs) frappe.utils.get_datetime(datetime_str) frappe.utils.now() doc.fieldname # Access any field on the document (as dict)
Example conditions:
doc.grand_total > 50000 doc.department == "HR" doc.grand_total > 50000 and doc.department != "Finance"
Workflow Actions
Workflow Action Master
Simple DocType with just a
workflow_action_name field. Common actions: Approve, Reject, Review, Send Back, Cancel. Create these first before defining transitions.
Workflow Action DocType
Tracks pending actions for users. Created automatically when a document enters a state with outgoing transitions.
| Field | Purpose |
|---|---|
| Open or Completed |
| The DocType of the document |
| The document name |
| Current workflow state |
| Assigned user |
| Table MultiSelect of roles that can act |
| User who completed the action |
| Role used to complete |
Workflow Actions appear in the user's "Workflow Action" list and can be acted on via email links.
Self-Approval Control
def has_approval_access(user, doc, transition): return (user == "Administrator" or transition.get("allow_self_approval") or user != doc.get("owner"))
- Administrator ALWAYS has approval access regardless of settings
- If
(default): document owner CAN approveallow_self_approval = 1 - If
: document owner CANNOT approve their own documentallow_self_approval = 0
Decision Tree
Need workflow on a DocType? ├── Is DocType submittable? │ ├── YES → States can use doc_status 0, 1, 2 │ └── NO → ALL states MUST have doc_status = 0 ├── Define states → Create Workflow State records first ├── Define transitions → Need Workflow Action Master records first ├── Who can edit in each state? → Set allow_edit per state ├── Need conditional transitions? │ └── Use Python expressions with doc.field access ├── Need to block self-approval? │ └── Set allow_self_approval = 0 on specific transitions └── Need email notifications? └── Set send_email_alert on Workflow + email templates on states
Common Errors
| Error | Cause | Fix |
|---|---|---|
| Document has no workflow_state set | Ensure workflow sets initial state on creation |
| Action not valid for current state/role | Verify transitions table covers all needed paths |
| User lacks role for transition, or self-approval blocked | Check role and |
| "Illegal Document Status" | Invalid docstatus transition (e.g., 0→2) | Fix state values |
| "Cannot cancel before submitting" | Transition from draft (0) to cancelled (2) | Add intermediate submitted (1) state |
See Also
- API Reference — Complete workflow Python API
- Examples — Workflow configuration examples
- Anti-Patterns — Common mistakes and how to avoid them
— Step-by-step implementation guidefrappe-impl-workflow