Skip to main content

Overview

Hooks are the primary way to customize and extend Frappe Helpdesk without modifying core code. All hooks are defined in helpdesk/hooks.py.

Available Hooks

App Configuration Hooks

# Basic app information
app_name = "helpdesk"
app_title = "Helpdesk"
app_publisher = "Frappe Technologies"
app_description = "Customer Service Software"
app_icon = "octicon octicon-file-directory"
app_color = "grey"
app_email = "[email protected]"
app_license = "AGPLv3"

# Required apps
required_apps = ["telephony"]

# Type annotations requirement
require_type_annotated_api_methods = True

Document Event Hooks

Execute custom code when documents are created, updated, or deleted.
# In helpdesk/hooks.py
doc_events = {
    "Contact": {
        "before_insert": "helpdesk.overrides.contact.before_insert",
    },
    "Assignment Rule": {
        "on_trash": "helpdesk.extends.assignment_rule.on_assignment_rule_trash",
        "validate": "helpdesk.extends.assignment_rule.on_assignment_rule_validate",
    },
}

Example: Contact Override (from overrides/contact.py)

import frappe

def before_insert(doc, event):
    """
    Custom logic before a Contact is inserted
    
    :param doc: Contact document instance
    :param event: Event name (not used)
    """
    # Add custom validation
    if not doc.email_id:
        frappe.throw("Email is required for Helpdesk contacts")
    
    # Set default values
    if not doc.status:
        doc.status = "Active"
    
    # Create related records
    create_customer_if_not_exists(doc.email_id)

Example: Assignment Rule Validation (from extends/assignment_rule.py)

import frappe
from frappe import _
from helpdesk.utils import is_json_valid

def on_assignment_rule_trash(doc, event):
    """Prevent deletion of last assignment rule"""
    if not frappe.get_all(
        "Assignment Rule",
        filters={"document_type": "HD Ticket", "name": ["!=", doc.name]},
    ):
        frappe.throw("There should at least be 1 assignment rule for ticket")

def on_assignment_rule_validate(doc, event):
    """Validate assignment rule conditions"""
    if doc.assign_condition_json and not is_json_valid(doc.assign_condition_json):
        frappe.throw(
            _("The Assign Condition JSON '{0}' is invalid").format(
                doc.assign_condition_json
            )
        )

All Document Events

doc_events = {
    "DocType Name": {
        # Before hooks
        "before_insert": "module.path.function",
        "before_validate": "module.path.function",
        "before_save": "module.path.function",
        "before_submit": "module.path.function",
        "before_cancel": "module.path.function",
        
        # Validation
        "validate": "module.path.function",
        
        # After hooks
        "after_insert": "module.path.function",
        "on_update": "module.path.function",
        "on_submit": "module.path.function",
        "on_cancel": "module.path.function",
        "on_trash": "module.path.function",
        
        # Special hooks
        "on_change": "module.path.function",
        "before_rename": "module.path.function",
        "after_rename": "module.path.function",
    },
}

Permission Hooks

Customize document-level permissions.
# In helpdesk/hooks.py
has_permission = {
    "HD Ticket": "helpdesk.helpdesk.doctype.hd_ticket.hd_ticket.has_permission",
    "HD Saved Reply": "helpdesk.helpdesk.doctype.hd_saved_reply.hd_saved_reply.has_permission",
}

permission_query_conditions = {
    "HD Ticket": "helpdesk.helpdesk.doctype.hd_ticket.hd_ticket.permission_query",
    "HD Saved Reply": "helpdesk.helpdesk.doctype.hd_saved_reply.hd_saved_reply.permission_query",
}
See DocTypes documentation for implementation examples.

Scheduler Hooks

Run background tasks on a schedule.
# In helpdesk/hooks.py
scheduler_events = {
    "all": [
        "helpdesk.search.build_index_if_not_exists",
        "helpdesk.search.download_corpus",
    ],
    "daily": [
        "helpdesk.helpdesk.doctype.hd_ticket.hd_ticket.close_tickets_after_n_days"
    ],
    "hourly": [
        "helpdesk.tasks.send_hourly_digest",
    ],
    "weekly": [
        "helpdesk.tasks.cleanup_old_data",
    ],
    "monthly": [
        "helpdesk.tasks.generate_monthly_report",
    ],
}

