> Use when configuring scheduler events and background jobs in Frappe/ERPNext v14/v15/v16. Covers scheduler_events in hooks.py, frappe.enqueue() for async jobs, queue configuration, job deduplication, error handling, and queue, async task, frappe.enqueue, scheduled task, cron syntax, how often does it run, background job example, enqueue example.
npx skills add https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-syntax-scheduler
Deterministic syntax reference for Frappe scheduler events and background job processing via Redis Queue (RQ).
Need periodic execution?
├─ Fixed interval (hourly/daily/weekly/monthly) → scheduler_events in hooks.py
├─ Custom cron schedule → scheduler_events.cron in hooks.py
├─ User-configurable interval → Scheduled Job Type DocType
└─ No, triggered by user/event
├─ Run method on a specific document → frappe.enqueue_doc()
├─ Run standalone function async → frappe.enqueue()
└─ Run from controller on self → self.queue_action()
# hooks.py — ALWAYS run bench migrate after changes
scheduler_events = {
# Standard events (default queue)
"all": ["myapp.tasks.every_tick"], # Every tick [v14: 240s, v15+: 60s]
"hourly": ["myapp.tasks.hourly_task"],
"daily": ["myapp.tasks.daily_task"],
"weekly": ["myapp.tasks.weekly_task"],
"monthly": ["myapp.tasks.monthly_task"],
# Long queue events (for heavy processing)
"hourly_long": ["myapp.tasks.hourly_heavy"],
"daily_long": ["myapp.tasks.daily_heavy"],
"weekly_long": ["myapp.tasks.weekly_heavy"],
"monthly_long": ["myapp.tasks.monthly_heavy"],
# Cron events (croniter-compatible syntax)
"cron": {
"*/15 * * * *": ["myapp.tasks.every_15_min"],
"0 9 * * 1-5": ["myapp.tasks.weekday_9am"],
"0 0 1 * *": ["myapp.tasks.first_of_month"],
}
}
CRITICAL: ALWAYS run bench migrate after ANY change to scheduler_events. Without it, changes are NOT applied.
| Event | Frequency | Queue | Use Case |
|-------|-----------|-------|----------|
| all | Every tick [v14: 4min, v15+: 60s] | default | Frequent polling |
| hourly | Once per hour | default | Sync, cleanup |
| daily | Once per day | default | Reports, summaries |
| weekly | Once per week | default | Archival |
| monthly | Once per month | default | Billing, statements |
| hourly_long | Once per hour | long | Heavy sync |
| daily_long | Once per day | long | Large exports |
| weekly_long | Once per week | long | Data warehousing |
| monthly_long | Once per month | long | Annual reports |
| cron | Custom schedule | configurable | Any custom timing |
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *
| Symbol | Meaning | Example |
|--------|---------|---------|
| * | Any value | * * * * * = every minute |
| , | List | 1,15 * * * * = minute 1 and 15 |
| - | Range | 0 9-17 * * * = hours 9 through 17 |
| / | Interval | */10 * * * * = every 10 minutes |
Common patterns:
*/5 * * * *0 9 * * 1-50 8 * * 10 9-17 * * 1-5frappe.enqueue(
method, # REQUIRED: function or "dotted.module.path"
queue="default", # "short", "default", "long", or custom
timeout=None, # Override queue timeout (seconds)
is_async=True, # False = run synchronously (skip worker)
now=False, # True = run via frappe.call() directly
job_id=None, # [v15+] Unique ID for deduplication
enqueue_after_commit=False, # Wait for DB commit before enqueue
at_front=False, # Place at front of queue
on_success=None, # Success callback
on_failure=None, # Failure callback
**kwargs # Arguments passed to method
)
| Queue | Default Timeout | Use When |
|-------|-----------------|----------|
| short | 300s (5 min) | Task < 30 seconds |
| default | 300s (5 min) | Task 30s - 5 min |
| long | 1500s (25 min) | Task 5 - 25 min |
| long + custom timeout | user-defined | Task > 25 min |
# Short queue — quick status update
frappe.enqueue("myapp.tasks.update_status", queue="short", doc=doc.name)
# Long queue — heavy report generation
frappe.enqueue("myapp.tasks.generate_report", queue="long", timeout=3600)
Enqueue a controller method on a specific document.
frappe.enqueue_doc(
"Sales Invoice", # DocType
"SINV-00001", # Document name
"send_notification", # Controller method name
queue="long",
timeout=600,
recipient="[email protected]" # kwargs passed to method
)
The controller method MUST be decorated with @frappe.whitelist():
class SalesInvoice(Document):
@frappe.whitelist()
def send_notification(self, recipient):
# self is the loaded document
pass
Alternative from within a controller:
class SalesOrder(Document):
def on_submit(self):
self.queue_action("send_emails", emails=email_list)
def send_emails(self, emails):
for email in emails:
send_mail(email)
from frappe.utils.background_jobs import is_job_enqueued
job_id = f"import::{doc.name}"
if not is_job_enqueued(job_id):
frappe.enqueue(
"myapp.tasks.import_data",
job_id=job_id,
doc_name=doc.name
)
else:
frappe.msgprint("Import already in progress")
from frappe.core.page.background_jobs.background_jobs import get_info
enqueued = [d.get("job_name") for d in get_info()]
if name not in enqueued:
frappe.enqueue(..., job_name=name)
ALWAYS use try/except with commit/rollback per record in batch jobs:
def process_records(records):
success, errors = 0, 0
for record in records:
try:
process_single(record)
frappe.db.commit()
success += 1
except Exception:
frappe.db.rollback()
frappe.log_error(
frappe.get_traceback(),
f"Process Error: {record}"
)
errors += 1
return {"success": success, "errors": errors}
def task_with_retry(data, retry_count=0, max_retries=3):
try:
external_api_call(data)
except Exception:
if retry_count < max_retries:
frappe.enqueue(
"myapp.tasks.task_with_retry",
queue="default",
data=data,
retry_count=retry_count + 1,
max_retries=max_retries,
enqueue_after_commit=True
)
frappe.log_error(f"Retry {retry_count+1}/{max_retries}", "Task Retry")
else:
frappe.log_error(frappe.get_traceback(), f"Failed after {max_retries} retries")
raise
def on_success_handler(job, connection, result, *args, **kwargs):
frappe.publish_realtime("show_alert", {"message": "Done!"})
def on_failure_handler(job, connection, type, value, traceback):
frappe.log_error(f"Job {job.id} failed: {value}", "Job Error")
frappe.enqueue(
"myapp.tasks.risky_task",
on_success=on_success_handler,
on_failure=on_failure_handler,
)
def long_task(items, user):
total = len(items)
for i, item in enumerate(items):
process_item(item)
frappe.publish_realtime(
"task_progress",
{"progress": (i + 1) / total * 100, "current": i + 1, "total": total},
user=user,
)
CRITICAL: Scheduler jobs run as Administrator. ALWAYS set explicit ownership when creating documents:
def scheduled_task():
doc = frappe.new_doc("ToDo")
doc.owner = "[email protected]"
doc.insert(ignore_permissions=True)
| Tool | Purpose |
|------|---------|
| bench doctor | Scheduler status, worker health |
| RQ Worker (DocType) | Worker status: busy/idle |
| RQ Job (DocType) | Job status, queue filtering |
| Scheduled Job Log (DocType) | Execution history, errors |
| logs/worker.error.log | Worker exceptions |
| logs/scheduler.log | Scheduler activity |
| Feature | v14 | v15+ |
|---------|-----|------|
| Tick interval (all event) | ~240s (4 min) | ~60s |
| Config key for tick | scheduler_interval | scheduler_tick_interval |
| Deduplication | job_name (deprecated) | job_id + is_job_enqueued() |
Custom tick in common_site_config.json:
{ "scheduler_tick_interval": 120 }
bench migrate after any scheduler_events change in hooks.pyjob_id + is_job_enqueued() for deduplication [v15+]job_name for deduplication in new code (v14 legacy)frappe-syntax-hooks — Full hooks.py referencefrappe-core-background — Background job architecturefrappe-errors-jobs — Job failure debuggingIntegration with protocols.io API for managing scientific protocols. This skill should be used when working with protocols.io to search, create, update, or publish protocols; manage protocol steps and materials; handle discussions and comments; organize workspaces; upload and manage files; or integrate protocols.io functionality into workflows. Applicable for protocol discovery, collaborative protocol development, experiment tracking, lab protocol management, and scientific documentation.
Analyzes job descriptions and generates tailored resumes that highlight relevant experience, skills, and achievements to maximize interview chances
Generate Excalidraw diagrams from natural language descriptions. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", or "generate an Excalidraw file". Supports flowcharts, relationship diagrams, mind maps, and system architecture diagrams. Outputs .excalidraw JSON files that can be opened directly in Excalidraw.
Build and distribute Expo development clients locally or via TestFlight
Use when you have a written implementation plan to execute in a separate session with review checkpoints
Data structure for annotated matrices in single-cell analysis. Use when working with .h5ad files or integrating with the scverse ecosystem. This is the data format skill—for analysis workflows use scanpy; for probabilistic models use scvi-tools; for population-scale queries use cellxgene-census.
Benchling R&D platform integration. Access registry (DNA, proteins), inventory, ELN entries, workflows via API, build Benchling Apps, query Data Warehouse, for lab data management automation.
Comprehensive molecular biology toolkit. Use for sequence manipulation, file parsing (FASTA/GenBank/PDB), phylogenetics, and programmatic NCBI/PubMed access (Bio.Entrez). Best for batch processing, custom bioinformatics pipelines, BLAST automation. For quick lookups use gget; for multi-service integration use bioservices.
Take impertio-studio/frappe-syntax-scheduler from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.