frappe-learning

n8n node for Frappe Learning (LMS): courses, chapters, lessons, batches, enrollments, quizzes, assignments and certificates

Package Information

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

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

  1. In your Frappe site, open the user you want n8n to act as (/app/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 /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 Chapter and Course Lesson — without the LMS prefix — 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 Enrollment is what older versions called LMS Batch Membership. LMS Batch Enrollment did not exist at all: batch students were rows of a Batch Student child table on LMS Batch — issue frappe/lms#724 asked for a standalone doctype "like Course Enrolment", and lms.patches.v2_0.migrate_batch_student_data copies the old rows over before Batch Student is 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.chapters is a table of Chapter Reference rows. It carries both which chapters belong to the course and in what order.
  • Course Chapter.lessons is a table of Lesson Reference rows, same deal for lessons.
  • The Course Chapter.course and Course Lesson.chapter fields are only back-links, used for counters and fetch_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:

  1. checks you may do it at all — validate_owner() requires Moderator or Batch Evaluator to enroll anyone other than yourself, whatever the doctype permissions say;
  2. refuses duplicates, full batches (seat_count), restricted batches (allow_self_enrollment off), and paid batches without a received payment;
  3. opens an LMS Enrollment for every course in the batch;
  4. 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 Lesson is autonamed format:{####} {title}, and create_lesson inserts under the placeholder title, so the new lesson comes back as 0003 Untitled lesson even though the PUT right 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 to 0003 Your title once the lesson has been untouched for 24 hours. A workflow that stores the returned name and reuses it a day later will get a 404. Store the title (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_details itself? Because it is unusable from an automation account on most sites. Its last act before saving is evaluator = 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 to Course Evaluator — and Frappe answers 417 Could not find Evaluator: <user>, because an API account is not a course evaluator. Going through /api/resource costs nothing in permissions: Moderator and Batch Evaluator, the two roles the method admits, both hold create on LMS Certificate, alongside System Manager and Course 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

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.

Discussion