Scheduler Frequencies

  • all - Every 5 minutes
  • hourly - Every hour
  • daily - Once per day
  • weekly - Once per week
  • monthly - Once per month
  • cron - Custom cron expression

Example: Scheduled Task

# In helpdesk/tasks.py
import frappe
from frappe.utils import add_days, nowdate

def close_tickets_after_n_days():
    """
    Close tickets that have been resolved for N days
    """
    days = frappe.db.get_single_value("HD Settings", "auto_close_after_days")
    
    if not days:
        return
    
    cutoff_date = add_days(nowdate(), -days)
    
    tickets = frappe.get_all(
        "HD Ticket",
        filters={
            "status": "Resolved",
            "resolution_date": ["<", cutoff_date],
        },
        pluck="name"
    )
    
    for ticket_id in tickets:
        ticket = frappe.get_doc("HD Ticket", ticket_id)
        ticket.status = "Closed"
        ticket.add_comment("Info", "Automatically closed after {0} days".format(days))
        ticket.save()
    
    frappe.db.commit()

Lifecycle Hooks

Run code at specific application lifecycle events.
# In helpdesk/hooks.py
after_install = "helpdesk.setup.install.after_install"
after_migrate = [
    "helpdesk.search.build_index_in_background",
    "helpdesk.search.download_corpus",
]
before_tests = "helpdesk.test_utils.before_tests"

Example: After Install Hook

# In helpdesk/setup/install.py
import frappe

def after_install():
    """
    Run after Helpdesk app is installed
    """
    create_default_settings()
    create_default_email_templates()
    create_default_assignment_rules()
    setup_roles_and_permissions()

def create_default_settings():
    """Create default settings document"""
    if not frappe.db.exists("HD Settings", "HD Settings"):
        settings = frappe.get_doc({
            "doctype": "HD Settings",
            "default_ticket_status": "Open",
            "auto_close_after_days": 7,
        })
        settings.insert()

Override DocType Class

Replace standard Frappe DocTypes with custom implementations.
# In helpdesk/hooks.py
override_doctype_class = {
    "Email Account": "helpdesk.overrides.email_account.CustomEmailAccount",
}

Example: Custom Email Account (from overrides/email_account.py)

from frappe.email.doctype.email_account.email_account import EmailAccount
import frappe
from frappe import _

class CustomEmailAccount(EmailAccount):
    """Custom Email Account with auto-generated email filtering"""
    
    def get_inbound_mails(self) -> list:
        """Override to filter out auto-generated emails"""
        mails = []
        
        def process_mail(messages, append_to=None):
            for index, message in enumerate(messages.get("latest_messages", [])):
                try:
                    _msg = message_from_string(
                        message.decode("utf-8", errors="replace")
                    )
                    
                    # Important: Filter auto-generated emails
                    if _msg.get("X-Auto-Generated"):
                        continue
                    
                    # Process the email
                    # ... rest of the implementation
                except Exception as e:
                    frappe.log_error(
                        title=_("Error processing email at index {0}").format(index),
                        message=frappe.get_traceback(),
                    )
        
        # ... rest of the implementation
        return mails

Website Route Hooks

# In helpdesk/hooks.py
website_route_rules = [
    {
        "from_route": "/helpdesk/<path:app_path>",
        "to_route": "helpdesk",
    },
]

Full-Text Search Hooks

# In helpdesk/hooks.py
sqlite_search = ["helpdesk.search_sqlite.HelpdeskSearch"]

Authentication Hooks

# In helpdesk/hooks.py
auth_hooks = ["helpdesk.auth.authenticate"]

User Invitation Hooks

# In helpdesk/hooks.py
user_invitation = {
    "allowed_roles": {
        "Agent Manager": ["Agent", "Agent Manager"],
        "System Manager": ["Agent", "Agent Manager", "System Manager"],
    },
    "after_accept": "helpdesk.helpdesk.hooks.user_invitation.after_accept",
}
Prevent deletion errors for specified DocTypes.
# In helpdesk/hooks.py
ignore_links_on_delete = [
    "HD Notification",
    "HD Ticket Comment",
]

Setup Wizard Hooks

