Extensions for Frappe CRM that add features without forking the core app.
- Global search bar — Cmd/Ctrl+K modal backed by
rtcamp/frappe_search(optional). - Extensible sidebar — any installed Frappe app can inject list views, routes, groups, or separators into the CRM sidebar via the
crm_sidebarhook. - Events tab — calendar events tab injected into every Lead and Deal page; create, edit, duplicate, and delete Frappe
Eventrecords linked to the record. - Event notifications — scheduler sends in-browser realtime alerts and optional emails to event owners and participants before their events.
- Deal inactivity Slack digest — daily scheduler posts a single Slack digest of CRM Deals with no activity (email, note, task, comment, or field edit) for exactly N days; entirely authored from a desk-editable
Notification. - Gmail thread activities — activity entries on Lead/Deal records resolve Gmail threads via
rtcamp/frappe_gmail_thread(optional). - Address management (Deal only) — add, create, link, and unlink
Addressrecords directly from Deal forms via an inline HTML panel. - Follow button (eye icon) — injected into the Lead/Deal header icon row. Toggles
Document Followfor the current user; filled eye = following, outline eye = not following. - Quotation auto-items — opening a
Quotationfrom a CRM Deal's "Create Quotation" link auto-populates items, customer, currency, and missing values from the source Deal. - Public Lead intake API — guest-allowed
POSTendpoints for creating CRM Leads and uploading files against them; intended for website / external form integrations. - Gmail Add-on backend — whitelisted endpoints powering the Gmail sidebar Add-on (contact lookup by email, linked Leads / Deals, latest activity).
- Bundled fixtures — Lead & Deal field layouts, Property Setters, Custom Fields, CRM Form Scripts, and a curated set of CRM View Settings ship as fixtures and install automatically. (Optional)
- Project creation on Won — when a Deal is marked Won, dialogs guide the user through updating MSA & Insurance details on the linked Customer and creating an ERPNext
Projectpre-filled from the deal.
Frappe CRM XT ships a Vite-built Vue 3 IIFE bundle (crm_xt_app.js) that is loaded into the Frappe CRM SPA alongside the standard Frappe assets. The bundle:
- mounts a minimal
App.vueroot component on a#crm-xt-appdiv injected into the page - uses
MutationObserverto watch the CRM SPA's DOM and inject the sidebar, custom routes, and the Events tab without touching any FCRM source files - ships its own copy of Vue 3 and frappe-ui, so all components use the same design tokens and Tailwind classes as native CRM views
| Component | Purpose |
|---|---|
App.vue |
Root — sidebar injection, route registration, Events tab injection, global search trigger |
ExtListView.vue |
Full-featured list view (frappe-ui ListView, filters, sort, column picker, pagination) |
InjectedEventsTab.vue |
Events tab timeline and empty state |
InjectedEventModal.vue |
Create / edit / delete / duplicate Event dialog |
SearchDialog.vue |
Cmd/Ctrl+K global search modal |
EventNotifications.vue |
Per-event notification rules editor |
ListFilterLocal.vue |
Ad-hoc filter panel used inside ExtListView |
Attendee.vue |
Email-tag input for event attendees |
UserAvatar.vue |
Resolved user avatar with fallback initials |
| Module | Purpose |
|---|---|
api/activity.py |
Overrides crm.api.activities.get_activities; appends Gmail thread entries |
api/event.py |
get_doc_events, create/save/delete helpers, notification scheduler tasks |
api/follow.py |
Document Follow toggle endpoint |
api/contact.py |
Gmail Add-on contact lookup endpoints |
api/quotation.py |
Quotation auto-populate helper |
api/search.py |
Global search backend (frappe_search or built-in fallback) |
api/sidebar.py |
Reads crm_sidebar hooks from all installed apps and returns merged item list |
api/deal_inactivity.py |
Daily "deal gone quiet" Slack digest scheduler |
Press Cmd+K (macOS) or Ctrl+K (Linux/Windows) anywhere in Frappe CRM to open a full-screen search modal.
Two backends are tried in order:
| Priority | Backend | Notes |
|---|---|---|
| 1 | rtcamp/frappe_search |
Full-text search with <mark> highlighting. Used when installed. |
| 2 | Frappe built-in global search | frappe.utils.global_search — always available, no extra install. |
Every Lead and Deal page gains an Events tab injected at the right end of the tab bar.
Capabilities:
- Timeline feed of all
Eventrecords linked to the current Lead/Deal viaEvent Participants - Create a new event — title, date/time or all-day, color, attendees (email tags), visibility, location, description
- Edit an existing event — all fields editable; changes saved via
frappe.client.save - Delete an event (with confirmation)
- Duplicate an event
- Color-coded accent bar per event card; color picker shows the active selection
- Participant avatar stack on each card (up to 3 + overflow count)
Events are stored as standard Frappe Event documents and also appear in the Frappe Desk Calendar (/app/event).
Permissions note: Events are fetched via frappe_crm_xt.api.event.get_doc_events, which bypasses the child-doctype read permission issue that prevents direct frappe.client.get_list calls on Event Participants.
Scheduler tasks fire at every interval and send notifications to event owners and participants ahead of their events.
Per-event rules can be configured in the Event Notifications child table on each Event record. If no per-event rules exist, global defaults from FCRM Settings are applied.
| Setting | Description |
|---|---|
event_notifications |
Rules for timed events |
all_day_event_notifications |
Rules for all-day events |
Each rule specifies: type (Notification / Email), before (number), interval (minutes / hours / days / weeks), and optionally time (for all-day events).
A daily scheduler (cron 09:00, site time zone) posts a single Slack digest listing every CRM Deal that has had no activity for exactly INACTIVE_DAYS (7) days — one nudge per streak, on the day the gap crosses the threshold. Won/Lost deals are excluded.
"Activity" is the most recent of all of the following — a note/task/comment does not bump deal.modified, so each is checked directly:
| Source | Field / record |
|---|---|
| Status / any deal field edit | deal.modified |
| Incoming email | deal.custom_last_incoming_email_time |
| Outgoing email / reply | deal.last_responded_on (Gmail-synced) and/or deal.custom_last_responded_on (migrated) |
| Notes | FCRM Note |
| Tasks | CRM Task |
| Comments | Comment |
Everything about the alert is authored from a desk-editable Notification (CRM Slack — CRM Deal Inactivity (7 days)) — no code change is needed to restyle or re-scope it:
| Notification field | Role in the digest |
|---|---|
enabled |
Feature on/off switch (the scheduler no-ops when disabled) |
subject |
Digest header — rendered with {{ count }} and {{ days }} |
message |
Per-deal block — rendered with {{ doc }} (the deal) |
| Condition / Filters | Extra per-deal scoping, honored before sending |
| Slack Webhook URL | Target Slack channel |
Dividers via Markdown comments: any <!-- ... --> comment in the message is stripped from the Slack output (like a Markdown comment); the inner text of the first comment becomes the divider between deal blocks (\n is interpreted as a newline). No comment → blocks are separated by a blank line.
Scales to large datasets: instead of scanning every stale deal, the job fetches only deals touched on the target day across all activity sources, so cost stays flat even with 100k+ deals.
The Notification is seeded (disabled) by the create_crm_slack_notifications patch, alongside two event-driven CRM → Slack notifications:
| Notification | Trigger |
|---|---|
| CRM Slack — CRM Deal Inactivity (7 days) | Daily scheduler (this feature) |
| CRM Slack — CRM Deal Note Added | New FCRM Note on a Deal |
| CRM Slack — CRM Deal Updates | Deal saved (title / status / value / owner / stage change) |
All three ship disabled. Point each at a Slack Webhook URL (core Frappe integration) and toggle Enabled from the desk to activate — the webhook holds a secret, so it is never shipped in the app.
Every CRM Deal form gains a custom Addresses HTML panel (not shown on CRM Lead). It renders all Address records linked to the Deal via Dynamic Link.
Capabilities:
- List all linked addresses with collapsible full-address detail
- Create a new address and automatically link it to the record
- Link an existing
Addressrecord by searching and selecting it - Unlink an address (removes the Dynamic Link entry; does not delete the Address document)
- Edit — each card has a direct link to the address's Frappe Desk form
An eye icon is injected into the Lead/Deal header icon row:
- Filled eye — you are currently following this record
- Outline eye — you are not following this record
Clicking the button calls the frappe_crm_xt.api.follow.toggle_follow endpoint and auto-enables track_changes on the doctype if needed.
When a CRM Deal's status is set to Won, two sequential dialogs are shown automatically:
- MSA & Insurance Details — pre-filled from the linked ERPNext
Customer; saves MSA start/end date, document link, insurance requested flag, insurance start/end date, and insurance document link back to the Customer. - Create Project — collects Project Manager, Territory, Billing Type, Customer, Currency, Estimated Hours, Service Type, Opportunity Amount, and Project Type; creates an ERPNext
Projectand opens it in a new tab.
If a project matching the deal already exists, the creation dialog is skipped and an Open Project action is added to the Deal's action menu instead.
Customer custom fields required (shipped as fixtures in custom_field.json):
| Field | Type | Description |
|---|---|---|
custom_msa_start_date |
Date | MSA start date |
custom_msa_end_date |
Date | MSA end date |
custom_msa_document_link |
Data | Link or path to MSA document |
custom_insurance_requested |
Check | Whether insurance was requested |
custom_insurance_start_date |
Date | Insurance start date |
custom_insurance_end_date |
Date | Insurance end date |
custom_insurance_document_link |
Data | Link or path to insurance document |
Any installed Frappe app can add items to the CRM sidebar by defining a crm_sidebar list in its hooks.py. Items appear below the built-in "Call Logs" entry.
Four item types are supported:
type |
Description |
|---|---|
"list_view" |
Opens the built-in list view at /xt/list/<doctype> |
"route" |
Navigates to an arbitrary URL (internal or external) |
"separator" |
Renders a horizontal divider line |
"group" |
Collapsible section; child items live in the "items" key (one level deep) |
# hooks.py (in any installed Frappe app)
crm_sidebar = [
{
# ── Required ──────────────────────────────────────────────────
"label": "Purchase Orders", # Sidebar display label
"type": "list_view", # "list_view" | "route" | "separator" | "group"
# ── Required for type == "list_view" ──────────────────────────
"doctype": "Purchase Order",
# ── Required for type == "route" ──────────────────────────────
# "url": "/app/purchase-order",
# ── Optional ──────────────────────────────────────────────────
"icon": "shopping-cart", # Lucide icon name (lucide.dev/icons)
# Filters shown in the filter UI — users can see and remove them
"default_filters": {
"status": ["=", "To Receive and Bill"],
},
# Filters always applied but never shown in the filter UI
"hidden_filters": {
"company": ["=", "My Company"],
},
# Columns to display (overrides DocType's in_list_view fields)
"fields": ["supplier", "transaction_date", "status", "grand_total"],
# Initial sort
"default_sort": {"field": "transaction_date", "dir": "desc"},
# Fieldname for the toolbar quick-search input
# Type-aware: Link → autocomplete, Select/Check → dropdown,
# Date/Datetime → date picker, everything else → text
"search_field": "supplier",
# URL template opened on row click; {name} is replaced with the record name
# Defaults to /app/<doctype-slug>/<name>
"row_url": "/desk/query-report/{name}",
},
{"type": "separator"},
{
"label": "Procurement",
"type": "group",
"icon": "package",
"items": [
{
"label": "Suppliers",
"type": "list_view",
"doctype": "Supplier",
"icon": "building",
},
{
"label": "Support Portal",
"type": "route",
"url": "https://support.example.com",
"icon": "external-link",
},
],
},
]| Key | Type | Required | Description |
|---|---|---|---|
label |
str |
✅ | Sidebar display text |
type |
str |
✅ | "list_view", "route", "separator", or "group" |
doctype |
str |
list_view only |
Frappe DocType to display |
url |
str |
route only |
Destination URL |
icon |
str |
— | Lucide icon name (default: "list") |
default_filters |
dict |
— | Pre-applied filters; users can see and clear them |
hidden_filters |
dict |
— | Always-applied filters; never shown in the UI |
fields |
list[str] |
— | Column order override; title and Modified are always included |
default_sort |
dict |
— | {"field": "<fieldname>", "dir": "asc"|"desc"} |
search_field |
str |
— | Fieldname for the toolbar quick-search input |
row_url |
str |
— | Row-click URL template; {name} is replaced with the record name |
items |
list |
group only |
Child items (same schema, one level deep) |
The built-in list view rendered for every "list_view" sidebar item includes:
- Column picker — toggle any field from the DocType on or off; selection persisted per-doctype in
localStorage - Filter panel — add, edit, and remove ad-hoc filters across any filterable field
- Hidden filters —
hidden_filtersfrom the hook are silently merged into every query; they do not appear in the filter panel - Type-aware quick search — toolbar input whose component matches the
search_fieldtype:Link→ autocomplete (loads options from the linked DocType)Select/Check→ dropdownDate/Datetime→ date picker- Everything else → text input (
like %value%)
- Sort — click any column header to toggle ascending/descending sort
- Load More — incremental pagination (20 rows per page)
- Custom row URL —
row_urltemplate determines row-click destination; defaults to the standard Frappe form view - Permission-safe — if the user lacks read permission for the doctype, the list shows an empty state instead of crashing
| Package | Required |
|---|---|
crm (Frappe CRM) |
✅ |
rtcamp/frappe_search |
Optional — enables Cmd/Ctrl+K global search |
rtcamp/frappe_gmail_thread |
Optional — enables Gmail thread activity entries on Lead/Deal |
erpnext |
Optional — required for Project creation on Won Deal and Customer MSA/Insurance fields |
bench get-app frappe_crm_xt
bench --site <your-site> install-app frappe_crm_xt
bench build --app frappe_crm_xt
bench --site <your-site> restart# Install pre-commit hooks (runs ruff, oxlint, prettier, eslint on commit)
pre-commit install
# Frontend watch build (Vite IIFE bundle)
cd frontend && yarn dev
# Production build
cd frontend && yarn build
# Run linter
cd frontend && yarn lintThe Vite build outputs frappe_crm_xt/public/js/crm_xt_app.js (IIFE, ~1.5 MB) and frappe_crm_xt/public/js/frappe-crm-xt-frontend.css. Run bench build --app frappe_crm_xt afterwards to have Frappe copy the assets to the site's public/ directory.
- The Events tab and sidebar items are injected into the Frappe CRM SPA via a
MutationObserverinApp.vue— no FCRM source files are modified. - The injected Events tab button deliberately does not carry
role="tab"to avoid Reka UI intercepting the click and collapsing all panels. get_doc_eventsusesignore_permissions=Trueonly on theEvent Participantschild-table queries (which have no standalone doctype-level permission); parentEventrecords respect normal read permissions.- The
crm_sidebarhook is collected from all installed apps viafrappe.get_hooks('crm_sidebar')inapi/sidebar.pyand returned as a single merged list to the frontend. frappe_crm_xtmust be listed aftercrminapps.txtso theoverride_whitelisted_methodsentry forcrm.api.activities.get_activitiestakes precedence correctly.