Frappe Syntax Scheduler logo

Frappe Syntax Scheduler

Organization
Impertio-Studio
frappe-syntax-scheduler

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 monitoring. Keywords: scheduler, background job, cron, RQ worker, job queue, async task, frappe.enqueue, scheduled task, cron syntax, how often does it run, background job example, enqueue example.

Overview

PublisherImpertio-Studio
RepositoryFrappe_Claude_Skill_Package
Skill namefrappe-syntax-scheduler
Stars
180
Forks
53
Bundled files
12
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 12 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by Impertio-Studio on GitHub. Read the source before you install it.

Installation

Install the Frappe Syntax Scheduler AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package.git /tmp/Frappe_Claude_Skill_Package
mkdir -p .claude/skills
cp -r /tmp/Frappe_Claude_Skill_Package/skills/source/syntax/frappe-syntax-scheduler .claude/skills/frappe-syntax-scheduler
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Frappe Syntax Scheduler in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Frappe Syntax Scheduler on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Frappe Syntax Scheduler is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Frappe Scheduler & Background Jobs

Deterministic syntax reference for Frappe scheduler events and background job processing via Redis Queue (RQ).

Decision Tree

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()

Quick Reference: Scheduler Events (hooks.py)

python
# 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.

Scheduler Event Types

EventFrequencyQueueUse Case
allEvery tick [v14: 4min, v15+: 60s]defaultFrequent polling
hourlyOnce per hourdefaultSync, cleanup
dailyOnce per daydefaultReports, summaries
weeklyOnce per weekdefaultArchival
monthlyOnce per monthdefaultBilling, statements
hourly_longOnce per hourlongHeavy sync
daily_longOnce per daylongLarge exports
weekly_longOnce per weeklongData warehousing
monthly_longOnce per monthlongAnnual reports
cronCustom scheduleconfigurableAny custom timing

Cron Syntax

┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *
SymbolMeaningExample
*Any value* * * * * = every minute
,List1,15 * * * * = minute 1 and 15
-Range0 9-17 * * * = hours 9 through 17
/Interval*/10 * * * * = every 10 minutes

Common patterns:

  • Every 5 min: */5 * * * *
  • Weekdays at 9:00: 0 9 * * 1-5
  • Monday at 8:00: 0 8 * * 1
  • Business hours hourly: 0 9-17 * * 1-5

Quick Reference: frappe.enqueue()

python
frappe.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 Types

QueueDefault TimeoutUse When
short300s (5 min)Task < 30 seconds
default300s (5 min)Task 30s - 5 min
long1500s (25 min)Task 5 - 25 min
long + custom timeoutuser-definedTask > 25 min
python
# 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)

frappe.enqueue_doc()

Enqueue a controller method on a specific document.

python
frappe.enqueue_doc(
    "Sales Invoice",              # DocType
    "SINV-00001",                 # Document name
    "send_notification",          # Controller method name
    queue="long",
    timeout=600,
    recipient="user@example.com"  # kwargs passed to method
)

The controller method MUST be decorated with @frappe.whitelist():

python
class SalesInvoice(Document):
    @frappe.whitelist()
    def send_notification(self, recipient):
        # self is the loaded document
        pass

self.queue_action()

Alternative from within a controller:

python
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)

Job Deduplication

[v15+] Recommended Pattern

python
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")

[v14] Legacy Pattern (NEVER use in new code)

python
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)

Error Handling Pattern

ALWAYS use try/except with commit/rollback per record in batch jobs:

python
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}

Retry Pattern

python
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

Callbacks

python
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,
)

Progress Updates

python
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,
        )

User Context

CRITICAL: Scheduler jobs run as Administrator. ALWAYS set explicit ownership when creating documents:

python
def scheduled_task():
    doc = frappe.new_doc("ToDo")
    doc.owner = "user@example.com"
    doc.insert(ignore_permissions=True)

Monitoring

ToolPurpose
bench doctorScheduler 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.logWorker exceptions
logs/scheduler.logScheduler activity

Version Differences

Featurev14v15+
Tick interval (all event)~240s (4 min)~60s
Config key for tickscheduler_intervalscheduler_tick_interval
Deduplicationjob_name (deprecated)job_id + is_job_enqueued()

Custom tick in common_site_config.json:

json
{ "scheduler_tick_interval": 120 }

Critical Rules

  1. ALWAYS run bench migrate after any scheduler_events change in hooks.py
  2. ALWAYS use job_id + is_job_enqueued() for deduplication [v15+]
  3. ALWAYS choose the correct queue: short/default/long based on task duration
  4. ALWAYS commit per record and rollback on error in batch jobs
  5. ALWAYS remember that scheduler jobs run as Administrator
  6. NEVER run heavy logic directly in a scheduler event — enqueue it instead
  7. NEVER use job_name for deduplication in new code (v14 legacy)

Reference Files

See Also

  • frappe-syntax-hooks — Full hooks.py reference
  • frappe-core-background — Background job architecture
  • frappe-errors-jobs — Job failure debugging

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Frappe Syntax Scheduler AI skill do?

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 monitoring. Keywords: scheduler, background job, cron, RQ worker, job queue, async task, frappe.enqueue, scheduled task, cron syntax, how often does it run, background job example, enqueue example.

Why use Frappe Syntax Scheduler on TypingMind?

Because you install it once and use it with any model. Frappe Syntax Scheduler is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Frappe Syntax Scheduler in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package/tree/main/skills/source/syntax/frappe-syntax-scheduler. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Frappe Syntax Scheduler?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Frappe Syntax Scheduler?

As many as you like. As long as a model supports skills, you can use Frappe Syntax Scheduler with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Frappe Syntax Scheduler AI skill free?

Yes. It is published on GitHub by Impertio-Studio under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