frappe-lending

n8n node for Frappe Lending: loan products, applications, loans, disbursements, repayments, securities and write-offs

Package Information

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

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

  1. In your Frappe site, open the user you want n8n to act as (/desk/user).
  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. 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 Type is now Loan Product.
The doctype was renamed when the lending module was split out of ERPNext into its own app. Tutorials and ERPNext v13 documentation still say Loan Type, and /api/resource/Loan Type returns 404 on a current site. Two collateral doctypes were renamed at the same time: Loan Security Pledge → Loan Security Assignment, and Loan 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 behind Process Loan Interest Accrual, the demands by Process 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.submit takes a whole document, because server-side it does frappe.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:

  1. GET the document, to read its current docstatus;
  2. refuse loudly if it is already submitted or cancelled — Frappe freezes those, and the fix is Cancel + Amend, not a silent failure;
  3. set status to Approved or Rejected;
  4. POST /api/method/frappe.client.submit with 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 Assignment and Loan Security Release are not. Pledging a security against a loan therefore still happens in Frappe.
  • Child tables. Loan Application.proposed_pledges, documents and co_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 Adjustment and Loan Balance Adjustment are lifecycle exceptions with their own validation chains.
  • The processing runs. Process Loan Interest Accrual, Process Loan Demand and Process Loan Security Shortfall are 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 $json holds the disbursement, not the loan. Loan Repayment.against_loan wants 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

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.

Discussion