Transform Learning with a Powerful Enterprise LMS

Learn more
Moodle

Backup and Restore Support for Moodle Activity Modules

The four files an activity module needs for course backup, import and Duplicate: backup task and structure step, restore task and step, id mapping, files, link decoding and testing in Moodle 5.3.

Learn more about this topic with AI

Gemini opens with the prompt copied - paste it into the chat to start.

Diagram-style illustration of a Moodle activity module's data flowing through backup and restore steps into a new course

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

FileClassExtendsJob
backup_logbook_activity_task.class.phpbackup_logbook_activity_taskbackup_activity_taskAdds your steps; encodes links to your scripts
backup_logbook_stepslib.phpbackup_logbook_activity_structure_stepbackup_activity_structure_stepDescribes the XML tree and its data sources
restore_logbook_activity_task.class.phprestore_logbook_activity_taskrestore_activity_taskAdds restore steps; declares link decode rules and log rules
restore_logbook_stepslib.phprestore_logbook_activity_structure_steprestore_activity_structure_stepMaps 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. Pass true as the fourth argument when files are stored against that item id, so add_related_files() can move them.
  • get_mappingid($itemname, $oldid) returns the new id for a user, group or one of your own items. It returns false when 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 as logbookid.

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.

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.xml and upgrade.php needs 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) before add_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

  1. Backup API - Moodle
  2. Restore API - Moodle
  3. mod_choice backup and restore classes (Moodle 5.3.0) - Moodle
  4. backup_activity_task (Moodle 5.3.0) - Moodle
  5. admin/cli/backup.php (Moodle 5.3.0) - Moodle
Add as a preferred source on Google

Add All-in-One LMS as a preferred source to see more of our articles in Google’s Top Stories.

Learn more about this topic with AI

Gemini opens with the prompt copied - paste it into the chat to start.

Keep reading

Before you go - get a free LMS audit

Tell us which LMS you use and what is not working. An engineer replies within one business day.

Choose which optional cookies we may use. You can change this at any time from "Cookie preferences" in the footer. Cookie policy · Privacy policy