Package Information
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
- In your Frappe site, open the user you want n8n to act as (
/app/user, or/desk/useron v16). - Scroll to Settings > API Access and click Generate Keys.
- 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_renameticked; Method > Callonly 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
DocTypewith aname likefilter, 50 at a time, and pages on scroll. Child tables (istable = 1) are excluded: they exist only inside their parent document and/api/resourcerefuses to serve them on their own. - By Name — type the doctype exactly as Frappe spells it, capitalisation and spaces included:
Sales Invoice, notsales_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.
LinkandDynamic Linkfields 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 Fielddoctype 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 readCustom 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
- n8n community nodes documentation
- Frappe REST API documentation
- Frappe Framework source
- Shared credential architecture
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.