frappe

Generic n8n node for Frappe and ERPNext: create, read, update, delete, submit, cancel and rename any doctype, and call whitelisted methods

Package Information

Released: 2 mo ago
Downloads: 0 weekly / 0 monthly
Latest Version: 0.1.0

Available Nodes

Documentation generating…

Documentation

n8n-nodes-frappe

This is an n8n community node package for Frappe Framework and ERPNext. It is the generic one: instead of covering the doctypes of a single application, it drives any doctype of your site — the doctype is picked at run time, and the form adapts to it.

n8n is a fair-code licensed workflow automation platform.

Other languages: Français · Español · Deutsch

Installation
Credentials
Operations
Usage
Compatibility
Resources
Version history
Development

Installation

Follow the installation guide in the n8n community nodes documentation, using n8n-nodes-frappe as the package name.

Self-hosted, via the n8n UI — go to Settings > Community nodes > Install, enter n8n-nodes-frappe and confirm.

Self-hosted, manually:

cd ~/.n8n/custom
npm install n8n-nodes-frappe

Restart n8n, then search for "Frappe" in the node panel.

How this compares to the ERPNext node bundled with n8n

n8n ships an ERPNext node in n8n-nodes-base. Despite the name it is a generic Frappe node: it calls /api/resource/{doctype} and reads the doctype meta, exactly like this one. If it does what you need, you do not need this package.

What differs, all of it verified in its shipped source:

Bundled ERPNext node This node
Operations Create, Delete, Get, Get Many, Update the same five, plus Submit, Cancel, Rename, Count, Get Value, Call, Call On Document
Doctype picker one flat dropdown of every doctype resource locator with server-side search — a typical site has 750+
Field entry one fixed list of name/value pairs resource mapper: the doctype's real fields, with types, Select options and required flags
Per-item state body and qs are declared outside the item loop and never reset, so item n's fields leak into item n+1 both built inside the loop
Auto-pagination advances limit_start by limit_page_length - 1, duplicating one record per page boundary advances by the page size
Filters // TODO: filters not working filters, or_filters, order_by, parent
Error messages any 403 becomes DocType unavailable. Frappe's own message, unwrapped from _server_messages
continueOnFail not honoured honoured
Credential its own erpNextApi, with the cloud domain fixed to erpnext.com / frappe.cloud the shared frappeApi, any site URL

The bundled node does two things right that are worth crediting: it is usableAsTool and it sets pairedItem correctly. This node does both as well.

Credentials

This package uses a single credential type, Frappe API (frappeApi) — the same credential type as the Frappe CRM, Frappe Helpdesk, Frappe HR, Frappe Insights, Frappe Learning and Frappe Lending nodes. If you already have it configured, this node can select it directly.

Generating API keys in Frappe

  1. In your Frappe site, open the user you want n8n to act as (/app/user, or /desk/user on v16).
  2. Scroll to Settings > API Access and click Generate Keys.
  3. Copy the API Secret — it is shown only once — and the API Key displayed on the user document.

The n8n node acts as that user, so it inherits that user's roles and permissions. If a call fails with a permission error, check the roles on the doctype rather than the credential.

Filling in the credential

Field Example Notes
Site URL https://my-site.frappe.cloud Site root. An application path or trailing / is stripped
API Key a1b2c3d4e5f6g7h
API Secret s1e2c3r4e5t6 Stored encrypted by n8n

Requests are authenticated with the header Authorization: token {apiKey}:{apiSecret}. Use Test to validate the connection — it calls /api/method/frappe.auth.get_logged_user at the site root and fails on a 401, on a 403, or on a Guest answer.

Pasting the URL your browser shows is fine: https://my-site.frappe.cloud/desk/hrms, or even https://my-site.frappe.cloud/desk/employee/HR-EMP-00001, is truncated back to the site root. This happens in the node and in the credential test — that second part matters, because a single-page-app router serves its own HTML page with a 200 for anything under its prefix, so a test aimed at /lms/api/method/... could come back green no matter what the keys were worth.

Permissions this node needs

Beyond the permissions of whatever doctype you are working with:

  • read on DocType — the doctype picker, the field mapper, date normalization and the Submit/Cancel guard all read the doctype definition. Without it the node still works, but you have to type the doctype and field names yourself — and, since nothing then says which fields hold a date, a value that carries a timezone (what the n8n date picker produces) is refused with an explicit message rather than sent as-is and silently shifted. Values that already have no timezone go through unchanged;
  • submit / cancel are permissions of their own, distinct from write;
  • rename is a permission of its own, and the doctype must have allow_rename ticked;
  • Method > Call only reaches functions decorated @frappe.whitelist(). Anything else answers 403 whatever your roles.

One credential for every Frappe node

frappeApi is deliberately not application-specific. Frappe authenticates a user on a site, not an application: the same API key works for this node and for the six application-specific ones, which all live on the same site and share the same /api endpoint.

