Skip to content

Custom Actions

Custom actions let you add buttons to the Lead or Deal page header. You can add single buttons, group them under a dropdown, or create labeled dropdown menus — each running your own logic when clicked.

Custom actions live inside a CRM Form Script. Open CRM Form Script in Desk, pick your DocType (Lead or Deal), and write a class. Set this.actions inside onLoad():

class CRMDeal {
onLoad() {
this.actions = [
{
label: "Open in Desk",
onClick: (close) => {
let URL = `https://frappecrm.frappe.cloud/app/crm-deal/${this.doc.name}`
window.open(URL, '_blank')
}
}
]
}
}

This adds a button labelled “Open in Desk” that opens the current deal in a new tab.

![0 *“Open In Desk” Button in Top Right of the header *

Use group to nest items under a single more-horizontal icon dropdown. Set hideLabel: true to hide the group heading from the dropdown menu.

class CRMDeal {
onLoad() {
this.actions = [
{
group: "Add",
hideLabel: true,
items: [
{ label: "Create Quotation", onClick: (close) => {} },
{ label: "Create Sales Order", onClick: (close) => {} },
]
},
{
group: "Delete",
items: [
{ label: "Delete Quotation", onClick: (close) => {} },
{ label: "Delete Sales Order", onClick: (close) => {} },
]
}
]
}
}

![1

Grouped Actions with Label (dropdown button)

Section titled “Grouped Actions with Label (dropdown button)”

Add buttonLabel to turn the three-dot icon into a labelled dropdown button.

class CRMDeal {
onLoad() {
this.actions = [
{
buttonLabel: "Create",
group: "Add",
hideLabel: true,
items: [
{ label: "Create Quotation", onClick: (close) => {} },
{ label: "Create Sales Order", onClick: (close) => {} },
]
}
]
}
}

![2

You can combine both styles in one array:

class CRMDeal {
onLoad() {
this.actions = [
{
label: "Refresh Score",
icon: "refresh-cw",
onClick: (close) => { /* ... */ }
},
{
group: "More",
hideLabel: true,
items: [
{ label: "Export PDF", onClick: (close) => {} },
{ label: "Duplicate", onClick: (close) => {} },
]
}
]
}
}

![3

Inside your script, these helpers are available without any imports:

  • this.doc — the current document. Read and write fields directly: this.doc.status = "Lost", this.doc.deal_name.
  • call(method, params) — call any server-side Python method. Returns a Promise.
  • createDialog(options) — show a confirmation dialog. Provide title, message, and actions.
  • formDialog(options) — show a dialog with form fields to collect input from the user.
  • toast.success(msg), toast.error(msg), toast.info(msg) — show notifications.
  • router.push(location), router.replace(location) — navigate to a different page.
  • throwError(msg) — show a red toast and stop execution immediately.

This action checks if the deal is Won, then calls the server to create a quotation. If successful, it navigates to the new quotation page.

class CRMDeal {
onLoad() {
this.actions = [
{
label: "Create Quotation",
icon: "file-text",
onClick: (close) => {
if (this.doc.status !== "Won") {
toast.info("This action is only available for Won deals");
return;
}
call("crm.api.quotation.create_from_deal", {
deal: this.doc.name,
customer: this.doc.organization,
}).then((data) => {
if (data?.name) {
router.push({ name: "Quotation", params: { name: data.name } });
toast.success("Quotation created successfully");
} else {
toast.error("Could not create quotation");
}
});
},
},
];
}
}

Asks the user to confirm, then updates the deal status directly.

class CRMDeal {
onLoad() {
this.actions = [
{
label: "Mark as Lost",
icon: "x-circle",
onClick: (close) => {
createDialog({
title: "Mark Deal Lost",
message: "Are you sure you want to mark this deal as Lost?",
actions: [
{
label: "Mark Lost",
theme: "red",
variant: "solid",
onClick: async ({ close: dialogClose }) => {
this.doc.status = "Lost";
toast.success("Deal marked as Lost");
dialogClose();
},
},
{
label: "Cancel",
variant: "outline",
onClick: ({ close: dialogClose }) => dialogClose(),
},
],
});
},
},
];
}
}

Opens a dialog with date and notes fields. When submitted, calls the server to save the follow-up.

class CRMDeal {
onLoad() {
this.actions = [
{
label: "Schedule Follow-up",
icon: "calendar",
onClick: (close) => {
formDialog({
title: "Schedule Follow-up",
fields: [
{
fieldname: "date",
fieldtype: "Date",
label: "Follow-up Date",
reqd: 1,
},
{ fieldname: "notes", fieldtype: "Small Text", label: "Notes" },
],
submitLabel: "Schedule",
cancelLabel: "Cancel",
onSubmit: async (data) => {
await call("crm.api.deal.schedule_followup", {
deal: this.doc.name,
date: data.date,
notes: data.notes,
});
toast.success("Follow-up scheduled for " + data.date);
},
});
},
},
];
}
}

Delete Deal (with confirmation and redirect)

Section titled “Delete Deal (with confirmation and redirect)”

Confirms before deleting, then redirects back to the deals list.

class CRMDeal {
onLoad() {
this.actions = [
{
label: "Delete Deal",
icon: "trash-2",
onClick: (close) => {
createDialog({
title: "Delete Deal",
message: `Are you sure you want to delete "${this.doc.deal_name}"? This cannot be undone.`,
actions: [
{
label: "Delete",
theme: "red",
variant: "solid",
onClick: async ({ close: dialogClose }) => {
await call("frappe.client.delete", {
doctype: "CRM Deal",
name: this.doc.name,
});
dialogClose();
router.push("/deals");
toast.success("Deal deleted");
},
},
{
label: "Cancel",
variant: "outline",
onClick: ({ close: dialogClose }) => dialogClose(),
},
],
});
},
},
];
}
}

Showing and Hiding Actions Based on Field Values

Section titled “Showing and Hiding Actions Based on Field Values”

Sometimes you only want certain actions to appear in certain situations. Re-set this.actions whenever the relevant field changes:

class CRMDeal {
onLoad() {
this._updateActions();
}
// Re-run whenever status changes
status() {
this._updateActions();
}
_updateActions() {
const isLost = this.doc.status === "Lost";
this.actions = [
{
label: "Create Quotation",
onClick: (close) => {
/* ... */
},
},
// Only show "Mark as Lost" if the deal isn't already lost
...(!isLost
? [
{
label: "Mark as Lost",
icon: "x-circle",
onClick: (close) => {
/* ... */
},
},
]
: []),
];
}
}