Package Information
Available Nodes
Documentation generating…
Documentation
n8n-nodes-frappe-lending
This is an n8n community node package for Frappe Lending, the loan management system built on ERPNext. It lets you drive the whole loan lifecycle — products, applications, loans, disbursements, repayments, write-offs and collateral — from your n8n workflows.
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-lending as the package name.
Self-hosted, via the n8n UI — go to Settings > Community nodes > Install, enter n8n-nodes-frappe-lending and confirm.
Self-hosted, manually:
cd ~/.n8n/custom
npm install n8n-nodes-frappe-lending
Restart n8n, then search for "Frappe Lending" in the node panel.
Credentials
This package uses a single credential type, Frappe API (frappeApi) — the same credential type as the Frappe CRM, Frappe Helpdesk and Frappe HR nodes. If you already have it configured, the Frappe Lending node can select it directly.
Generating API keys in Frappe
- In your Frappe site, open the user you want n8n to act as (
/desk/user). - 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. A trailing /app or / is stripped automatically |
| 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 and fails if the site answers as Guest, which is what Frappe returns when the keys are not recognised.
One credential for every Frappe node
frappeApi is deliberately not lending-specific. Frappe authenticates a user on a site, not an application: the same API key works for Frappe Lending, Frappe CRM, Frappe Helpdesk and Frappe HR, 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, the list of consuming nodes, and the Frappe roles each operation needs.
Operations
| Resource | Frappe doctype | Operations |
|---|---|---|
| Loan Product | Loan Product |
Create, Get, Get Many, Update, Delete |
| Loan Application | Loan Application |
Create, Get, Get Many, Update, Delete, Submit, Cancel, Approve, Reject |
| Loan | Loan |
Create, Get, Get Many, Update, Delete, Submit, Cancel, Get Due Details, Apply Charge, Request Closure |
| Loan Disbursement | Loan Disbursement |
Create, Get, Get Many, Update, Delete, Submit, Cancel |
| Loan Repayment | Loan Repayment |
Create, Get, Get Many, Update, Delete, Submit, Cancel |
| Loan Write Off | Loan Write Off |
Create, Get, Get Many, Update, Delete, Submit, Cancel |
| Loan Security | Loan Security |
Create, Get, Get Many, Update, Delete |
| Loan Repayment Schedule | Loan Repayment Schedule |
Get, Get Many, Simulate — no write |
| Loan Interest Accrual | Loan Interest Accrual |
Get, Get Many — read-only |
| Loan Demand | Loan Demand |
Get, Get Many — read-only |
All operations go through the standard Frappe REST API at /api/resource/{doctype} using GET, POST, PUT and DELETE. Submit, Cancel, Approve and Reject additionally call /api/method/frappe.client.submit or /api/method/frappe.client.cancel. Simulate, Get Due Details, Apply Charge and Request Closure call the lending app's own whitelisted methods — see below.
Doctype names were verified against github.com/frappe/lending, directory lending/loan_management/doctype/.
Loan Typeis nowLoan Product.
The doctype was renamed when the lending module was split out of ERPNext into its own app. Tutorials and ERPNext v13 documentation still sayLoan Type, and/api/resource/Loan Typereturns 404 on a current site. Two collateral doctypes were renamed at the same time:Loan Security Pledge→Loan Security Assignment, andLoan Security Unpledge→Loan Security Release.
Loan Repayment Schedule, Loan Interest Accrual and Loan Demand are read-only, on purpose.
All three are generated by the app: the schedule when a loan is disbursed (and regenerated on every restructure, tranche or advance payment), the accruals by the scheduled job behindProcess Loan Interest Accrual, the demands byProcess Loan Demand. Every amount they carry is recomputed from the loan, its schedule and its accrual history, and each posts its own journal entry. Writing one over REST would either be overwritten by the next scheduled run or leave the loan's general ledger inconsistent with its own child tables.
The app's own business endpoints
Beyond the REST resources, the lending app whitelists a handful of methods of its own. Four of them are exposed as operations, because they do things /api/resource cannot express:
| Operation | Frappe method | What it does |
|---|---|---|
| Loan Repayment Schedule → Simulate | lending.api.get_repayment_schedule |
Builds a schedule in memory and returns the instalments — nothing is created |
| Loan → Get Due Details | lending.api.get_due_details |
What is owed as of a date: overdue principal, interest, penalty, charges |
| Loan → Apply Charge | lending.api.apply_charge |
Applies a fee and raises the matching Loan Demand |
| Loan → Request Closure | lending.…doctype.loan.loan.request_loan_closure |
Closes a settled loan, or flags it Loan Closure Requested |
Simulate is the one to reach for when quoting: it takes a product, an amount, a rate and a tenure, and returns one n8n item per instalment — no Loan, no Loan Application, no document at all. Get Due Details answers the collection question in one call, instead of paging through Loan Demand rows and summing them yourself.
Two further methods, make_loan_disbursement and make_repayment_entry, are deliberately not used. They only pre-fill a draft from the loan, which POST /api/resource already does while exposing more fields — and make_loan_disbursement assigns disbursement_date from its posting_date argument and vice versa, so the two dates come out swapped. Creating the document directly avoids inheriting that.
Submittable doctypes and docstatus
Every doctype in this node except Loan Product and Loan Security is submittable in Frappe: it carries a docstatus of 0 (draft), 1 (submitted) or 2 (cancelled).
This matters more here than in a CRM or HR node, because a draft accounting document does nothing. POST /api/resource/Loan Disbursement inserts a record; it moves no money, posts no general ledger entry, and generates no repayment schedule. A draft Loan Repayment clears no demand. Create is therefore half of the operation, not all of it.
That is why this node exposes Submit and Cancel alongside CRUD:
- Submit reads the document back, refuses loudly if it is already submitted or cancelled, then calls
frappe.client.submit. The read-back is not optional:frappe.client.submittakes a whole document, because server-side it doesfrappe.get_doc(doc), which builds an object from the dict it is given rather than loading one. - Cancel checks that the document is submitted, then calls
frappe.client.cancel. Cancelling unwinds the document's general ledger entries, and Frappe refuses when a later document depends on it — you cannot cancel a disbursement while a repayment sits on top of it. That error comes back as-is.
Frappe also refuses to edit a submitted document: Update on a docstatus: 1 record fails for any field not flagged allow_on_submit. On a Loan, those are Freeze Account, Freeze Date, Manual NPA, Unmark NPA and the three Line of Credit limit fields; on a Loan Repayment, only Manual Remarks. For anything else, cancel and amend in Frappe.
Approve / Reject a loan application
Loan Application.status is a Select field (Open, Approved, Rejected) declared at permlevel 1 — writing it requires a role with permlevel 1 access, typically Loan Manager.
The node therefore does, in order:
GETthe document, to read its currentdocstatus;- refuse loudly if it is already submitted or cancelled — Frappe freezes those, and the fix is Cancel + Amend, not a silent failure;
- set
statustoApprovedorRejected; POST /api/method/frappe.client.submitwith the resulting document, which saves and submits in one round trip.
The Submit option (on by default) controls step 4. An approved application only becomes a Loan once submitted, so turning it off stops the origination halfway — do that only when a later stage of your workflow does the submitting.
What this node does not cover
Deliberate gaps in 0.1.0, all of which remain doable from the Frappe UI or through the HTTP Request node:
- Collateral beyond the register.
Loan Security(the pledgeable asset) is exposed;Loan Security Type,Loan Security Price,Loan Security AssignmentandLoan Security Releaseare not. Pledging a security against a loan therefore still happens in Frappe. - Child tables.
Loan Application.proposed_pledges,documentsandco_applicants, and the loan charge tables, are not exposed. Frappe validates them against several other doctypes, and a partially-filled row is rejected in ways that are hard to explain from a workflow. - Restructures, refunds and adjustments.
Loan Restructure,Loan Refund,Loan AdjustmentandLoan Balance Adjustmentare lifecycle exceptions with their own validation chains. - The processing runs.
Process Loan Interest Accrual,Process Loan DemandandProcess Loan Security Shortfallare scheduled jobs. Triggering them from a workflow is possible but rarely what you want — they are already on Frappe's scheduler.
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 |
| 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": "Disbursed" }
[["docstatus", "=", 1], ["days_past_due", ">", 30]]
Note that Frappe does not return child tables in a list query. To read the instalments of a Loan Repayment Schedule, use Get on one schedule rather than Get Many with a fields list.
Dates
Frappe stores naive datetimes, interpreted in the site's timezone (Settings > System Settings > Time Zone). The node converts values that carry a timezone — what the n8n date picker produces, such as 2026-08-15T09:00:00+02:00 or ...Z — into the n8n workflow timezone, and passes values that already have no timezone through unchanged.
Date fields are sent as YYYY-MM-DD, Datetime fields as YYYY-MM-DD HH:mm:ss. Keep your n8n workflow timezone and your Frappe site timezone the same, or a repayment recorded at 09:00 will land at a different hour — and on a loan, an hour is not cosmetic: the value date is what the interest accrual is computed from.
The same field name is not always the same type here. posting_date is a Date on Loan and Loan Application, but a Datetime on Loan Repayment, Loan Demand, Loan Interest Accrual and Loan Repayment Schedule. value_date is a Datetime on Loan Repayment and a Date on Loan Write Off. The node therefore keys its date handling by resource, not by field name alone — see DATE_FIELD_KINDS in nodes/FrappeLending/types.ts.
posting_date is not writable on Loan Repayment and Loan Write Off, and the node does not offer it. Both doctypes flag it reqd, which is misleading: their validate() assigns self.posting_date = get_datetime() unconditionally, so any value you send is replaced by the moment of processing. value_date is the date that matters — loan_repayment.py passes posting_date=self.value_date to every entry it generates.
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 Sanctioned Amount limit crossed for Loan Product CREDIT-AUTO-60 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.
Link fields
Fields that point at another Frappe record are pickers, not free text. Two shapes, chosen by what
the target doctype holds:
- A searchable list for anything the day fills up — loans, accounts, disbursements. Filtering
happens on the Frappe side, 50 rows at a time, and the list shows a readable label beside the
identifier:ACC-LOAN-2026-00001 — Acme Corp. Every one of them keeps a By Name tab for a
literal value or an expression. - A dropdown for the configuration doctypes an administrator maintains — designations, leave
types, currencies. n8n labels these… Name or ID.
A picker never blocks you: if the list cannot be read, the manual mode still accepts the identifier.
Note that a searchable field is stored as { "__rl": true, "mode": …, "value": … }, which is why
the examples below spell it out.
Loan Product — create
{
"parameters": {
"resource": "loanProduct",
"operation": "create",
"product_code": "CREDIT-AUTO-60",
"product_name": "Crédit auto 60 mois",
"company": "Acme SAS",
"rate_of_interest": 4.9,
"additionalFields": {
"is_term_loan": true,
"repayment_schedule_type": "Monthly as per repayment start date",
"maximum_loan_amount": 40000,
"penalty_interest_rate": 12,
"grace_period_in_days": 5,
"loan_account": {
"__rl": true,
"mode": "name",
"value": "Loans and Advances (Assets) - A"
},
"payment_account": {
"__rl": true,
"mode": "name",
"value": "Bank Account - A"
},
"disbursement_account": {
"__rl": true,
"mode": "name",
"value": "Bank Account - A"
},
"interest_income_account": {
"__rl": true,
"mode": "name",
"value": "Interest Income - A"
},
"penalty_income_account": {
"__rl": true,
"mode": "name",
"value": "Interest Income - A"
}
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Create Loan Product",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
product_code becomes the document ID (autoname: field:product_code), so it is what every later node passes as Loan Product. The account fields are Links to ERPNext Account records: give the account's name, which includes the company abbreviation.
Only the accounts a workflow realistically sets are exposed. The doctype carries around twenty more — suspense, accrued, waiver and additional-interest accounts, plus the four collection offset sequences. Those are chart-of-accounts wiring: set them once in the Frappe UI.
Loan Application — create
{
"parameters": {
"resource": "loanApplication",
"operation": "create",
"applicant_type": "Customer",
"company": "Acme SAS",
"loan_product": "CREDIT-AUTO-60",
"additionalFields": {
"applicant": "CUST-2026-00012",
"applicant_email_address": "jean.martin@email.com",
"applicant_phone_number": "+33 6 12 34 56 78",
"loan_amount": 18000,
"repayment_method": "Repay Over Number of Periods",
"repayment_periods": 60,
"loan_purpose": "Vehicle",
"posting_date": "2026-07-30"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Submit Loan Application",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
The record is created as a draft with status Open. rate_of_interest, total_payable_amount and maximum_loan_amount are not exposed: the doctype marks them read-only and Frappe computes them from the product.
Frappe makes applicant_email_address and applicant_phone_number mandatory when applicant_type is Customer.
Loan Application — approve
{
"parameters": {
"resource": "loanApplication",
"operation": "approve",
"documentId": {
"__rl": true,
"mode": "name",
"value": "ACC-LOAP-2026-00001"
},
"approvalOptions": { "submit": true }
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Approve Application",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
Reject is the same node with "operation": "reject"; it sets the status to Rejected and submits identically — a rejected application is a submitted document, not a deleted one.
The output is the full submitted document, docstatus included, so a downstream node can branch on it.
Loan — create, then submit
{
"parameters": {
"resource": "loan",
"operation": "create",
"applicant_type": "Customer",
"applicant": "CUST-2026-00012",
"company": "Acme SAS",
"loan_product": "CREDIT-AUTO-60",
"additionalFields": {
"loan_amount": 18000,
"posting_date": "2026-08-01",
"repayment_start_date": "2026-09-01",
"repayment_method": "Repay Over Number of Periods",
"repayment_periods": 60,
"repayment_frequency": "Monthly",
"rate_of_interest": 4.5
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Book Loan",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
Submitting is what generates the Loan Repayment Schedule, so a second node is usually chained straight after:
{
"parameters": {
"resource": "loan",
"operation": "submit",
"documentId": {
"__rl": true,
"mode": "name",
"value": "={{ $json.name }}"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Submit Loan",
"position": [220, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
rate_of_interest is optional: Frappe fetches it from the product. Setting it here is how you record a negotiated rate on one loan. is_term_loan and repayment_schedule_type are not exposed — whether a loan amortises is a property of its Loan Product, not of the loan.
Set auto_create_disbursement_on_loan_booking in the additional fields if you want Frappe to create and submit the disbursement itself on submission. Leave it off when the loan is paid out in tranches.
Complete origination workflow
Six nodes, end to end: create the loan, submit it, disburse, then record the first repayment. Paste the whole thing into an n8n canvas.
{
"nodes": [
{
"parameters": {
"resource": "loan",
"operation": "create",
"applicant_type": "Customer",
"applicant": "CUST-2026-00012",
"company": "Acme SAS",
"loan_product": "CREDIT-AUTO-60",
"additionalFields": {
"loan_amount": 18000,
"posting_date": "2026-08-01",
"repayment_start_date": "2026-09-01",
"repayment_method": "Repay Over Number of Periods",
"repayment_periods": 60,
"repayment_frequency": "Monthly"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "1. Create Loan",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
},
{
"parameters": {
"resource": "loan",
"operation": "submit",
"documentId": {
"__rl": true,
"mode": "name",
"value": "={{ $json.name }}"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "2. Submit Loan",
"position": [220, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
},
{
"parameters": {
"resource": "loanDisbursement",
"operation": "create",
"against_loan": {
"__rl": true,
"mode": "name",
"value": "={{ $json.name }}"
},
"disbursement_date": "2026-08-02",
"disbursed_amount": 18000,
"additionalFields": {
"mode_of_payment": "Wire Transfer",
"reference_number": "VIR-20260802-4471"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "3. Create Disbursement",
"position": [440, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
},
{
"parameters": {
"resource": "loanDisbursement",
"operation": "submit",
"documentId": {
"__rl": true,
"mode": "name",
"value": "={{ $json.name }}"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "4. Submit Disbursement",
"position": [660, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
},
{
"parameters": {
"resource": "loanRepayment",
"operation": "create",
"against_loan": {
"__rl": true,
"mode": "name",
"value": "={{ $('2. Submit Loan').item.json.name }}"
},
"amount_paid": 337.42,
"value_date": "2026-09-01T00:00:00",
"additionalFields": { "repayment_type": "Normal Repayment" }
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "5. Create Repayment",
"position": [880, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
},
{
"parameters": {
"resource": "loanRepayment",
"operation": "submit",
"documentId": {
"__rl": true,
"mode": "name",
"value": "={{ $json.name }}"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "6. Submit Repayment",
"position": [1100, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
],
"connections": {
"1. Create Loan": { "main": [[{ "node": "2. Submit Loan", "type": "main", "index": 0 }]] },
"2. Submit Loan": { "main": [[{ "node": "3. Create Disbursement", "type": "main", "index": 0 }]] },
"3. Create Disbursement": { "main": [[{ "node": "4. Submit Disbursement", "type": "main", "index": 0 }]] },
"4. Submit Disbursement": { "main": [[{ "node": "5. Create Repayment", "type": "main", "index": 0 }]] },
"5. Create Repayment": { "main": [[{ "node": "6. Submit Repayment", "type": "main", "index": 0 }]] }
}
}
Two things make this chain work:
- Every create is followed by a submit. Steps 2, 4 and 6 are not optional polish — without them the loan has no schedule, the money never moves, and the repayment clears no demand. This is the single most common mistake when driving Frappe Lending over REST.
- Step 5 reaches back with
$('2. Submit Loan')rather than$json.name, because by then$jsonholds the disbursement, not the loan.Loan Repayment.against_loanwants the loan.
repayment_start_date is set to a month after the disbursement, and value_date on the repayment matches the first instalment. Read the actual instalment amount from the generated Loan Repayment Schedule instead of hard-coding 337.42 — or call Loan Repayment Schedule → Simulate before creating anything, to quote the loan first.
Loan Disbursement — create
{
"parameters": {
"resource": "loanDisbursement",
"operation": "create",
"against_loan": {
"__rl": true,
"mode": "name",
"value": "ACC-LOAN-2026-00001"
},
"disbursement_date": "2026-08-02",
"disbursed_amount": 18000,
"additionalFields": {
"bank_account": {
"__rl": true,
"mode": "name",
"value": "Compte Jean Martin"
},
"mode_of_payment": "Wire Transfer",
"reference_number": "VIR-20260802-4471",
"reference_date": "2026-08-02"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Disburse Loan",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
company, applicant and applicant_type are reqd on the doctype but also read-only with a fetch_from on the loan — Frappe fills them in, so the node does not ask. disbursement_date is what makes interest start running, not the document's creation date.
Remember to Submit: a draft disbursement moves no money.
Loan Repayment — create
{
"parameters": {
"resource": "loanRepayment",
"operation": "create",
"against_loan": {
"__rl": true,
"mode": "name",
"value": "ACC-LOAN-2026-00001"
},
"amount_paid": 337.42,
"value_date": "2026-09-01T00:00:00",
"additionalFields": {
"repayment_type": "Normal Repayment",
"mode_of_payment": "Wire Transfer",
"reference_number": "VIR-20260901-8812",
"reference_date": "2026-09-01"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Record Repayment",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
You supply amount_paid; Frappe decides how it splits across penalties, interest, charges and principal, following the collection offset sequence configured on the product. Every one of those amounts (penalty_amount, interest_payable, principal_amount_paid, total_interest_paid, the repayment_details table…) is read-only, so the node does not expose them.
value_date is required and has no default in the doctype — it is the date the interest calculation and every generated journal entry use. Give it the date the money actually arrived, not the date you are keying the record: posting_date is stamped by the server with the moment of processing and cannot be set. repayment_type defaults to Normal Repayment; the other twenty values cover waivers, capitalisations and settlements, and each posts entirely different journal entries for the same amount.
Loan Demand — who is late
{
"parameters": {
"resource": "loanDemand",
"operation": "getAll",
"returnAll": true,
"options": {
"fields": "name,loan,applicant,demand_type,demand_date,demand_amount,outstanding_amount",
"filters": "[[\"docstatus\",\"=\",1],[\"outstanding_amount\",\">\",0],[\"demand_date\",\"<\",\"2026-09-01\"]]",
"sortField": "demand_date",
"sortOrder": "asc"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Overdue Demands",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
This is the doctype to read for a dunning workflow, rather than the loan itself: Loan.days_past_due is a single counter, whereas the demands say what is overdue — demand_type separates EMI from Penalty, Charges, BPI and Additional Interest.
Loan Repayment Schedule — get the active one
{
"parameters": {
"resource": "loanRepaymentSchedule",
"operation": "getAll",
"returnAll": false,
"limit": 1,
"options": {
"filters": "[[\"loan\",\"=\",\"ACC-LOAN-2026-00001\"],[\"status\",\"=\",\"Active\"]]"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Active Schedule",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
Filtering on status = Active matters: every restructure, tranche or advance payment marks the previous schedule Outdated and inserts a new one, so a loan accumulates several over its life. Chain a Get on the resulting name to read the instalments themselves — Frappe does not return the repayment_schedule child table in a list query.
Loan Security — create
{
"parameters": {
"resource": "loanSecurity",
"operation": "create",
"loan_security_code": "VEH-AB123CD",
"loan_security_name": "Renault Clio 2024 – AB-123-CD",
"loan_security_type": "Vehicle",
"additionalFields": {
"original_security_value": 21000,
"haircut": 20
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Register Collateral",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
haircut and loan_to_value_ratio carry a fetch_from on the security type but are not read-only: leave them empty to inherit the type's values, or fill them to override for this one asset. Attaching the security to a loan is a separate doctype — Loan Security Assignment — which this node does not expose.
Cancel
Any submittable resource, given its document ID:
{
"parameters": {
"resource": "loanDisbursement",
"operation": "cancel",
"documentId": {
"__rl": true,
"mode": "name",
"value": "LM-DIS-00001"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Cancel Disbursement",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
Cancelling reverses the document's general ledger entries. Frappe refuses when a later document links to it — cancel the repayment before the disbursement it settles.
Delete
{
"parameters": {
"resource": "loanProduct",
"operation": "delete",
"documentId": {
"__rl": true,
"mode": "name",
"value": "CREDIT-AUTO-60"
}
},
"type": "n8n-nodes-frappe-lending.frappeLending",
"typeVersion": 1,
"name": "Delete Loan Product",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
The node outputs { "success": true, "doctype": "Loan Product", "name": "CREDIT-AUTO-60" }. Frappe refuses to delete a submitted document, and any document another record links to — cancel it first.
Compatibility
Tested end to end against n8n 2.x and Frappe Framework 16 with Frappe Lending 16 and ERPNext 16, on a live site.
The node targets version-16. It also runs on version-15 — the REST endpoints, the docstatus transitions and every CRUD resource behave identically — but four things differ, and all four are version-16 behaviours the node now assumes:
version-15 |
version-16 |
|
|---|---|---|
Loan Demand |
absent — the resource answers DocType Loan Demand not found | present |
lending.api (Simulate, Get Due Details, Apply Charge) |
absent | present |
Loan Application.status |
allow_on_submit — a submitted application could still be approved |
frozen on submit — approve or reject the draft |
value_date on Loan Repayment / Loan Write Off |
absent, posting_date carried the accounting date |
required, and it is what dates the journal entries |
The desk also moved from /app/… to /desk/… in Frappe 16 — the loan workspace is at /desk/loans. This only affects the links you follow in a browser, not the REST paths the node calls.
ERPNext is required: Frappe Lending is built on it, and every posting the app generates lands in ERPNext's chart of accounts. Customer, Company, Account, Cost Center, Mode of Payment and Bank Account — all of which this node links to — come from ERPNext, not from the lending app.
A site running ERPNext v13, before the lending module was extracted, does not work: the doctypes were named differently then (Loan Type rather than Loan Product) and several fields this node writes did not exist.
On version-16 a Loan Product cannot be created without its collection offset sequences — the order in which an incoming payment is applied to penalty, interest, principal and charges. They carry no reqd flag, so nothing in the schema announces them, but the app validates them in Python and refuses the insert with Collection Offset Sequence For Standard Asset is mandatory. Create a Loan Demand Offset Order first, then pass it to the four Collection Offset Sequence fields (or set a default on the Company).
Resources
- n8n community nodes documentation
- Frappe REST API documentation
- Frappe Lending source
- Shared credential architecture
Version history
0.1.0
Initial release. Frappe Lending node with the Loan Product, Loan Application, Loan, Loan Disbursement, Loan Repayment, Loan Write Off, Loan Security, Loan Repayment Schedule, Loan Interest Accrual and Loan Demand resources, the Submit/Cancel docstatus transitions, the Approve/Reject application workflow, 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.