Create one credential per site (Frappe – prod, Frappe – staging), not per application. See docs/CREDENTIALS.md for the full architecture and the list of consuming nodes.

Operations

Document

Operation Calls
Create POST /api/resource/{doctype}
Get GET /api/resource/{doctype}/{name}
Get Many GET /api/resource/{doctype}
Update PUT /api/resource/{doctype}/{name}
Delete DELETE /api/resource/{doctype}/{name}
Submit frappe.client.submit
Cancel frappe.client.cancel
Rename frappe.client.rename_doc
Count frappe.client.get_count
Get Value frappe.client.get_value

Method

Operation Calls
Call /api/method/{dotted.path}, with the HTTP verb you choose
Call On Document POST /api/resource/{doctype}/{name} with run_method

Choosing the doctype

The DocType field is a resource locator with three modes:

  • From List — searches the site's doctypes as you type. It queries DocType with a name like filter, 50 at a time, and pages on scroll. Child tables (istable = 1) are excluded: they exist only inside their parent document and /api/resource refuses to serve them on their own.
  • By Name — type the doctype exactly as Frappe spells it, capitalisation and spaces included: Sales Invoice, not sales_invoice. This is also how you reach a child table, paired with the Parent option of Get Many.
  • Expression — an expression, which is what makes the node usable in a loop over doctype names.

The Document field on the single-document operations works the same way, searching the selected doctype's records by name, most recently modified first.

Filling in fields

Create and Update use n8n's resource mapper. Pick a doctype and the form fills itself from the doctype definition: real labels, the right widget per field type, Select options as a dropdown, and mandatory fields marked as required.

  • Layout entries — Section Break, Column Break, Tab Break, HTML, Heading, Fold, Button, Image — are left out, along with read-only, hidden and virtual fields. None of them are things you can write.
  • Link and Dynamic Link fields are plain text rather than dropdowns: the target doctype can hold millions of rows and the mapper has no server-side search. Resolve the link with a Get Value step if you only have a human-readable label.
  • If the doctype's naming is set to Prompt, a required Name (ID) field is added at the top: that doctype expects you to supply the primary key.
  • Update marks nothing as required. It is a partial update; send only the fields you are changing.
  • Custom Fields are included. Frappe keeps fields added after the fact in a separate Custom Field doctype and only assembles them in memory, so reading a doctype definition over REST returns an incomplete schema. The node reads both sources and merges them in form order. If the API user cannot read Custom Field, the form falls back to the standard fields instead of failing.
  • Switching the mapper to Map Automatically sends the incoming item's JSON as the document body, unchanged.

Get Many options

Option Maps to Notes
Return All auto-paginates limit_start Fetches 100 records per request until the last page
Limit limit_page_length Used when Return All is off
Offset limit_start Ignored when Return All is on
Fields fields Comma-separated or a JSON array. Defaults to ["*"]
Filters (JSON) filters Frappe filter syntax
Or Filters (JSON) or_filters Same syntax, combined with OR
Parent parent Parent doctype, required to read a child table
Sort Field / Order order_by e.g. modified desc

Frappe returns only the name column when fields is not specified, so the node defaults to ["*"] to give you the full document.

Filters accept both Frappe forms — an object for simple equality, or an array of triples for operators:

{ "status": "Open" }
[["creation", ">=", "2026-01-01"], ["status", "!=", "Cancelled"]]

Submit, Cancel and Rename

A submittable doctype — Sales Invoice, Payment Entry, Stock Entry, Training Event… — has a docstatus that goes 0 (Draft) → 1 (Submitted) → 2 (Cancelled). That column cannot be written through the REST resource route, so Submit and Cancel go through frappe.client.submit and frappe.client.cancel instead. Submit reads the document first and posts it whole, which is what makes the server run the full validation.

The node checks is_submittable on the doctype before either one and tells you plainly when the doctype is not submittable, rather than passing on Frappe's opaque answer.

Rename changes a document's primary key, which Frappe treats as a real operation: it repoints every link to it. With Merge on, renaming onto an existing name merges the two documents — the source is deleted and its links are repointed. That cannot be undone.

Calling methods

Call reaches any function decorated @frappe.whitelist(), addressed by its dotted path (frappe.client.get_list, erpnext.stock.get_item_details.get_item_details, …). Arguments go in as a JSON object and are spread as keyword arguments, so an unexpected key fails with a 417. Pick the HTTP verb: a plain @frappe.whitelist() accepts all four, but methods=["GET"] refuses anything else.

Call On Document runs a controller method on an existing document — a method of the document class, running with that document as self. It has to be whitelisted on the class.

Dates and times

Frappe stores naive values, with no timezone attached. The node converts values that carry one — what the n8n date picker produces, such as 2026-01-12T09:00:00+01:00 or ...Z — into the n8n workflow timezone, and passes values that already have no timezone through unchanged.

