Package Information
Available Nodes
Documentation generating…
Documentation
n8n-nodes-frappe-learning
This is an n8n community node package for Frappe Learning (the lms app, formerly known as Frappe LMS). It lets you read and write courses, chapters, lessons, batches, enrollments, quizzes, assignments and certificates 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-learning as the package name.
Self-hosted, via the n8n UI — go to Settings > Community nodes > Install, enter n8n-nodes-frappe-learning and confirm.
Self-hosted, manually:
cd ~/.n8n/custom
npm install n8n-nodes-frappe-learning
Restart n8n, then search for "Frappe Learning" 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, Frappe HR, Frappe Insights and Frappe Lending nodes. If you already have it configured, the Frappe Learning 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). - 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 /lms 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 at the site root and fails on a 401, on a 403, or on a Guest answer.
Pasting the URL your browser shows, https://my-site.frappe.cloud/lms, is fine: the SPA paths (/lms, /crm, /helpdesk, /hr, /insights, /app…) are stripped, by the node and by the credential test. That last part matters — the LMS router serves its own HTML page with a 200 for anything under /lms, so a test aimed at /lms/api/method/... would come back green no matter what the keys were worth.
Roles you will need
The node inherits the roles of the user whose keys you generated:
| What you want to do | Frappe role |
|---|---|
| Read published courses and your own progress | LMS Student |
| Create or edit courses, chapters, lessons, quizzes | Course Creator or Moderator |
| Enroll someone else in a batch | Moderator or Batch Evaluator |
| Delete a course, chapter or lesson | Course author, instructor, or Moderator |
| Issue a certificate | Batch Evaluator, Moderator, Course Creator or System Manager |
One credential for every Frappe node
frappeApi is deliberately not LMS-specific. Frappe authenticates a user on a site, not an application: the same API key works for Frappe Learning, Frappe CRM, Frappe Helpdesk, Frappe HR, Frappe Insights and Frappe Lending, 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 |
|---|---|---|
| Course | LMS Course |
Create, Get, Get Many, Update, Delete |
| Chapter | Course Chapter |
Create, Get, Get Many, Update, Delete |
| Lesson | Course Lesson |
Create, Get, Get Many, Update, Delete |
| Batch | LMS Batch |
Create, Get, Get Many, Update, Delete |
| Course Enrollment | LMS Enrollment |
Create, Get, Get Many, Update, Delete |
| Batch Enrollment | LMS Batch Enrollment |
Create, Get, Get Many, Update, Delete |
| Quiz | LMS Quiz |
Create, Get, Get Many, Update, Delete |
| Quiz Submission | LMS Quiz Submission |
Get, Get Many — read-only |
| Assignment | LMS Assignment |
Create, Get, Get Many, Update, Delete |
| Assignment Submission | LMS Assignment Submission |
Create, Get, Get Many, Update, Delete, Evaluate |
| Certificate | LMS Certificate |
Create, Get, Get Many, Update, Delete |
| Course Progress | LMS Course Progress |
Get, Get Many — read-only |
Most operations go through the standard Frappe REST API at /api/resource/{doctype} using GET, POST, PUT and DELETE. Six of them do not — see The course outline is not what you think below.
Doctype names were verified against github.com/frappe/lms (lms/lms/doctype/).
Mind the prefixes. They are not uniform. The course outline uses
Course ChapterandCourse Lesson— without theLMSprefix — while everything else has it (LMS Course,LMS Batch, …). Getting this wrong yields a 404 that reads like a permission problem.
This node targets Frappe Learning v2.0 and later. Both enrollment doctypes moved in that release, and a pre-2.0 site answers 404 on them.
LMS Enrollmentis what older versions calledLMS Batch Membership.LMS Batch Enrollmentdid not exist at all: batch students were rows of aBatch Studentchild table onLMS Batch— issue frappe/lms#724 asked for a standalone doctype "like Course Enrolment", andlms.patches.v2_0.migrate_batch_student_datacopies the old rows over beforeBatch Studentis dropped. The two shapes cannot be served from one path, so the node supports the current one only.
The course outline is not what you think
This is the one thing worth reading before automating Frappe Learning.
A course's structure lives in child reference tables, not in the back-links you would expect:
LMS Course.chaptersis a table ofChapter Referencerows. It carries both which chapters belong to the course and in what order.Course Chapter.lessonsis a table ofLesson Referencerows, same deal for lessons.- The
Course Chapter.courseandCourse Lesson.chapterfields are only back-links, used for counters andfetch_from.
So a plain POST /api/resource/Course Chapter does insert the document — and the chapter still does not appear in the course. Deleting one through DELETE /api/resource/... leaves the outline pointing at a document that no longer exists, or fails outright with LinkExistsError.
The node therefore routes six operations through whitelisted lms.lms.api methods instead:
| Operation | Calls | Why |
|---|---|---|
| Chapter create | upsert_chapter |
Inserts the chapter and its Chapter Reference row |
| Lesson create | create_lesson, then PUT |
Inserts the lesson and its Lesson Reference, then fills it in |
| Chapter delete | delete_chapter |
Removes lessons, references, progress, discussions; reindexes |
| Lesson delete | delete_lesson |
Renumbers the chapter's remaining lessons |
| Course delete | delete_course |
Unlinks enrollments, certificates, quizzes, batches… |
| Batch delete | delete_batch |
Unlinks enrollments, courses, assessments, timetables |
These methods call can_modify_course() / can_modify_batch(), so a 403 here comes from the role — Course Creator, Moderator, or being an instructor on the course — not from the credential. delete_lesson needs the chapter as a parameter; the node reads it off the lesson so you do not have to supply it twice.
One limit worth stating plainly: updating a chapter's course field does not move it between courses. The Chapter Reference row stays where it is, and there is no LMS method to move it (update_chapter_index only reorders within one course). Delete and recreate the chapter instead.
SCORM chapters are not exposed either: upsert_chapter accepts a SCORM package, but demands an already-uploaded File document that it unzips server-side. Create those from the LMS interface.
Read-only resources, on purpose
Quiz Submission is computed end to end by the LMS when a student sends their answers: it derives score, percentage and the result table, and fetches score_out_of and passing_percentage from the quiz. Every writable field is either derived or graded, so creating one by hand would record a result nobody took.
Course Progress is a trap rather than a feature: its on_update recalculates the progress percentage of the matching LMS Enrollment, and the LMS advances current_lesson in the same transaction. Writing rows directly desynchronises the learner's progress. The supported way to mark a lesson done is the student's own action, or lms.lms.api.mark_lesson_progress. Reading it is genuinely useful, though: Get Many filtered on course and member gives the detail behind the percentage shown on the enrollment.
For the same reason, some fields are not exposed on writable resources. LMS Enrollment.progress and purchased_certificate are reverted to their stored value by enforce_server_managed_fields() for any caller without a privileged role — sending them does nothing. LMS Quiz.total_marks is reqd and read_only: it is the sum of the questions table.
Enrollments have side effects
Creating a Batch Enrollment is not a plain insert. In one request, Frappe Learning:
- checks you may do it at all —
validate_owner()requiresModeratororBatch Evaluatorto enroll anyone other than yourself, whatever the doctype permissions say; - refuses duplicates, full batches (
seat_count), restricted batches (allow_self_enrollmentoff), and paid batches without a received payment; - opens an
LMS Enrollmentfor every course in the batch; - sends the confirmation e-mail and adds the member to the batch's live classes.
Set Confirmation Email Sent to true on create if you want to suppress step 4's e-mail.
There is a whitelisted lms.lms.utils.enroll_in_batch method, and the node deliberately does not use it: it hardcodes member = frappe.session.user, so it would enroll whoever owns the API keys rather than the person you named. Creating the document over REST is also what the LMS admin interface does.
A plain Enrollment has its own guards: no duplicate for the same member and course, no unpublished course, no paid course without a received payment, and none at all if the course has disable_self_learning — unless you hold a privileged role.
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:
{ "published": 1 }
[["start_date", ">=", "2026-01-01"], ["published", "=", 1]]
Document IDs
Frappe's name field is what every single-document operation takes, and Frappe Learning names things four different ways:
| Resource | Looks like | Naming rule |
|---|---|---|
| Course, Batch, Quiz | python-for-beginners |
Slug generated from the title |
| Chapter, Lesson | 0001 Introduction |
Counter followed by the title |
| Assignment, Assignment Submission | ASG-00001, ASG-SUB-00001 |
Series |
| Enrollment, Certificate, … | a1b2c3d4e5 |
Random hash |
You rarely have to type any of these: the Document field searches the site and shows the identifier next to a readable label — an enrollment appears as a1b2c3d4e5 — Jane Smith. Get Many with a filter such as [["member","=","student@example.com"],["course","=","python-for-beginners"]] remains the way to find one from an expression.
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.
Date fields are sent as YYYY-MM-DD. Time fields (Start Time, End Time on a batch) are sent as HH:mm:ss; a bare 9:00 is padded to 09:00:00. Batch times are interpreted in the batch's own Timezone field, which is why that field is required.
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 Student is already enrolled in this course. 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 — courses, batches, lessons, members.
Filtering happens on the Frappe side, 50 rows at a time, and the list shows a readable label
beside the identifier:python-for-beginners — Python for Beginners. 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.
Course — create
{
"parameters": {
"resource": "course",
"operation": "create",
"title": "Python for Beginners",
"short_introduction": "Learn Python from scratch, no prior experience needed.",
"description": "<p>Variables, control flow, functions, and a final project.</p>",
"instructors": "teacher@acme.io,assistant@acme.io",
"additionalFields": {
"category": "Programming",
"tags": "python,beginner",
"published": true,
"enable_certification": true,
"card_gradient": "Blue"
}
},
"type": "n8n-nodes-frappe-learning.frappeLearning",
"typeVersion": 1,
"name": "Create Course",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
title, short_introduction, description and instructors are the four fields the doctype declares reqd. Instructors is a comma-separated list of Frappe user e-mails; the node expands it into the Course Instructor child table for you.
The new course's name is a slug derived from the title (python-for-beginners) — read it off the node's output, do not assume it.
published matters more than it looks: the LMS refuses to enroll anyone in an unpublished course unless the caller holds a privileged role.
Chapter — create
{
"parameters": {
"resource": "chapter",
"operation": "create",
"course": {
"__rl": true,
"mode": "name",
"value": "python-for-beginners"
},
"title": "Getting Started"
},
"type": "n8n-nodes-frappe-learning.frappeLearning",
"typeVersion": 1,
"name": "Add Chapter",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
This one goes through lms.lms.api.upsert_chapter, not POST /api/resource, so the chapter lands in the course outline and not just in the database. It is appended at the end of the outline.
Lesson — create
{
"parameters": {
"resource": "lesson",
"operation": "create",
"chapter": {
"__rl": true,
"mode": "name",
"value": "0001 Getting Started"
},
"title": "Variables and Types",
"additionalFields": {
"body": "## Variables\n\nA variable is a name bound to a value.",
"include_in_preview": true,
"youtube": "dQw4w9WgXcQ"
}
},
"type": "n8n-nodes-frappe-learning.frappeLearning",
"typeVersion": 1,
"name": "Add Lesson",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
Two round trips under the hood: create_lesson appends a draft lesson and its Lesson Reference row atomically, then the node PUTs your fields onto it. course is not exposed — the doctype fetches it from the chapter.
The lesson id this returns is not durable.
Course Lessonis autonamedformat:{####} {title}, andcreate_lessoninserts under the placeholder title, so the new lesson comes back as0003 Untitled lessoneven though thePUTright after sets the real title — renaming a document is not a field update. A daily scheduled job,rename_settled_untitled_lessons, then rewrites the docname to0003 Your titleonce the lesson has been untouched for 24 hours. A workflow that stores the returnednameand reuses it a day later will get a 404. Store thetitle(or the chapter and index) and resolve the lesson with Get Many at the time you need it.
Note the two body fields. Body (Markdown) is the legacy field, rendered when the other is empty. Content (EditorJS JSON) is what the current LMS editor writes, shaped {"blocks": [...]}; it is sanitised server-side, so invalid JSON is silently dropped rather than rejected. Use Markdown unless you are reproducing editor output.
Batch — create
{
"parameters": {
"resource": "batch",
"operation": "create",
"title": "Python — January 2026",
"description": "Six evening sessions, live.",
"batch_details": "<p>Tuesdays and Thursdays, 18:00–20:00 CET.</p>",
"instructors": "teacher@acme.io",
"start_date": "2026-01-12",
"end_date": "2026-02-13",
"start_time": "18:00:00",
"end_time": "20:00:00",
"timezone": "Europe/Paris",
"additionalFields": {
"courses": "python-for-beginners",
"medium": "Online",
"seat_count": 20,
"allow_self_enrollment": true,
"published": true,
"certification": true
}
},
"type": "n8n-nodes-frappe-learning.frappeLearning",
"typeVersion": 1,
"name": "Create Batch",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
Nine required fields, which is unusual — the LMS Batch controller calls _validate_mandatory() at the top of validate(), so the usual "create bare, fill in later" is not available here.
Courses is comma-separated like Instructors, expanded into the Batch Course table. It is what makes batch enrollment useful: enrolling a student in the batch enrolls them in each of these courses.
The LMS validates that end_date is not before start_date, and that start_time is strictly before end_time.
Batch Enrollment — create
{
"parameters": {
"resource": "batchEnrollment",
"operation": "create",
"member": {
"__rl": true,
"mode": "name",
"value": "student@example.com"
},
"batch": {
"__rl": true,
"mode": "name",
"value": "python-january-2026"
},
"additionalFields": {
"source": "Website"
}
},
"type": "n8n-nodes-frappe-learning.frappeLearning",
"typeVersion": 1,
"name": "Enroll Student",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
This is the highest-leverage operation in the node, and the one with the most side effects — see Enrollments have side effects. It needs Moderator or Batch Evaluator to enroll anyone but yourself.
Course Enrollment — create
Enrolling in a single course, without a batch:
{
"parameters": {
"resource": "courseEnrollment",
"operation": "create",
"member": {
"__rl": true,
"mode": "name",
"value": "student@example.com"
},
"course": {
"__rl": true,
"mode": "name",
"value": "python-for-beginners"
},
"additionalFields": {
"member_type": "Student",
"role": "Member"
}
},
"type": "n8n-nodes-frappe-learning.frappeLearning",
"typeVersion": 1,
"name": "Enroll in Course",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
progress is not exposed: the server owns it. Read it back with Get Many once lessons are completed.
Quiz — create
{
"parameters": {
"resource": "quiz",
"operation": "create",
"title": "Variables Quiz",
"passing_percentage": 60,
"additionalFields": {
"questions": {
"question": [
{ "question": "LMS-QST-00001", "marks": 2 },
{ "question": "LMS-QST-00002", "marks": 3 }
]
},
"max_attempts": 2,
"shuffle_questions": true,
"show_answers": true,
"duration": "00:15:00"
}
},
"type": "n8n-nodes-frappe-learning.frappeLearning",
"typeVersion": 1,
"name": "Create Quiz",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
Each row of Questions links to an existing LMS Question record — the node does not create the questions themselves. total_marks is not exposed: the LMS sums the marks column.
On Update, an empty Questions collection is dropped rather than sent as [], so you cannot accidentally zero out a quiz you meant to rename. To actually change the question list, supply the full new list.
Assignment Submission — evaluate
{
"parameters": {
"resource": "assignmentSubmission",
"operation": "evaluate",
"documentId": {
"__rl": true,
"mode": "name",
"value": "ASG-SUB-00001"
},
"status": "Pass",
"evaluationOptions": {
"comments": "<p>Clean solution. Watch the edge case on empty input.</p>",
"evaluator": {
"__rl": true,
"mode": "name",
"value": "teacher@acme.io"
}
}
},
"type": "n8n-nodes-frappe-learning.frappeLearning",
"typeVersion": 1,
"name": "Grade Assignment",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
A submission is a plain document, not a submittable one, so grading is a field update — there is no docstatus transition to worry about, and you can re-grade freely. Status is one of Pass, Fail, Not Graded, Not Applicable; assignments with grade_assignment off stay at Not Applicable.
Certificate — create
{
"parameters": {
"resource": "certificate",
"operation": "create",
"member": {
"__rl": true,
"mode": "name",
"value": "student@example.com"
},
"issue_date": "2026-02-14",
"template": "Certificate",
"additionalFields": {
"course": {
"__rl": true,
"mode": "name",
"value": "python-for-beginners"
},
"batch_name": {
"__rl": true,
"mode": "name",
"value": "python-january-2026"
},
"expiry_date": "2029-02-14",
"published": true
}
},
"type": "n8n-nodes-frappe-learning.frappeLearning",
"typeVersion": 1,
"name": "Issue Certificate",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
Create here means upsert. Before inserting, the node looks for a certificate on the same (member, course) pair and rewrites that one if it exists, so running the workflow twice does not issue a second certificate to the same member. That is the behaviour of lms.lms.api.save_certificate_details, the method the LMS front end uses, reproduced on /api/resource.
Why not call
save_certificate_detailsitself? Because it is unusable from an automation account on most sites. Its last act before saving isevaluator = get_evaluator(course, batch_name) or frappe.session.user, and it never reads an evaluator from its arguments. As soon as the course has no evaluator assigned, it therefore stores your API user in a field that Links toCourse Evaluator— and Frappe answers417 Could not find Evaluator: <user>, because an API account is not a course evaluator. Going through/api/resourcecosts nothing in permissions:ModeratorandBatch Evaluator, the two roles the method admits, both holdcreateonLMS Certificate, alongsideSystem ManagerandCourse Creator.
Since the write is a plain one, evaluator is offered on create as well as update. Leave it empty and the certificate simply has no evaluator; set it to the name of a Course Evaluator record — the evaluator's e-mail — to sign the certificate.
template is a Link to a Print Format, the document that decides how the certificate renders. Frappe Learning ships exactly one, named Certificate, which is the default here. Note that the batch field is called batch_name, not batch.
The member must already be enrolled. validate_criteria() checks the enrollment before issuing: with batch_name set, the member needs an LMS Batch Enrollment for that batch; with course set, an LMS Enrollment for that course — batch takes precedence when both are given. Issuing without one fails with « Certification cannot be issued as the member is not enrolled in this course. » Put a Course Enrollment — create ahead of this node in the workflow. Note also that course and batch_name sit in Additional Fields but are not truly optional: only a Moderator, Course Creator or Batch Evaluator may issue a certificate attached to neither.
Course Progress — get many
Every lesson one student has completed on a course:
{
"parameters": {
"resource": "courseProgress",
"operation": "getAll",
"returnAll": true,
"options": {
"fields": "name,member,course,chapter,lesson,status,creation",
"filters": "[[\"course\",\"=\",\"python-for-beginners\"],[\"member\",\"=\",\"student@example.com\"],[\"status\",\"=\",\"Complete\"]]",
"sortField": "creation",
"sortOrder": "asc"
}
},
"type": "n8n-nodes-frappe-learning.frappeLearning",
"typeVersion": 1,
"name": "Student Progress",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
Sorting by creation ascending gives the order the student actually worked through the course, which the outline order will not tell you.
Delete
Any writable resource, given its document ID:
{
"parameters": {
"resource": "course",
"operation": "delete",
"documentId": {
"__rl": true,
"mode": "name",
"value": "python-for-beginners"
}
},
"type": "n8n-nodes-frappe-learning.frappeLearning",
"typeVersion": 1,
"name": "Delete Course",
"position": [0, 0],
"credentials": { "frappeApi": { "id": "1", "name": "Frappe account" } }
}
The node outputs { "success": true, "doctype": "LMS Course", "name": "python-for-beginners" }.
Deleting a course, batch, chapter or lesson is cascading, because that is the only way it works in Frappe Learning: delete_course removes the enrollments, progress rows, certificates, reviews and chapters attached to it, and unlinks its quizzes and assignments rather than deleting the graded work. This is not something the node adds — it is what the LMS itself does when you delete from its interface. There is no undo.
Compatibility
Tested against n8n 2.x and Frappe Framework v15 and v16 with Frappe Learning (lms). Frappe Learning v2.0 or later is required, because of the enrollment doctypes described above.
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 the standard /api/resource REST endpoints, the node calls six whitelisted methods in lms.lms.api: upsert_chapter, create_lesson, delete_chapter, delete_lesson, delete_course and delete_batch. Those are part of the LMS's own front-end API, so they track its version rather than the framework's — a much older lms release may not have upsert_chapter, which is where chapter creation would fail first.
The payments app is required only for paid courses and batches; the LMS raises an explicit error pointing at its documentation if you set paid_course or paid_batch without it.
Resources
- n8n community nodes documentation
- Frappe REST API documentation
- Frappe Learning documentation
- Frappe Learning source
- Shared credential architecture
Version history
0.1.0
Initial release. Frappe Learning node with the Course, Chapter, Lesson, Batch, Course Enrollment, Batch Enrollment, Quiz, Quiz Submission, Assignment, Assignment Submission, Certificate and Course Progress resources, outline-aware chapter and lesson handling and deduplicated certificate issuance through lms.lms.api, the Evaluate grading operation, 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.