Skip to content

Commit ec51f9e

Browse files
FreekHeijtingclaude
andcommitted
feat: V2.5 new skills P0 batch 2 (6 skills)
New skills with full content: - frappe-core-workflow: engine internals, states, transitions, actions - frappe-impl-workflow: 10-step implementation, approval patterns - frappe-testing-unit: FrappeTestCase, IntegrationTestCase [v15+], fixtures - frappe-testing-cicd: GitHub Actions, pre-commit, ruff, Semgrep - frappe-impl-reports: Query/Script Report, charts, Number Cards, dashboards - frappe-ops-app-lifecycle: bench new-app to marketplace publishing All P0 priority skills now complete. 9/25 new skills done. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a105128 commit ec51f9e

27 files changed

Lines changed: 6638 additions & 33 deletions

File tree

skills/source/core/frappe-core-workflow/SKILL.md

Lines changed: 215 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,227 @@ metadata:
1414

1515
# Workflow Engine
1616

17-
> **STATUS**: Stub - content pending V2.5 development phase.
17+
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.
1818

1919
## Quick Reference
2020

21-
_Content to be developed._
21+
```
22+
Workflow DocType → Defines the state machine for a specific DocType
23+
├── states (child table) → Workflow Document State rows
24+
│ ├── state → Link to Workflow State
25+
│ ├── doc_status → 0 (Draft), 1 (Submitted), 2 (Cancelled)
26+
│ ├── allow_edit → Role that can edit in this state
27+
│ ├── update_field → Field to update when entering state
28+
│ ├── update_value → Value to set (literal or expression)
29+
│ └── next_action_email_template → Email Template link
30+
└── transitions (child table) → Workflow Transition rows
31+
├── state → Source state (Link to Workflow State)
32+
├── action → Link to Workflow Action Master
33+
├── next_state → Target state (Link to Workflow State)
34+
├── allowed → Role that can perform this action
35+
├── allow_self_approval → Check (default: 1)
36+
├── condition → Python expression (optional)
37+
└── transition_tasks → Link to Workflow Transition Tasks
38+
```
39+
40+
### Key Fields on Workflow DocType
41+
42+
| Field | Type | Purpose |
43+
|-------|------|---------|
44+
| `workflow_name` | Data | Unique identifier |
45+
| `document_type` | Link → DocType | Target DocType |
46+
| `is_active` | Check | Only ONE workflow per DocType can be active |
47+
| `workflow_state_field` | Data | Default: `workflow_state` |
48+
| `override_status` | Check | Prevent workflow from overriding list view status |
49+
| `send_email_alert` | Check | Email notifications with next possible actions |
50+
51+
## How the Engine Works
52+
53+
### 1. Activation and Field Creation
54+
55+
When a Workflow is saved with `is_active = 1`:
56+
- All other workflows for the same DocType are deactivated automatically
57+
- A hidden Custom Field (`workflow_state_field`, default `workflow_state`) is created on the target DocType if it does not exist
58+
- The field is type `Link` to `Workflow State`, with `hidden=1`, `allow_on_submit=1`, `no_copy=1`
59+
- Existing documents with empty workflow state get their state set based on their current `docstatus`
60+
61+
### 2. State Resolution
62+
63+
Every document under a workflow has a `workflow_state` field. The engine resolves available transitions by:
64+
65+
1. Reading current `workflow_state` from the document
66+
2. Filtering `workflow.transitions` where `transition.state == current_state`
67+
3. Filtering by user roles: `transition.allowed in frappe.get_roles()`
68+
4. Evaluating `transition.condition` via `frappe.safe_eval()` (if set)
69+
5. Returning matching transitions as available actions
70+
71+
### 3. Applying a Transition
72+
73+
When `apply_workflow(doc, action)` is called:
74+
75+
1. Load document from DB (fresh read)
76+
2. Get available transitions for current user
77+
3. Find transition matching the requested `action`
78+
4. Check self-approval: blocked if `allow_self_approval=0` AND user is document owner
79+
5. Set `workflow_state_field` to `transition.next_state`
80+
6. If `update_field` is set on the target state, update that field
81+
7. Execute transition tasks (sync first, then async via `frappe.enqueue`)
82+
8. Handle docstatus change based on source/target state `doc_status` values
83+
9. Save/Submit/Cancel document accordingly
84+
10. Add workflow comment
85+
86+
## Workflow and DocStatus Interaction
87+
88+
**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.
89+
90+
### DocStatus Transition Rules
91+
92+
| Source doc_status | Target doc_status | Engine Action | Valid? |
93+
|:-:|:-:|---|:-:|
94+
| 0 (Draft) | 0 (Draft) | `doc.save()` | YES |
95+
| 0 (Draft) | 1 (Submitted) | `doc.submit()` | YES |
96+
| 1 (Submitted) | 1 (Submitted) | `doc.save()` | YES |
97+
| 1 (Submitted) | 2 (Cancelled) | `doc.cancel()` | YES |
98+
| 2 (Cancelled) | ANY | BLOCKED | NO |
99+
| 1 (Submitted) | 0 (Draft) | BLOCKED | NO |
100+
| 0 (Draft) | 2 (Cancelled) | BLOCKED | NO |
101+
102+
**ALWAYS** define your states so that docstatus only moves forward: 0→0, 0→1, 1→1, 1→2.
103+
**NEVER** create a transition from a cancelled state or from submitted back to draft.
104+
105+
### Non-Submittable DocTypes
106+
107+
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.
108+
109+
## Workflow States
110+
111+
Workflow State is a separate DocType used as a master list. Each state has:
112+
113+
| Field | Purpose |
114+
|-------|---------|
115+
| `state` | Display name of the state |
116+
| `style` | CSS class for badge display (Primary, Success, Warning, Danger, Info, Inverse) |
117+
| `icon` | Font Awesome icon class |
118+
119+
### State Row Fields (Workflow Document State)
120+
121+
| Field | Purpose |
122+
|-------|---------|
123+
| `state` | Link to Workflow State |
124+
| `doc_status` | Select: 0, 1, or 2 |
125+
| `allow_edit` | Link to Role — ONLY this role can edit the document in this state |
126+
| `update_field` | Field to update when document enters this state |
127+
| `update_value` | Value to set (string or Python expression if `evaluate_as_expression=1`) |
128+
| `is_optional_state` | Check — optional states are skipped in `get_next_possible_transitions` |
129+
| `send_email` | Check (default 1) — send email notification on entering this state |
130+
| `next_action_email_template` | Link to Email Template |
131+
| `message` | Text message for the email notification |
132+
133+
## Workflow Transitions
134+
135+
Each transition row defines one possible action:
136+
137+
| Field | Purpose |
138+
|-------|---------|
139+
| `state` | Source state (MUST exist in states table) |
140+
| `action` | Link to Workflow Action Master (e.g., "Approve", "Reject", "Review") |
141+
| `next_state` | Target state (MUST exist in states table) |
142+
| `allowed` | Link to Role — ONLY users with this role see this action |
143+
| `allow_self_approval` | Check (default 1) — if 0, document owner cannot perform this action |
144+
| `condition` | Python expression evaluated with `frappe.safe_eval()` |
145+
| `transition_tasks` | Link to Workflow Transition Tasks (v15+) |
146+
147+
### Condition Expressions
148+
149+
Conditions are Python expressions evaluated in a sandboxed environment. Available globals:
150+
151+
```python
152+
# Available in condition expressions:
153+
frappe.db.get_value(doctype, name, fieldname)
154+
frappe.db.get_list(doctype, filters, fields)
155+
frappe.session.user
156+
frappe.session.roles # NOT available — use frappe.get_roles() outside conditions
157+
frappe.utils.now_datetime()
158+
frappe.utils.add_to_date(date, **kwargs)
159+
frappe.utils.get_datetime(datetime_str)
160+
frappe.utils.now()
161+
doc.fieldname # Access any field on the document (as dict)
162+
```
163+
164+
Example conditions:
165+
```python
166+
doc.grand_total > 50000
167+
doc.department == "HR"
168+
doc.grand_total > 50000 and doc.department != "Finance"
169+
```
170+
171+
## Workflow Actions
172+
173+
### Workflow Action Master
174+
175+
Simple DocType with just a `workflow_action_name` field. Common actions: Approve, Reject, Review, Send Back, Cancel. Create these first before defining transitions.
176+
177+
### Workflow Action DocType
178+
179+
Tracks pending actions for users. Created automatically when a document enters a state with outgoing transitions.
180+
181+
| Field | Purpose |
182+
|-------|---------|
183+
| `status` | Open or Completed |
184+
| `reference_doctype` | The DocType of the document |
185+
| `reference_name` | The document name |
186+
| `workflow_state` | Current workflow state |
187+
| `user` | Assigned user |
188+
| `permitted_roles` | Table MultiSelect of roles that can act |
189+
| `completed_by` | User who completed the action |
190+
| `completed_by_role` | Role used to complete |
191+
192+
Workflow Actions appear in the user's "Workflow Action" list and can be acted on via email links.
193+
194+
## Self-Approval Control
195+
196+
```python
197+
def has_approval_access(user, doc, transition):
198+
return (user == "Administrator"
199+
or transition.get("allow_self_approval")
200+
or user != doc.get("owner"))
201+
```
202+
203+
- **Administrator** ALWAYS has approval access regardless of settings
204+
- If `allow_self_approval = 1` (default): document owner CAN approve
205+
- If `allow_self_approval = 0`: document owner CANNOT approve their own document
22206