# In helpdesk/hooks.py
setup_wizard_complete = "helpdesk.setup.setup_wizard.setup_complete"

Creating Custom Hooks

1. Global Document Hook

Apply to all documents:
# In helpdesk/hooks.py
doc_events = {
    "*": {
        "before_insert": "helpdesk.utils.audit.log_document_creation",
        "on_trash": "helpdesk.utils.audit.log_document_deletion",
    },
}
# In helpdesk/utils/audit.py
import frappe

def log_document_creation(doc, event):
    """Log all document creations"""
    frappe.get_doc({
        "doctype": "Audit Log",
        "action": "Create",
        "document_type": doc.doctype,
        "document_name": doc.name,
        "user": frappe.session.user,
    }).insert(ignore_permissions=True)

def log_document_deletion(doc, event):
    """Log all document deletions"""
    frappe.get_doc({
        "doctype": "Audit Log",
        "action": "Delete",
        "document_type": doc.doctype,
        "document_name": doc.name,
        "user": frappe.session.user,
    }).insert(ignore_permissions=True)

2. Custom Scheduler Event

# In helpdesk/hooks.py
scheduler_events = {
    "cron": {
        "0 9 * * MON": [
            "helpdesk.tasks.send_weekly_digest",
        ],
    },
}
# In helpdesk/tasks.py
import frappe
from frappe.utils import add_days, nowdate, get_weekday

def send_weekly_digest():
    """
    Send weekly digest every Monday at 9 AM
    """
    agents = frappe.get_all("HD Agent", filters={"is_active": 1}, pluck="user")
    
    for agent in agents:
        # Generate digest content
        stats = get_agent_weekly_stats(agent)
        
        # Send email
        frappe.sendmail(
            recipients=[agent],
            subject="Your Weekly Helpdesk Digest",
            template="weekly_digest",
            args=stats,
        )

def get_agent_weekly_stats(agent: str) -> dict:
    """Get weekly statistics for an agent"""
    start_date = add_days(nowdate(), -7)
    
    return {
        "tickets_closed": frappe.db.count(
            "HD Ticket",
            {"_assign": ["like", f"%{agent}%"], "status": "Closed", "modified": [">=", start_date]}
        ),
        "avg_response_time": get_avg_response_time(agent, start_date),
    }

3. Custom Fixture

Load data during tests:
# In helpdesk/hooks.py
fixtures = [
    {"dt": "Custom Field", "filters": [["dt", "in", ["HD Ticket", "Contact"]]]},
    {"dt": "Role", "filters": [["name", "in", ["Agent", "Agent Manager"]]]},
]

Best Practices

  1. Use Extends/Overrides: Place custom logic in helpdesk/extends/ or helpdesk/overrides/
  2. Error Handling: Always use try-except in hooks to prevent breaking core functionality
  3. Permissions: Check permissions before executing sensitive operations
  4. Performance: Keep hooks lightweight; use background jobs for heavy operations
  5. Logging: Log errors with frappe.log_error() for debugging
  6. Testing: Test all custom hooks thoroughly
  7. Documentation: Document custom hooks for maintainability
  8. Transactions: Be careful with frappe.db.commit() in hooks
  9. Global Hooks: Use * sparingly as it affects all documents
  10. Versioning: Consider backward compatibility when modifying hooks

Debugging Hooks

def my_hook(doc, event):
    """Debug hook execution"""
    frappe.log_error(
        title=f"Hook Debug: {doc.doctype}",
        message=frappe.as_json({
            "event": event,
            "doc": doc.as_dict(),
            "user": frappe.session.user,
        })
    )
View logs:
bench --site helpdesk.test logs

Testing Hooks

Test hook execution:
import frappe
import unittest

class TestCustomHooks(unittest.TestCase):
    def test_contact_before_insert(self):
        """Test contact before insert hook"""
        contact = frappe.get_doc({
            "doctype": "Contact",
            "first_name": "Test",
            "email_id": "[email protected]",
        })
        
        contact.insert()
        
        # Verify hook executed
        self.assertEqual(contact.status, "Active")
Run tests:
bench --site helpdesk.test run-tests --module helpdesk.tests.test_hooks

Build docs developers (and LLMs) love