$npx -y skills add Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-impl-schedulerUse when implementing scheduled tasks and background jobs in Frappe v14/v15/v16. Covers hooks.py scheduler_events, frappe.enqueue, queue selection, job deduplication, testing with bench execute/scheduler, monitoring via Scheduled Job Log and RQ Dashboard, error handling, long-run
| 1 | # Frappe Scheduler & Background Jobs - Implementation |
| 2 | |
| 3 | Workflow for implementing scheduled tasks and background jobs. For exact syntax, see `frappe-syntax-scheduler`. |
| 4 | |
| 5 | **Version**: v14/v15/v16 compatible |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## Main Decision: scheduler_events vs frappe.enqueue |
| 10 | |
| 11 | ``` |
| 12 | WHAT ARE YOU BUILDING? |
| 13 | | |
| 14 | +-- Runs at fixed intervals/times? |
| 15 | | +-- YES --> scheduler_events (hooks.py) |
| 16 | | | Task receives NO arguments |
| 17 | | | See: Workflow 1-2 |
| 18 | | | |
| 19 | | +-- NO --> Triggered by user action or code? |
| 20 | | +-- YES --> frappe.enqueue() |
| 21 | | | Pass any serializable data |
| 22 | | | See: Workflow 3-4 |
| 23 | | | |
| 24 | | +-- NO --> Reconsider requirements |
| 25 | ``` |
| 26 | |
| 27 | | Aspect | scheduler_events | frappe.enqueue | |
| 28 | |--------|------------------|----------------| |
| 29 | | Triggered by | Time/interval | Code execution | |
| 30 | | Defined in | hooks.py | Python code | |
| 31 | | Arguments | NONE (must be parameterless) | Any serializable data | |
| 32 | | Use case | Daily cleanup, hourly sync | User-triggered long task | |
| 33 | | Queue control | Event suffix (_long) | queue= parameter | |
| 34 | | Restart behavior | Runs on schedule | Lost if worker restarts | |
| 35 | |
| 36 | --- |
| 37 | |
| 38 | ## Which Scheduler Event Type? |
| 39 | |
| 40 | | Need | Event Key | Queue | |
| 41 | |------|-----------|-------| |
| 42 | | Every scheduler tick | `all` | short (NEVER >60s) | |
| 43 | | Hourly (<5 min) | `hourly` | short | |
| 44 | | Hourly (5-25 min) | `hourly_long` | long | |
| 45 | | Daily (<5 min) | `daily` | short | |
| 46 | | Daily (5-25 min) | `daily_long` | long | |
| 47 | | Weekly (<5 min) | `weekly` | short | |
| 48 | | Weekly (5-25 min) | `weekly_long` | long | |
| 49 | | Monthly (<5 min) | `monthly` | short | |
| 50 | | Monthly (5-25 min) | `monthly_long` | long | |
| 51 | | Custom schedule | `cron["expr"]` | short | |
| 52 | |
| 53 | **Rule**: ALWAYS use `*_long` suffix for tasks exceeding 5 minutes. |
| 54 | |
| 55 | --- |
| 56 | |
| 57 | ## Which Queue for frappe.enqueue? |
| 58 | |
| 59 | | Queue | Default Timeout | Use For | |
| 60 | |-------|-----------------|---------| |
| 61 | | `short` | 300s (5 min) | Quick operations (<1 min) | |
| 62 | | `default` | 300s (5 min) | Standard tasks (1-5 min) | |
| 63 | | `long` | 1500s (25 min) | Heavy processing (>5 min) | |
| 64 | |
| 65 | **Rule**: ALWAYS specify `queue=` explicitly. NEVER rely on the default. |
| 66 | |
| 67 | --- |
| 68 | |
| 69 | ## Implementation Step 1: Scheduler Event |
| 70 | |
| 71 | ```python |
| 72 | # myapp/tasks.py |
| 73 | import frappe |
| 74 | |
| 75 | def daily_cleanup(): |
| 76 | """Daily cleanup - NO parameters allowed.""" |
| 77 | cutoff = frappe.utils.add_days(frappe.utils.nowdate(), -30) |
| 78 | frappe.db.delete("Error Log", {"creation": ("<", cutoff)}) |
| 79 | frappe.db.commit() |
| 80 | ``` |
| 81 | |
| 82 | ```python |
| 83 | # hooks.py |
| 84 | scheduler_events = { |
| 85 | "daily": ["myapp.tasks.daily_cleanup"] |
| 86 | } |
| 87 | ``` |
| 88 | |
| 89 | **After editing hooks.py**: ALWAYS run `bench migrate`. |
| 90 | |
| 91 | --- |
| 92 | |
| 93 | ## Implementation Step 2: Background Job (frappe.enqueue) |
| 94 | |
| 95 | ```python |
| 96 | # myapp/api.py |
| 97 | import frappe |
| 98 | from frappe.utils.background_jobs import is_job_enqueued |
| 99 | |
| 100 | @frappe.whitelist() |
| 101 | def process_documents(doctype, filters): |
| 102 | job_id = f"process_{doctype}_{frappe.session.user}" |
| 103 | |
| 104 | if is_job_enqueued(job_id): |
| 105 | return {"message": "Already in progress"} |
| 106 | |
| 107 | frappe.enqueue( |
| 108 | "myapp.tasks.process_batch", |
| 109 | queue="long", |
| 110 | timeout=1800, |
| 111 | job_id=job_id, |
| 112 | enqueue_after_commit=True, |
| 113 | doctype=doctype, |
| 114 | filters=filters |
| 115 | ) |
| 116 | return {"status": "queued"} |
| 117 | ``` |
| 118 | |
| 119 | --- |
| 120 | |
| 121 | ## Testing Scheduled Tasks |
| 122 | |
| 123 | ### Method 1: bench execute (direct) |
| 124 | ```bash |
| 125 | # Run the function directly (no queue involved) |
| 126 | bench --site mysite execute myapp.tasks.daily_cleanup |
| 127 | ``` |
| 128 | |
| 129 | ### Method 2: bench scheduler (full scheduler test) |
| 130 | ```bash |
| 131 | # Check scheduler status |
| 132 | bench --site mysite scheduler status |
| 133 | |
| 134 | # Enable scheduler |
| 135 | bench --site mysite scheduler enable |
| 136 | |
| 137 | # Trigger all pending scheduler events NOW |
| 138 | bench --site mysite scheduler trigger |
| 139 | |
| 140 | # Run specific event type |
| 141 | bench --site mysite execute frappe.utils.scheduler.trigger --args "['daily']" |
| 142 | ``` |
| 143 | |
| 144 | ### Method 3: bench console (interactive) |
| 145 | ```python |
| 146 | bench --site mysite console |
| 147 | >>> frappe.enqueue("myapp.tasks.my_task", queue="short", now=True) |
| 148 | # now=True executes synchronously for testing |
| 149 | ``` |
| 150 | |
| 151 | ### Method 4: Check Scheduled Job Type |
| 152 | ``` |
| 153 | 1. Go to: Setup > Scheduled Job Type |
| 154 | 2. Find: myapp.tasks.daily_cleanup |
| 155 | 3. Verify: Frequency correct, Stopped = No |
| 156 | 4. Click "Run Now" to trigger manually |
| 157 | ``` |
| 158 | |
| 159 | --- |
| 160 | |
| 161 | ## Monitoring |
| 162 | |
| 163 | ### Scheduled Job Log (UI) |
| 164 | ``` |
| 165 | Setup > S |