23207
## Decision Tree
24208

25-
_Content to be developed._
209+
```
210+
Need workflow on a DocType?
211+
├── Is DocType submittable?
212+
│ ├── YES → States can use doc_status 0, 1, 2
213+
│ └── NO → ALL states MUST have doc_status = 0
214+
├── Define states → Create Workflow State records first
215+
├── Define transitions → Need Workflow Action Master records first
216+
├── Who can edit in each state? → Set allow_edit per state
217+
├── Need conditional transitions?
218+
│ └── Use Python expressions with doc.field access
219+
├── Need to block self-approval?
220+
│ └── Set allow_self_approval = 0 on specific transitions
221+
└── Need email notifications?
222+
└── Set send_email_alert on Workflow + email templates on states
223+
```
224+
225+
## Common Errors
226+
227+
| Error | Cause | Fix |
228+
|-------|-------|-----|
229+
| `WorkflowStateError` | Document has no workflow_state set | Ensure workflow sets initial state on creation |
230+
| `WorkflowTransitionError` | Action not valid for current state/role | Verify transitions table covers all needed paths |
231+
| `WorkflowPermissionError` | User lacks role for transition, or self-approval blocked | Check `allowed` role and `allow_self_approval` |
232+
| "Illegal Document Status" | Invalid docstatus transition (e.g., 0→2) | Fix state `doc_status` values |
233+
| "Cannot cancel before submitting" | Transition from draft (0) to cancelled (2) | Add intermediate submitted (1) state |
26234

27235
## See Also
28236

29-
_Cross-references to be added._
237+
- [API Reference](references/api-reference.md) — Complete workflow Python API
238+
- [Examples](references/examples.md) — Workflow configuration examples
239+
- [Anti-Patterns](references/anti-patterns.md) — Common mistakes and how to avoid them
240+
- `frappe-impl-workflow` — Step-by-step implementation guide

0 commit comments

Comments
 (0)