Moodle services
Moodle plugin development
All-in-One LMS writes, repairs and maintains custom plugins for Moodle™ sites run by companies, universities and training providers. Each plugin is built on the plugin type that fits the need and ships with its own upgrade script, permissions, privacy declarations and automated tests. This page explains how Moodle plugin development is done properly, so you can judge any proposal, including ours, before you sign it.
What Moodle plugin development involves
Moodle plugin development is the work of adding a feature to Moodle LMS as a self-contained package. The package sits in one of Moodle's defined plugin folders and talks to core only through published APIs. It carries its own database tables, permissions, language strings and upgrade steps, so it can be updated, disabled or removed without touching Moodle's own code.
A feature written into core files lasts until the next release overwrites it. A plugin keeps working while the site takes security fixes underneath it.
When a plugin is the right tool
Not every request needs code. Many "custom features" turn out to be a setting, a role change or a custom field Moodle already has, and others are presentation only and belong in a theme. Our page on customizing an existing Moodle site explains how we choose between those routes.
A plugin is the answer when the site needs new behavior or new data: a workflow Moodle lacks, a record it does not keep, or a connection to another system. The first design decision is then the plugin type, because the type decides which Moodle APIs the code gets for free.
Plugin types and when each fits
Moodle 5.2 defines more than forty plugin types. These are the ones most projects use. Folder paths are relative to the Moodle code; from Moodle 5.1 they sit inside the public directory.
| Type (prefix, folder) | Choose it when | Typical example |
|---|---|---|
Local plugin (local, /local) | The feature is site-wide and is not an activity, a block or a report | A nightly sync of job roles from HR |
Activity module (mod, /mod) | Learners do something in a course that is graded, tracked for completion and backed up | A skills checklist signed off by an assessor |
Block (block, /blocks) | A compact panel should appear on the dashboard or beside course content | A "my overdue training" panel |
Enrolment plugin (enrol, /enrol) | Course access is granted or removed by a rule or an outside system | Enrollment driven by a booking system |
Authentication plugin (auth, /auth) | Users sign in against an external identity source | Sign-in against a legacy member database |
Report (report, /report) | Admins or teachers need a fixed view of site or course data | Compliance status by department |
Course format (format, /course/format) | The course page itself needs a different layout or navigation | A step-by-step onboarding path |
Question type (qtype, /question/type) | Quizzes need an interaction the core question types lack | Labeling points on a technical drawing |
Admin tool (tool, /admin/tool) | Administrators need a utility or a maintenance function | Rule-based archiving of old courses |
Availability condition (availability, /availability/condition) | An activity or section should open only when a custom rule is met | Access after a manager's approval |
Payment gateway (paygw, /payment/gateway) | Moodle's payment subsystem needs a processor it does not include | A regional card processor |
AI provider or placement (aiprovider, aiplacement) | A model, or a new AI feature, has to plug into Moodle's AI subsystem | Moodle 4.5 and later only |
The usual mistake is putting everything in a local plugin because it is the easiest type to start. An activity built that way has to reimplement grading, completion, backup and restore, all of which an activity module inherits.
Four types have their own pages on this site.
Authentication. An authentication plugin sits in the login flow, so an error locks everyone out at once. Protocols, account matching and testing are covered under single sign-on integration.
Enrollment on payment. Selling a course normally uses Moodle's Enrolment on payment method together with a payment gateway plugin, and PayPal is included as standard. Adding another processor, or deciding that an external store is the better route, is described on the payment gateway integration page.
Reports and dashboards. A stand-alone report plugin is often not the first choice, because Moodle's Report builder accepts custom report sources from any plugin. That decision is covered under dashboards and custom reports.
AI features. Since Moodle 4.5, AI work is split into provider plugins that connect a model and placement plugins that put an action in front of users. Configuration, private models and data controls are on the AI integration page.
Anatomy of a plugin
A plugin is a folder, and its full name joins the type prefix to the folder name: local_trainingsync, mod_skillcheck. Moodle calls this the frankenstyle name. Database tables, settings, capabilities and language strings all carry it as a prefix, which is what stops two plugins from colliding.
Inside the folder, Moodle looks for files with fixed names. Two are required. version.php holds the metadata read at install and upgrade time: the component name, a version number in YYYYMMDDXX form, the minimum Moodle version, the supported branches and any dependencies. lang/en/ holds the English strings, starting with pluginname.
The other files appear as the plugin needs them:
db/install.xmldefines the tables created at first install. Moodle requires it to be edited with the built-in XMLDB editor, not by hand.db/upgrade.phpholds the steps that take an older installed version to the current one. Every schema change needs a matching step here.db/access.phpdeclares the plugin's capabilities.db/services.php,db/events.php,db/tasks.phpanddb/hooks.phpregister web service functions, event observers, scheduled tasks and hook callbacks.classes/holds the PHP classes.settings.php,templates/andamd/hold admin settings, Mustache templates and JavaScript modules.tests/holds PHPUnit tests and, undertests/behat/, browser tests.
Classes placed in classes/ under a namespace that starts with the component name are autoloaded, so no file needs a manual require. Standard sub-namespaces such as external, privacy, task, event and output tell the next developer where to look. Older plugins keep their logic in lib.php and locallib.php, which Moodle's documentation now treats as legacy.
// local/trainingsync/version.php
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'local_trainingsync';
$plugin->version = 2026100500; // YYYYMMDDXX.
$plugin->requires = 2024100700; // Moodle 4.5.
$plugin->supported = [405, 503]; // Branches 4.5 to 5.3.
$plugin->maturity = MATURITY_STABLE;
$plugin->release = '1.4.0';
// local/trainingsync/db/access.php
$capabilities = [
'local/trainingsync:runsync' => [
'riskbitmask' => RISK_DATALOSS,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [
'manager' => CAP_ALLOW,
],
],
];
Functions exposed to other systems are declared in db/services.php, a subject with its own page: custom web services and API development.
Capabilities and the Privacy API
A capability is a named permission in the form plugintype/pluginname:action. It is declared in db/access.php with a read or write type, the context level where it is checked, risk flags and a default for each role archetype. The code then calls require_capability() or has_capability() against the right context before it shows or does anything.
With proper capabilities, an administrator can give the feature to a department manager on the roles screen. A common failure we see is a plugin that tests for "site administrator" in code, so nobody else can be given access without a developer.
The Privacy API is the other piece that gets skipped. Each plugin provides a class at classes/privacy/provider.php. A plugin that holds no personal data implements the null provider and states why. A plugin that does hold personal data describes each table and each external system it sends data to, and implements the methods that find, export and delete a person's records.
Moodle's data request tools call those methods. When a plugin stores personal data and declares nothing, an export or deletion request leaves that data out.
Coding standards, automated tests and CI
Moodle publishes a coding style that builds on PSR-12 and adds its own naming and documentation rules. It is enforced with PHP_CodeSniffer and Moodle's own ruleset, moodle-cs, usually called the code checker. We run it on every commit.
Tests come in two kinds. PHPUnit tests exercise the plugin's logic against a separate test database and data folder, so they never touch real records. Behat tests are plain-language scenarios that drive a browser through the feature, which is how you prove that a learner can submit and a manager can approve.
Both run inside moodle-plugin-ci, the tool Moodle HQ maintains for testing a plugin in a CI service such as GitHub Actions. One run covers:
- PHP linting and the PHP Mess Detector
- the code checker and the PHPDoc checker
- validation of the plugin's file structure
- a check that upgrade steps and savepoints are consistent
- Mustache template linting and Grunt tasks
- the PHPUnit and Behat suites
We run that pipeline as a matrix: each supported Moodle branch, the PHP versions those branches allow, and both PostgreSQL and MySQL or MariaDB. SQL that works on one database and fails on the other is a classic plugin defect. Test effort goes where a mistake costs money: enrollment rules, grade calculations, sync logic and upgrade steps.
Describe the plugin you need
How we keep a plugin working across Moodle releases
Moodle HQ ships a major release every six months. A plugin stays healthy across them when four things hold.
It says what it supports. requires blocks installation on a Moodle version that is too old, and supported states the range of branches the release was tested on.
It reacts to deprecations early. Moodle removes old functions in stages tied to its LTS releases: a function is deprecated, then finally deprecated in the first major version after an LTS, then removed in the series after that. Deprecated calls raise notices when developer debugging is on, so our CI fails on them long before removal.
It follows the platform's structural changes. Moodle 4.3 introduced the Hooks API as the replacement for many lib.php callbacks. Moodle 5.0 moved the Boost theme to Bootstrap 5.3, so templates and JavaScript written for Bootstrap 4 need updating before the compatibility layer reaches final deprecation in Moodle 6.0. And from Moodle 5.1 plugins live inside the public folder, which can break code that builds file paths by hand.
It is retested against each new branch. When a release is published, the matrix gains a column and the upgrade notes are read against the plugin's code. That makes the plugin a known quantity when you plan your next Moodle upgrade.
Publishing a plugin: from the Plugins directory to Moodle Marketplace
Most client plugins stay private. Publishing makes sense when you want other institutions to use the plugin, or intend to sell it, and the route has changed. Moodle's documentation now states that plugins are found in Moodle Marketplace, which has replaced the old Plugins directory, and it marks the earlier contribution pages as legacy guidance. As of October 2026 the documented steps are: sign in to Marketplace, choose "Submit a plugin", decide whether the listing is free or paid, and upload a ZIP with a unique frankenstyle component name. The submission then joins a review queue.
Moodle's FAQ says free plugins are reviewed once, with no stated timeframe, and paid plugins are reviewed on submission and annually, typically in two to four weeks. Passing review does not publish anything: the provider completes the plugin page and publishes it. Administrators then download the ZIP and install it through site administration, because the directory's one-click install has not been carried over.
Moodle's contribution checklist, written for the directory, is still the clearest public statement of what reviewers expect: a GPL version 3 or later license, a public repository and issue tracker, frankenstyle prefixes throughout, SQL that runs on PostgreSQL as well as MySQL, backup and restore in activity modules, and a Privacy API implementation wherever data leaves the site. The same guidance noted that almost all plugins came back from first review needing more work.
We prepare the submission and respond to review comments. Approval and its timing belong to Moodle's reviewers, so we do not promise either.
Taking over or repairing an existing plugin
Much plugin work starts with code somebody else wrote. The developer has moved on, a community plugin stopped at an old Moodle version, or a plugin works but blocks the next upgrade.
We start by reading the code against a fixed list: capability and session-key checks before every action, query placeholders instead of SQL built from user input, upgrade steps that match the install schema, no dependence on an edited core file, a privacy provider, and tests.
The outcome is a written assessment with one of three recommendations. Repair, when the structure is sound and the faults are local. Refactor in stages, when the plugin works but cannot be tested or upgraded as it stands. Or rebuild, when the plugin type or the data model is wrong, with upgrade steps that carry existing data into the new structure.
For an abandoned community plugin there is one more choice: maintain a private fork, or send fixes back to the original project. The GPL allows both.
How a plugin project runs
- Requirements call. You describe the need to an engineer. An NDA is signed before any code, data or credentials change hands.
- Design note. A short document names the plugin type, the data model, the capabilities, the personal data involved and the Moodle versions to support, for your approval.
- Build in your repository. Work is done in version control against a staging copy of your site, never against production.
- Tests and pipeline. PHPUnit and Behat tests are written with the feature, and the CI matrix runs on every change.
- Review and demonstration. A second engineer reviews the code, then we show you the plugin working on staging.
- Release and handover. You receive the install package, admin notes, and a list of what to recheck at each Moodle release.
What drives cost and timeline
The plugin type is the biggest factor. An activity module with grading, completion, backup and restore is several times the work of a block that displays data Moodle already holds. After that, the estimate moves with the number of screens and roles, the external systems involved, and the volume of existing data to migrate.
Three quieter factors also count: how many Moodle versions the plugin must support, how deep the automated tests go, and whether the plugin will be published. A block or a small local plugin is typically days to a couple of weeks of work. Activity modules and plugins that talk to outside systems run to several weeks.
Why All-in-One LMS for plugin work
Learning platforms are the only thing we build. The same engineers write plugins for other platforms, described under our platform-neutral LMS plugin development service, which helps when a Moodle plugin has to exchange data with a WordPress or custom system. Plugin work is one part of our wider Moodle development services.
The code is yours. It sits in your repository from the first commit, with documentation, and nothing in it depends on a license or a server of ours. If you expect a steady stream of plugin work, a dedicated Moodle developer on an hourly, part-time or full-time basis can cost less than a series of fixed-price projects.
Frequently asked questions
Will the plugin still work after the next Moodle upgrade?
It is built to. The plugin uses only supported APIs, declares the Moodle branches it was tested on, and runs in a CI matrix that fails when a deprecated function is called. No one can promise that a future release changes nothing, so the handover lists what to recheck, and retesting against each new release is a small recurring task.
Who owns the plugin, and what license applies?
You own the code we write for you. Moodle is licensed under the GPL, and Moodle's contribution checklist requires GPL version 3 or later for plugins that are published. A plugin used only on your own site does not have to be published or shared with anyone.
Can a plugin change a core Moodle page without editing core?
Often, yes. Hooks, event observers, theme template overrides, availability conditions and custom fields cover most cases. Sometimes Moodle offers no extension point for a specific change. When that happens we say so, and propose either a different design or a request to Moodle HQ for a new hook, not a core edit.
Do we have to publish our plugin on Moodle Marketplace?
No. Publishing is a choice, and most client plugins are installed on one site from a private repository. If you do want a public or paid listing, we prepare the submission and handle review comments. Approval and timing are decided by Moodle's reviewers.
Which Moodle versions will the plugin support?
Whichever you run now, plus the release you plan to move to. Today that usually means Moodle 4.5 LTS and the 5.x series up to Moodle 5.3 LTS (October 2026). A wider range is possible but adds test time, because every extra branch is another column in the test matrix.
Start with the requirement, not the code
Send a description of what the plugin should do, your Moodle version and any existing code. An engineer replies within one business day with questions or a proposed call.