Which conversion applies is read from the doctype definition, not guessed from the field name: Date fields are sent as YYYY-MM-DD, Datetime as YYYY-MM-DD HH:mm:ss, Time as HH:mm:ss (a bare 9:00 is padded to 09:00:00).

In practice: keep your n8n workflow timezone and your Frappe site timezone the same.

Error handling

Frappe reports errors in a _server_messages field that contains JSON encoded inside JSON, often with HTML markup. The node unwraps it and surfaces the actual message — you get Customer is mandatory rather than Request failed with status code 417. It falls back to the exception field, then to the HTTP status.

401 and 403 responses carry an extra hint pointing at the Frappe role rather than the credential, because that is nearly always the cause.

Usage

Each example below is a node you can paste into an n8n workflow. Replace the credentials block with your own credential.

Create a document

{
	"parameters": {
		"resource": "document",
		"operation": "create",
		"docType": { "__rl": true, "mode": "list", "value": "ToDo" },
		"fields": {
			"mappingMode": "defineBelow",
			"value": {
				"description": "Call the customer back",
				"priority": "High",
				"date": "2026-08-15"
			}
		}
	},
	"type": "n8n-nodes-frappe.frappe",
	"typeVersion": 1,
	"name": "Create ToDo",
	"position": [0, 0],
	"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}

Get many, filtered

{
	"parameters": {
		"resource": "document",
		"operation": "getAll",
		"docType": { "__rl": true, "mode": "list", "value": "Sales Invoice" },
		"returnAll": true,
		"options": {
			"fields": "name,customer,grand_total,status",
			"filters": "[[\"status\",\"=\",\"Overdue\"],[\"posting_date\",\">=\",\"2026-01-01\"]]",
			"sortField": "posting_date",
			"sortOrder": "desc"
		}
	},
	"type": "n8n-nodes-frappe.frappe",
	"typeVersion": 1,
	"name": "Overdue Invoices",
	"position": [0, 0],
	"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}

Submit a document

{
	"parameters": {
		"resource": "document",
		"operation": "submit",
		"docType": { "__rl": true, "mode": "list", "value": "Sales Invoice" },
		"documentId": { "__rl": true, "mode": "name", "value": "ACC-SINV-2026-00042" }
	},
	"type": "n8n-nodes-frappe.frappe",
	"typeVersion": 1,
	"name": "Submit Invoice",
	"position": [0, 0],
	"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}

Look up one value

Cheaper than Get Many when you only need a field or two of the first matching document:

{
	"parameters": {
		"resource": "document",
		"operation": "getValue",
		"docType": { "__rl": true, "mode": "list", "value": "Customer" },
		"fieldNames": "name,customer_group,territory",
		"filters": "{\"customer_name\": \"Acme Inc\"}"
	},
	"type": "n8n-nodes-frappe.frappe",
	"typeVersion": 1,
	"name": "Resolve Customer",
	"position": [0, 0],
	"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}

Call a whitelisted method

{
	"parameters": {
		"resource": "method",
		"operation": "call",
		"method": "frappe.client.get_list",
		"httpMethod": "GET",
		"args": "{\"doctype\": \"Item\", \"filters\": {\"is_stock_item\": 1}, \"limit_page_length\": 20}"
	},
	"type": "n8n-nodes-frappe.frappe",
	"typeVersion": 1,
	"name": "List Stock Items",
	"position": [0, 0],
	"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}

Compatibility

Tested against n8n 2.x and Frappe Framework v15 and v16, on a site running frappe, erpnext, hrms, lending, lms, insights, crm and helpdesk.

Frappe v16 changes nothing the node relies on: /api/resource (the v1 REST API) still answers alongside /api/v2/document (which ships on 15 as well), limit_start / limit_page_length / filters / or_filters / order_by behave as before, and errors still carry exception, exc_type and _server_messages. One difference is worth knowing when a credential fails: an unauthenticated call to frappe.auth.get_logged_user returned 200 {"message":"Guest"} on v15, whereas v16 raises a PermissionError and answers 403 — bad keys give 401 on both.

Beyond /api/resource, the node calls five framework methods: frappe.client.submit, frappe.client.cancel, frappe.client.rename_doc, frappe.client.get_count and frappe.client.get_value. All five ship with Frappe Framework itself, on both major versions.

Resources

Version history

0.1.0

Initial release. Generic Frappe node with the Document resource (Create, Get, Get Many, Update, Delete, Submit, Cancel, Rename, Count, Get Value) and the Method resource (Call, Call On Document), server-side doctype and document search, a resource mapper driven by the doctype definition, schema-driven date normalization, and the shared frappeApi credential.

Development

npm install
npm run build     # compiles to dist/ and copies icons
npm run dev       # development loop against a local n8n
npm run lint      # same command the CI runs
npm run lint:fix

There is no test runner in this repository. Verify changes with npm run build followed by a real load in n8n.

See AGENTS.md for the full contributor guide.

Discussion