Adding Moodle backup restore API plugin support to an activity module takes four files in mod/yourplugin/backup/moodle2/: a backup task, a backup structure step, a restore task and a restore structure step, plus FEATURE_BACKUP_MOODLE2 => true in the module's _supports() function. The backup step describes your tables as a tree of nested elements; the restore step walks the same tree, inserts rows and maps every old id to its new id.
Without that support, your activity is silently left out of course backups, course imports and the Duplicate action, which Moodle builds on the same backup and restore machinery. This guide explains how the pieces fit, shows a minimal implementation for a hypothetical mod_logbook, and lists the mistakes that break restores in production.
Checked against the Moodle 5.3.0 source (public/backup/, mod_choice, mod_glossary and mod_assign) and the Moodle developer documentation for the Backup and Restore APIs, October 2026.
How the backup and restore plans work
Moodle backs up a course by executing a backup plan. The plan holds tasks (one for the course root, one per section, one per activity, one per block) and each task holds steps. Restore mirrors this with a restore plan, restore tasks and restore steps. As a module author you write one activity task for each direction. Core adds the generic steps around yours.
In 5.3, backup_activity_task already adds steps for module.xml, roles, filters, comments, user completion, logs, calendar events, grade items, grades, advanced grading, grade history, competencies and xAPI state. Your code only has to export the data in your own tables and files. If your module stores grades through the gradebook API, completion through core completion and events through the calendar API, the core steps carry them for you.
The planner decides whether to include your module by calling plugin_supports('mod', $modname, FEATURE_BACKUP_MOODLE2). The same check gates the Duplicate item in the activity action menu, so a module without backup support cannot be duplicated either.
The four files and what each one does
| File | Class | Extends | Job |
|---|---|---|---|
backup_logbook_activity_task.class.php | backup_logbook_activity_task | backup_activity_task | Adds your steps; encodes links to your scripts |
backup_logbook_stepslib.php | backup_logbook_activity_structure_step | backup_activity_structure_step | Describes the XML tree and its data sources |
restore_logbook_activity_task.class.php | restore_logbook_activity_task | restore_activity_task | Adds restore steps; declares link decode rules and log rules |
restore_logbook_stepslib.php | restore_logbook_activity_structure_step | restore_activity_structure_step | Maps XML paths to process_*() methods that insert rows |
An optional backup_logbook_settingslib.php holds module-specific backup settings. Most modules do not need one. From Moodle 5.1 the web root is public/, so the folder lives at public/mod/logbook/backup/moodle2/ in the repository, while $CFG->dirroot still resolves it as /mod/logbook/.... If you are new to plugin layout, Moodle plugin types explained covers where each type sits.
The backup side: task and structure step
backup_activity_task declares two abstract methods, define_my_settings() and define_my_steps(), and a static encode_content_links() that throws a coding_exception unless you override it. The example below is not run on a live site; test on staging first.
// mod/logbook/backup/moodle2/backup_logbook_activity_task.class.php
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/mod/logbook/backup/moodle2/backup_logbook_stepslib.php');
class backup_logbook_activity_task extends backup_activity_task {
protected function define_my_settings() {
}
protected function define_my_steps() {
$this->add_step(new backup_logbook_activity_structure_step('logbook_structure', 'logbook.xml'));
}
public static function encode_content_links($content) {
global $CFG;
$base = preg_quote($CFG->wwwroot, '/');
$search = '/(' . $base . '\/mod\/logbook\/view.php\?id\=)([0-9]+)/';
return preg_replace($search, '$@LOGBOOKVIEWBYID*$2@$', $content);
}
}
The structure step builds backup_nested_element objects, links them with add_child(), sets sources and annotates ids and files:
// mod/logbook/backup/moodle2/backup_logbook_stepslib.php
class backup_logbook_activity_structure_step extends backup_activity_structure_step {
protected function define_structure() {
$userinfo = $this->get_setting_value('userinfo');
$logbook = new backup_nested_element('logbook', ['id'], [
'name', 'intro', 'introformat', 'timeopen', 'timeclose', 'timemodified']);
$entries = new backup_nested_element('entries');
$entry = new backup_nested_element('entry', ['id'], [
'userid', 'text', 'textformat', 'timecreated']);
$logbook->add_child($entries);
$entries->add_child($entry);
$logbook->set_source_table('logbook', ['id' => backup::VAR_ACTIVITYID]);
if ($userinfo) {
$entry->set_source_table('logbook_entry', ['logbookid' => backup::VAR_PARENTID], 'id ASC');
}
$entry->annotate_ids('user', 'userid');
$logbook->annotate_files('mod_logbook', 'intro', null);
$entry->annotate_files('mod_logbook', 'entry', 'id');
return $this->prepare_activity_structure($logbook);
}
}
Three details matter. Wrap learner data in the userinfo check so "Include users" off really excludes it. Use annotate_ids('user', ...) on every user id field, otherwise the user record is not added to the backup and restore cannot map it. Use set_source_sql() instead of set_source_table() when a source needs a join, and keep parameters as backup constants or '../../id' style paths, never values built from strings.
The restore side: task and structure step
restore_activity_task requires define_my_settings() and define_my_steps(), and its static define_decode_contents(), define_decode_rules() and define_restore_log_rules() all throw a coding_exception if you leave them out. Return an empty array when you have nothing to declare. The example is not run on a live site; test on staging first.
// mod/logbook/backup/moodle2/restore_logbook_activity_task.class.php
require_once($CFG->dirroot . '/mod/logbook/backup/moodle2/restore_logbook_stepslib.php');
class restore_logbook_activity_task extends restore_activity_task {
protected function define_my_settings() {
}
protected function define_my_steps() {
$this->add_step(new restore_logbook_activity_structure_step('logbook_structure', 'logbook.xml'));
}
public static function define_decode_contents() {
return [new restore_decode_content('logbook', ['intro'], 'logbook')];
}
public static function define_decode_rules() {
return [new restore_decode_rule('LOGBOOKVIEWBYID', '/mod/logbook/view.php?id=$1', 'course_module')];
}
public static function define_restore_log_rules() {
return [];
}
}
The structure step declares restore_path_element objects and one process_<name>() method per path:
// mod/logbook/backup/moodle2/restore_logbook_stepslib.php
class restore_logbook_activity_structure_step extends restore_activity_structure_step {
protected function define_structure() {
$paths = [new restore_path_element('logbook', '/activity/logbook')];
if ($this->get_setting_value('userinfo')) {
$paths[] = new restore_path_element('logbook_entry', '/activity/logbook/entries/entry');
}
return $this->prepare_activity_structure($paths);
}
protected function process_logbook($data) {
global $DB;
$data = (object) $data;
$data->course = $this->get_courseid();
$data->timeopen = $this->apply_date_offset($data->timeopen);
$data->timeclose = $this->apply_date_offset($data->timeclose);
$newitemid = $DB->insert_record('logbook', $data);
$this->apply_activity_instance($newitemid);
}
protected function process_logbook_entry($data) {
global $DB;
$data = (object) $data;
$oldid = $data->id;
$data->logbookid = $this->get_new_parentid('logbook');
$data->userid = $this->get_mappingid('user', $data->userid);
$newitemid = $DB->insert_record('logbook_entry', $data);
$this->set_mapping('logbook_entry', $oldid, $newitemid, true);
}
protected function after_execute() {
$this->add_related_files('mod_logbook', 'intro', null);
$this->add_related_files('mod_logbook', 'entry', 'logbook_entry');
}
}
Id mapping, files and dates
Every id in a backup is an id from the source site. Restore keeps a temporary mapping table, and three calls work with it:
set_mapping($itemname, $oldid, $newid, $restorefiles)records the new id. Passtrueas the fourth argument when files are stored against that item id, soadd_related_files()can move them.get_mappingid($itemname, $oldid)returns the new id for a user, group or one of your own items. It returnsfalsewhen there is no mapping, so decide whether to skip the row or keep it.get_new_parentid($itemname)returns the new id of the parent element, used for foreign keys such aslogbookid.
apply_date_offset() shifts dates when a teacher restores with a new course start date. Apply it to every date a teacher would expect to move, such as open and close times, and not to audit timestamps such as timecreated. apply_activity_instance() must run right after the instance row is inserted, because later steps need the new instance id.
Content links and subplugins
Links between activities break after restore unless you encode them. encode_content_links() replaces absolute URLs with tokens such as $@LOGBOOKVIEWBYID*42@$; on restore, the rules from define_decode_rules() turn them back into URLs with the new course module id, and define_decode_contents() lists which fields of which tables to scan. Every token you encode needs a matching decode rule.
If your module has subplugins, call add_subplugin_structure('logbooktype', $element, true) in the backup step and add_subplugin_structure('logbooktype', $element) in the restore step. Each subplugin then ships classes extending backup_subplugin and restore_subplugin with a define_<element>_subplugin_structure() method. mod_assign does this for its assignsubmission and assignfeedback types, and mod_bigbluebuttonbn for bbbext. Other plugin types can join a course backup through classes such as backup_local_plugin, backup_format_plugin and backup_enrol_plugin.
Testing backup and restore support
Test three paths before release: a full course backup and restore into a new course, an import into an existing course, and Duplicate on a single activity. In 5.3, admin/cli/backup.php accepts --cmid as well as --courseid, which makes it easy to back up one activity from a script; admin/cli/restore_backup.php takes --file and --categoryid.
For PHPUnit, the core pattern is a backup_controller created with backup::TYPE_1ACTIVITY, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO and backup::MODE_IMPORT, followed by execute_plan(), then a restore_controller with execute_precheck() and execute_plan(). That is the same sequence the core Duplicate action uses. Assert row counts, mapped user ids, file counts per area and decoded links. Run it with users included and excluded.
Common mistakes that break restores
- A new column that is not in the backup. Every schema change in
install.xmlandupgrade.phpneeds a matching field in the nested element, or the column restores empty. - Old backups that lack the new field. Restore code meets backups made by earlier versions of your plugin. Check with
isset()and set a default rather than assuming the field exists. - User ids without
annotate_ids(). The row restores pointing at the wrong user or none. - Files without a mapping. A file area keyed by item id needs
set_mapping(..., true)beforeadd_related_files(). - Missing decode rules. Encoded tokens appear in restored text as raw
$@...@$strings. - Large user data. Big file areas make course backups slow. Our large course backup troubleshooting guide covers the site-side fixes, and course backups never replace a full site backup of code, moodledata and the database.
How we can help
We build and maintain custom Moodle activity modules with backup, restore, privacy and upgrade paths that survive real course rollovers. See our LMS plugin development service or the Moodle platform page, and if you are starting a new plugin, our Moodle local plugin tutorial shows the base structure and our Moodle guide maps the rest of the platform.
Frequently asked questions
Where do Moodle backup and restore files go in a plugin?
In the plugin's backup/moodle2/ folder, for example mod/foobar/backup/moodle2/. The same folder holds both the backup and the restore classes. From Moodle 5.1 that is public/mod/foobar/backup/moodle2/ in the repository.
Why is my activity missing from course backups?
The backup planner includes a module only when plugin_supports('mod', $modname, FEATURE_BACKUP_MOODLE2) is true and the backup task class exists. Add FEATURE_BACKUP_MOODLE2 => true to your module's _supports() function and check the class and file names follow the backup_<module>_activity_task pattern.
Does Moodle back up grades and completion for my module automatically?
Yes, when your module uses the gradebook and core completion APIs. backup_activity_task adds core steps for grade items, grades, grade history, user completion, logs, calendar events, filters, comments and competencies. Your structure step only needs your own tables and file areas.
How do I test a Moodle activity backup from the command line?
In Moodle 5.3, admin/cli/backup.php accepts --cmid to back up a single course module, as well as --courseid. admin/cli/restore_backup.php restores a .mbz file with --file and --categoryid. For automated tests, use backup_controller and restore_controller in PHPUnit.
Free 30-minute consultation
Not sure where to start? Talk to an LMS engineer.
Tell us what platform you run and what you need. You’ll get honest, practical advice - even if the answer is that you don’t need us.
Sources & references
- Backup API - Moodle
- Restore API - Moodle
- mod_choice backup and restore classes (Moodle 5.3.0) - Moodle
- backup_activity_task (Moodle 5.3.0) - Moodle
- admin/cli/backup.php (Moodle 5.3.0) - Moodle
Learn more about this topic with AI
Gemini opens with the prompt copied - paste it into the chat to start.