{"id":"229fc902-b51b-4e9c-b6d8-1d8a43e258f4","shortId":"KZryQe","kind":"skill","title":"moodle-external-api-development","tagline":"This skill guides you through creating custom external web service APIs for Moodle LMS, following Moodle's external API framework and coding standards.","description":"# Moodle External API Development\n\nThis skill guides you through creating custom external web service APIs for Moodle LMS, following Moodle's external API framework and coding standards.\n\n## When to Use This Skill\n\n- Creating custom web services for Moodle plugins\n- Implementing REST/AJAX endpoints for course management\n- Building APIs for quiz operations, user tracking, or reporting\n- Exposing Moodle functionality to external applications\n- Developing mobile app backends using Moodle\n\n## Core Architecture Pattern\n\nMoodle external APIs follow a strict three-method pattern:\n\n1. **`execute_parameters()`** - Defines input parameter structure\n2. **`execute()`** - Contains business logic\n3. **`execute_returns()`** - Defines return structure\n\n## Step-by-Step Implementation\n\n### Step 1: Create the External API Class File\n\n**Location**: `/local/yourplugin/classes/external/your_api_name.php`\n\n```php\n<?php\nnamespace local_yourplugin\\external;\n\ndefined('MOODLE_INTERNAL') || die();\nrequire_once(\"$CFG->libdir/externallib.php\");\n\nuse external_api;\nuse external_function_parameters;\nuse external_single_structure;\nuse external_value;\n\nclass your_api_name extends external_api {\n    \n    // Three required methods will go here\n    \n}\n```\n\n**Key Points**:\n- Class must extend `external_api`\n- Namespace follows: `local_pluginname\\external` or `mod_modname\\external`\n- Include the security check: `defined('MOODLE_INTERNAL') || die();`\n- Require externallib.php for base classes\n\n### Step 2: Define Input Parameters\n\n```php\npublic static function execute_parameters() {\n    return new external_function_parameters([\n        'userid' => new external_value(PARAM_INT, 'User ID', VALUE_REQUIRED),\n        'courseid' => new external_value(PARAM_INT, 'Course ID', VALUE_REQUIRED),\n        'options' => new external_single_structure([\n            'includedetails' => new external_value(PARAM_BOOL, 'Include details', VALUE_DEFAULT, false),\n            'limit' => new external_value(PARAM_INT, 'Result limit', VALUE_DEFAULT, 10)\n        ], 'Options', VALUE_OPTIONAL)\n    ]);\n}\n```\n\n**Common Parameter Types**:\n- `PARAM_INT` - Integers\n- `PARAM_TEXT` - Plain text (HTML stripped)\n- `PARAM_RAW` - Raw text (no cleaning)\n- `PARAM_BOOL` - Boolean values\n- `PARAM_FLOAT` - Floating point numbers\n- `PARAM_ALPHANUMEXT` - Alphanumeric with extended chars\n\n**Structures**:\n- `external_value` - Single value\n- `external_single_structure` - Object with named fields\n- `external_multiple_structure` - Array of items\n\n**Value Flags**:\n- `VALUE_REQUIRED` - Parameter must be provided\n- `VALUE_OPTIONAL` - Parameter is optional\n- `VALUE_DEFAULT, defaultvalue` - Optional with default\n\n### Step 3: Implement Business Logic\n\n```php\npublic static function execute($userid, $courseid, $options = []) {\n    global $DB, $USER;\n\n    // 1. Validate parameters\n    $params = self::validate_parameters(self::execute_parameters(), [\n        'userid' => $userid,\n        'courseid' => $courseid,\n        'options' => $options\n    ]);\n\n    // 2. Check permissions/capabilities\n    $context = \\context_course::instance($params['courseid']);\n    self::validate_context($context);\n    require_capability('moodle/course:view', $context);\n\n    // 3. Verify user access\n    if ($params['userid'] != $USER->id) {\n        require_capability('moodle/course:viewhiddenactivities', $context);\n    }\n\n    // 4. Database operations\n    $sql = \"SELECT id, name, timecreated\n            FROM {your_table}\n            WHERE userid = :userid\n              AND courseid = :courseid\n            LIMIT :limit\";\n    \n    $records = $DB->get_records_sql($sql, [\n        'userid' => $params['userid'],\n        'courseid' => $params['courseid'],\n        'limit' => $params['options']['limit']\n    ]);\n\n    // 5. Process and return data\n    $results = [];\n    foreach ($records as $record) {\n        $results[] = [\n            'id' => $record->id,\n            'name' => $record->name,\n            'timestamp' => $record->timecreated\n        ];\n    }\n\n    return [\n        'items' => $results,\n        'count' => count($results)\n    ];\n}\n```\n\n**Critical Steps**:\n1. **Always validate parameters** using `validate_parameters()`\n2. **Check context** using `validate_context()`\n3. **Verify capabilities** using `require_capability()`\n4. **Use parameterized queries** to prevent SQL injection\n5. **Return structured data** matching return definition\n\n### Step 4: Define Return Structure\n\n```php\npublic static function execute_returns() {\n    return new external_single_structure([\n        'items' => new external_multiple_structure(\n            new external_single_structure([\n                'id' => new external_value(PARAM_INT, 'Item ID'),\n                'name' => new external_value(PARAM_TEXT, 'Item name'),\n                'timestamp' => new external_value(PARAM_INT, 'Creation time')\n            ])\n        ),\n        'count' => new external_value(PARAM_INT, 'Total items')\n    ]);\n}\n```\n\n**Return Structure Rules**:\n- Must match exactly what `execute()` returns\n- Use appropriate parameter types\n- Document each field with description\n- Nested structures allowed\n\n### Step 5: Register the Service\n\n**Location**: `/local/yourplugin/db/services.php`\n\n```php\n<?php\ndefined('MOODLE_INTERNAL') || die();\n\n$functions = [\n    'local_yourplugin_your_api_name' => [\n        'classname'   => 'local_yourplugin\\external\\your_api_name',\n        'methodname'  => 'execute',\n        'classpath'   => 'local/yourplugin/classes/external/your_api_name.php',\n        'description' => 'Brief description of what this API does',\n        'type'        => 'read',  // or 'write'\n        'ajax'        => true,\n        'capabilities'=> 'moodle/course:view', // comma-separated if multiple\n        'services'    => [MOODLE_OFFICIAL_MOBILE_SERVICE] // Optional\n    ],\n];\n\n$services = [\n    'Your Plugin Web Service' => [\n        'functions' => [\n            'local_yourplugin_your_api_name'\n        ],\n        'restrictedusers' => 0,\n        'enabled' => 1\n    ]\n];\n```\n\n**Service Registration Keys**:\n- `classname` - Full namespaced class name\n- `methodname` - Always 'execute'\n- `type` - 'read' (SELECT) or 'write' (INSERT/UPDATE/DELETE)\n- `ajax` - Set true for AJAX/REST access\n- `capabilities` - Required Moodle capabilities\n- `services` - Optional service bundles\n\n### Step 6: Implement Error Handling & Logging\n\n```php\nprivate static function log_debug($message) {\n    global $CFG;\n    $logdir = $CFG->dataroot . '/local_yourplugin';\n    if (!file_exists($logdir)) {\n        mkdir($logdir, 0777, true);\n    }\n    $debuglog = $logdir . '/api_debug.log';\n    $timestamp = date('Y-m-d H:i:s');\n    file_put_contents($debuglog, \"[$timestamp] $message\\n\", FILE_APPEND | LOCK_EX);\n}\n\npublic static function execute($userid, $courseid) {\n    global $DB;\n\n    try {\n        self::log_debug(\"API called: userid=$userid, courseid=$courseid\");\n        \n        // Validate parameters\n        $params = self::validate_parameters(self::execute_parameters(), [\n            'userid' => $userid,\n            'courseid' => $courseid\n        ]);\n\n        // Your logic here\n        \n        self::log_debug(\"API completed successfully\");\n        return $result;\n\n    } catch (\\invalid_parameter_exception $e) {\n        self::log_debug(\"Parameter validation failed: \" . $e->getMessage());\n        throw $e;\n    } catch (\\moodle_exception $e) {\n        self::log_debug(\"Moodle exception: \" . $e->getMessage());\n        throw $e;\n    } catch (\\Exception $e) {\n        // Log detailed error info\n        $lastsql = method_exists($DB, 'get_last_sql') ? $DB->get_last_sql() : '[N/A]';\n        self::log_debug(\"Fatal error: \" . $e->getMessage());\n        self::log_debug(\"Last SQL: \" . $lastsql);\n        self::log_debug(\"Stack trace: \" . $e->getTraceAsString());\n        throw $e;\n    }\n}\n```\n\n**Error Handling Best Practices**:\n- Wrap logic in try-catch blocks\n- Log errors with timestamps and context\n- Capture SQL queries on database errors\n- Preserve stack traces for debugging\n- Re-throw exceptions after logging\n\n## Advanced Patterns\n\n### Complex Database Operations\n\n```php\n// Transaction example\n$transaction = $DB->start_delegated_transaction();\n\ntry {\n    // Insert record\n    $recordid = $DB->insert_record('your_table', $dataobject);\n    \n    // Update related records\n    $DB->set_field('another_table', 'status', 1, ['recordid' => $recordid]);\n    \n    // Commit transaction\n    $transaction->allow_commit();\n} catch (\\Exception $e) {\n    $transaction->rollback($e);\n    throw $e;\n}\n```\n\n### Working with Course Modules\n\n```php\n// Create course module\n$moduleid = $DB->get_field('modules', 'id', ['name' => 'quiz'], MUST_EXIST);\n\n$cm = new \\stdClass();\n$cm->course = $courseid;\n$cm->module = $moduleid;\n$cm->instance = 0; // Will be updated after activity creation\n$cm->visible = 1;\n$cm->groupmode = 0;\n$cmid = add_course_module($cm);\n\n// Create activity instance (e.g., quiz)\n$quiz = new \\stdClass();\n$quiz->course = $courseid;\n$quiz->name = 'My Quiz';\n$quiz->coursemodule = $cmid;\n// ... other quiz fields ...\n\n$quizid = quiz_add_instance($quiz, null);\n\n// Update course module with instance ID\n$DB->set_field('course_modules', 'instance', $quizid, ['id' => $cmid]);\ncourse_add_cm_to_section($courseid, $cmid, 0);\n```\n\n### Access Restrictions (Groups/Availability)\n\n```php\n// Restrict activity to specific user via group\n$groupname = 'activity_' . $activityid . '_user_' . $userid;\n\n// Create or get group\nif (!$groupid = $DB->get_field('groups', 'id', ['courseid' => $courseid, 'name' => $groupname])) {\n    $groupdata = (object)[\n        'courseid' => $courseid,\n        'name' => $groupname,\n        'timecreated' => time(),\n        'timemodified' => time()\n    ];\n    $groupid = $DB->insert_record('groups', $groupdata);\n}\n\n// Add user to group\nif (!$DB->record_exists('groups_members', ['groupid' => $groupid, 'userid' => $userid])) {\n    $DB->insert_record('groups_members', (object)[\n        'groupid' => $groupid,\n        'userid' => $userid,\n        'timeadded' => time()\n    ]);\n}\n\n// Set availability condition\n$restriction = [\n    'op' => '&',\n    'show' => false,\n    'c' => [\n        [\n            'type' => 'group',\n            'id' => $groupid\n        ]\n    ],\n    'showc' => [false]\n];\n\n$DB->set_field('course_modules', 'availability', json_encode($restriction), ['id' => $cmid]);\n```\n\n### Random Question Selection with Tags\n\n```php\nprivate static function get_random_questions($categoryid, $tagname, $limit) {\n    global $DB;\n    \n    $sql = \"SELECT q.id\n            FROM {question} q\n            INNER JOIN {question_versions} qv ON qv.questionid = q.id\n            INNER JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid\n            INNER JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid\n            JOIN {tag_instance} ti ON ti.itemid = q.id\n            JOIN {tag} t ON t.id = ti.tagid\n            WHERE LOWER(t.name) = :tagname\n              AND qc.id = :categoryid\n              AND ti.itemtype = 'question'\n              AND q.qtype = 'multichoice'\";\n    \n    $qids = $DB->get_fieldset_sql($sql, [\n        'categoryid' => $categoryid,\n        'tagname' => strtolower($tagname)\n    ]);\n    \n    shuffle($qids);\n    return array_slice($qids, 0, $limit);\n}\n```\n\n## Testing Your API\n\n### 1. Via Moodle Web Services Test Client\n\n1. Enable web services: **Site administration > Advanced features**\n2. Enable REST protocol: **Site administration > Plugins > Web services > Manage protocols**\n3. Create service: **Site administration > Server > Web services > External services**\n4. Test function: **Site administration > Development > Web service test client**\n\n### 2. Via curl\n\n```bash\n# Get token first\ncurl -X POST \"https://yourmoodle.com/login/token.php\" \\\n  -d \"username=admin\" \\\n  -d \"password=yourpassword\" \\\n  -d \"service=moodle_mobile_app\"\n\n# Call your API\ncurl -X POST \"https://yourmoodle.com/webservice/rest/server.php\" \\\n  -d \"wstoken=YOUR_TOKEN\" \\\n  -d \"wsfunction=local_yourplugin_your_api_name\" \\\n  -d \"moodlewsrestformat=json\" \\\n  -d \"userid=2\" \\\n  -d \"courseid=3\"\n```\n\n### 3. Via JavaScript (AJAX)\n\n```javascript\nrequire(['core/ajax'], function(ajax) {\n    var promises = ajax.call([{\n        methodname: 'local_yourplugin_your_api_name',\n        args: {\n            userid: 2,\n            courseid: 3\n        }\n    }]);\n\n    promises[0].done(function(response) {\n        console.log('Success:', response);\n    }).fail(function(error) {\n        console.error('Error:', error);\n    });\n});\n```\n\n## Common Pitfalls & Solutions\n\n### 1. \"Function not found\" Error\n**Solution**: \n- Purge caches: **Site administration > Development > Purge all caches**\n- Verify function name in services.php matches exactly\n- Check namespace and class name are correct\n\n### 2. \"Invalid parameter value detected\"\n**Solution**:\n- Ensure parameter types match between definition and usage\n- Check required vs optional parameters\n- Validate nested structure definitions\n\n### 3. SQL Injection Vulnerabilities\n**Solution**:\n- Always use placeholder parameters (`:paramname`)\n- Never concatenate user input into SQL strings\n- Use Moodle's database methods: `get_record()`, `get_records()`, etc.\n\n### 4. Permission Denied Errors\n**Solution**:\n- Call `self::validate_context($context)` early in execute()\n- Check required capabilities match user's permissions\n- Verify user has role assignments in the context\n\n### 5. Transaction Deadlocks\n**Solution**:\n- Keep transactions short\n- Always commit or rollback in finally blocks\n- Avoid nested transactions\n\n## Debugging Checklist\n\n- [ ] Check Moodle debug mode: **Site administration > Development > Debugging**\n- [ ] Review web services logs: **Site administration > Reports > Logs**\n- [ ] Check custom log files in `$CFG->dataroot/local_yourplugin/`\n- [ ] Verify database queries using `$DB->set_debug(true)`\n- [ ] Test with admin user to rule out permission issues\n- [ ] Clear browser cache and Moodle caches\n- [ ] Check PHP error logs on server\n\n## Plugin Structure Checklist\n\n```\nlocal/yourplugin/\n├── version.php                 # Plugin version and metadata\n├── db/\n│   ├── services.php           # External service definitions\n│   └── access.php             # Capability definitions (optional)\n├── classes/\n│   └── external/\n│       ├── your_api_name.php  # External API implementation\n│       └── another_api.php    # Additional APIs\n├── lang/\n│   └── en/\n│       └── local_yourplugin.php  # Language strings\n└── tests/\n    └── external_test.php      # Unit tests (optional but recommended)\n```\n\n## Examples from Real Implementation\n\n### Simple Read API (Get Quiz Attempts)\n\n```php\n<?php\nnamespace local_userlog\\external;\n\ndefined('MOODLE_INTERNAL') || die();\nrequire_once(\"$CFG->libdir/externallib.php\");\n\nuse external_api;\nuse external_function_parameters;\nuse external_single_structure;\nuse external_value;\n\nclass get_quiz_attempts extends external_api {\n    public static function execute_parameters() {\n        return new external_function_parameters([\n            'userid' => new external_value(PARAM_INT, 'User ID'),\n            'courseid' => new external_value(PARAM_INT, 'Course ID')\n        ]);\n    }\n\n    public static function execute($userid, $courseid) {\n        global $DB;\n\n        self::validate_parameters(self::execute_parameters(), [\n            'userid' => $userid,\n            'courseid' => $courseid\n        ]);\n\n        $sql = \"SELECT COUNT(*) AS quiz_attempts\n                FROM {quiz_attempts} qa\n                JOIN {quiz} q ON qa.quiz = q.id\n                WHERE qa.userid = :userid AND q.course = :courseid\";\n\n        $attempts = $DB->get_field_sql($sql, [\n            'userid' => $userid,\n            'courseid' => $courseid\n        ]);\n\n        return ['quiz_attempts' => (int)$attempts];\n    }\n\n    public static function execute_returns() {\n        return new external_single_structure([\n            'quiz_attempts' => new external_value(PARAM_INT, 'Total number of quiz attempts')\n        ]);\n    }\n}\n```\n\n### Complex Write API (Create Quiz from Categories)\n\nSee attached `create_quiz_from_categories.php` for a comprehensive example including:\n- Multiple database insertions\n- Course module creation\n- Quiz instance configuration\n- Random question selection with tags\n- Group-based access restrictions\n- Extensive error logging\n- Transaction management\n\n## Quick Reference: Common Moodle Tables\n\n| Table | Purpose |\n|-------|---------|\n| `{user}` | User accounts |\n| `{course}` | Courses |\n| `{course_modules}` | Activity instances in courses |\n| `{modules}` | Available activity types (quiz, forum, etc.) |\n| `{quiz}` | Quiz configurations |\n| `{quiz_attempts}` | Quiz attempt records |\n| `{question}` | Question bank |\n| `{question_categories}` | Question categories |\n| `{grade_items}` | Gradebook items |\n| `{grade_grades}` | Student grades |\n| `{groups}` | Course groups |\n| `{groups_members}` | Group memberships |\n| `{logstore_standard_log}` | Activity logs |\n\n## Additional Resources\n\n- [Moodle External API Documentation](https://moodledev.io/docs/5.2/apis/subsystems/external/functions)\n- [Moodle Coding Style](https://moodledev.io/general/development/policies/codingstyle)\n- [Moodle Database API](https://moodledev.io/docs/5.2/apis/core/dml)\n- [Web Services API Documentation](https://moodledev.io/docs/5.2/apis/subsystems/external)\n\n## Guidelines\n\n- Always validate input parameters using `validate_parameters()`\n- Check user context and capabilities before operations\n- Use parameterized SQL queries (never string concatenation)\n- Implement comprehensive error handling and logging\n- Follow Moodle naming conventions (lowercase, underscores)\n- Document all parameters and return values clearly\n- Test with different user roles and permissions\n- Consider transaction safety for write operations\n- Purge caches after service registration changes\n- Keep API methods focused and single-purpose\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.","tags":["moodle","external","api","development","antigravity","awesome","skills","sickn33","agent-skills","agentic-skills","ai-agent-skills","ai-agents"],"capabilities":["skill","source-sickn33","skill-moodle-external-api-development","topic-agent-skills","topic-agentic-skills","topic-ai-agent-skills","topic-ai-agents","topic-ai-coding","topic-ai-workflows","topic-antigravity","topic-antigravity-skills","topic-claude-code","topic-claude-code-skills","topic-codex-cli","topic-codex-skills"],"categories":["antigravity-awesome-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/sickn33/antigravity-awesome-skills/moodle-external-api-development","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add sickn33/antigravity-awesome-skills","source_repo":"https://github.com/sickn33/antigravity-awesome-skills","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 34666 github stars · SKILL.md body (18,171 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-04-23T06:51:35.725Z","embedding":null,"createdAt":"2026-04-18T21:41:03.794Z","updatedAt":"2026-04-23T06:51:35.725Z","lastSeenAt":"2026-04-23T06:51:35.725Z","tsv":"'/api_debug.log':719 '/docs/5.2/apis/core/dml)':1846 '/docs/5.2/apis/subsystems/external)':1853 '/docs/5.2/apis/subsystems/external/functions)':1834 '/general/development/policies/codingstyle)':1840 '/local/yourplugin/classes/external/your_api_name.php':140 '/local/yourplugin/db/services.php':592 '/local_yourplugin':708 '/login/token.php':1282 '/webservice/rest/server.php':1302 '0':656,962,974,1029,1219,1347 '0777':715 '1':108,132,363,474,658,917,971,1224,1231,1363 '10':273 '2':115,212,379,481,1239,1270,1319,1343,1391 '3':120,348,397,487,1250,1322,1323,1345,1414 '4':411,493,509,1260,1441 '5':446,501,587,1469 '6':691 'access':400,681,1030,1759 'access.php':1554 'account':1775 'activ':967,981,1035,1042,1780,1786,1824 'activityid':1043 'add':976,1003,1023,1077 'addit':1565,1826 'admin':1285,1521 'administr':1236,1244,1254,1264,1372,1493,1501 'advanc':885,1237 'ajax':628,676,1326,1331 'ajax.call':1334 'ajax/rest':680 'allow':585,923 'alphanumer':306 'alphanumext':305 'alway':475,668,1419,1476,1855 'anoth':914 'another_api.php':1564 'api':4,16,24,31,43,51,75,100,136,157,171,175,188,603,610,622,653,752,777,1223,1296,1312,1339,1562,1566,1585,1605,1623,1729,1830,1843,1849,1915 'app':91,1293 'append':737 'applic':88 'appropri':575 'architectur':96 'arg':1341 'array':325,1216 'ask':1955 'assign':1465 'attach':1735 'attempt':1588,1620,1673,1676,1690,1702,1704,1716,1726,1795,1797 'avail':1104,1122,1785 'avoid':1483 'backend':92 'bank':1162,1801 'base':209,1758 'bash':1273 'best':853 'block':861,1482 'bool':257,296 'boolean':297 'boundari':1963 'brief':617 'browser':1529 'build':74 'bundl':689 'busi':118,350 'c':1110 'cach':1370,1376,1530,1533,1909 'call':753,1294,1446 'capabl':393,407,489,492,630,682,685,1456,1555,1866 'captur':868 'catch':782,797,810,860,925 'categori':1171,1733,1803,1805 'categoryid':1140,1195,1208,1209 'cfg':153,704,706,1509,1601 'chang':1913 'char':309 'check':201,380,482,1384,1405,1454,1488,1504,1534,1862 'checklist':1487,1542 'clarif':1957 'class':137,169,184,210,665,1387,1558,1617 'classnam':605,662 'classpath':614 'clean':294 'clear':1528,1894,1930 'client':1230,1269 'cm':951,954,957,960,969,972,979,1024 'cmid':975,997,1021,1028,1127 'code':27,54,1836 'comma':634 'comma-separ':633 'commit':920,924,1477 'common':277,1360,1768 'complet':778 'complex':887,1727 'comprehens':1739,1877 'concaten':1425,1875 'condit':1105 'configur':1750,1793 'consid':1902 'console.error':1357 'console.log':1351 'contain':117 'content':731 'context':382,383,390,391,396,410,483,486,867,1449,1450,1468,1864 'convent':1885 'core':95 'core/ajax':1329 'correct':1390 'count':469,470,557,1670 'cours':72,243,384,935,939,955,977,989,1008,1016,1022,1120,1648,1745,1776,1777,1778,1783,1815 'courseid':237,358,375,376,387,426,427,439,441,745,756,757,769,770,956,990,1027,1057,1058,1063,1064,1321,1344,1642,1655,1666,1667,1689,1698,1699 'coursemodul':996 'creat':11,38,61,133,938,980,1046,1251,1730 'create_quiz_from_categories.php':1736 'creation':555,968,1747 'criteria':1966 'critic':472 'curl':1272,1277,1297 'custom':12,39,62,1505 'd':725,1283,1286,1289,1303,1307,1314,1317,1320 'data':450,504 'databas':412,872,888,1434,1512,1743,1842 'dataobject':907 'dataroot':707 'dataroot/local_yourplugin':1510 'date':721 'db':361,431,747,820,824,894,902,911,942,1013,1052,1072,1082,1091,1117,1144,1203,1515,1549,1657,1691 'deadlock':1471 'debug':701,751,776,789,803,831,838,844,878,1486,1490,1495,1517 'debuglog':717,732 'default':261,272,342,346 'defaultvalu':343 'defin':111,123,147,202,213,510,595,1595 'definit':507,1402,1413,1553,1556 'deleg':896 'deni':1443 'describ':1934 'descript':582,616,618 'detail':259,814 'detect':1395 'develop':5,32,89,1265,1373,1494 'die':150,205,598,1598 'differ':1897 'document':578,1831,1850,1888 'done':1348 'e':786,793,796,800,806,809,812,834,847,850,927,930,932 'e.g':983 'earli':1451 'en':1568 'enabl':657,1232,1240 'encod':1124 'endpoint':70 'ensur':1397 'entri':1163 'environ':1946 'environment-specif':1945 'error':693,815,833,851,863,873,1356,1358,1359,1367,1444,1536,1762,1878 'etc':1440,1790 'ex':739 'exact':570,1383 'exampl':892,1579,1740 'except':785,799,805,811,882,926 'execut':109,116,121,220,356,371,517,572,613,669,743,765,1453,1627,1653,1662,1708 'exist':711,819,950,1084 'expert':1951 'expos':83 'extend':173,186,308,1621 'extens':1761 'extern':3,13,23,30,40,50,87,99,135,146,156,159,163,167,174,187,193,197,224,229,239,249,254,265,311,315,322,521,526,530,535,543,551,559,608,1258,1551,1559,1561,1594,1604,1607,1611,1615,1622,1631,1636,1644,1712,1718,1829 'external_test.php':1573 'externallib.php':207 'fail':792,1354 'fals':262,1109,1116 'fatal':832 'featur':1238 'field':321,580,913,944,1000,1015,1054,1119,1693 'fieldset':1205 'file':138,710,729,736,1507 'final':1481 'first':1276 'flag':329 'float':300,301 'focus':1917 'follow':20,47,101,190,1882 'foreach':452 'forum':1789 'found':1366 'framework':25,52 'full':663 'function':85,160,219,225,355,516,599,649,699,742,1136,1262,1330,1349,1355,1364,1378,1608,1626,1632,1652,1707 'get':432,821,825,943,1048,1053,1137,1204,1274,1436,1438,1586,1618,1692 'getmessag':794,807,835 'gettraceasstr':848 'global':360,703,746,1143,1656 'go':180 'grade':1806,1810,1811,1813 'gradebook':1808 'group':1040,1049,1055,1075,1080,1085,1094,1112,1757,1814,1816,1817,1819 'group-bas':1756 'groupdata':1061,1076 'groupid':1051,1071,1087,1088,1097,1098,1114 'groupmod':973 'groupnam':1041,1060,1066 'groups/availability':1032 'guid':8,35 'guidelin':1854 'h':726 'handl':694,852,1879 'html':287 'id':234,244,405,416,457,459,533,540,946,1012,1020,1056,1113,1126,1641,1649 'implement':68,130,349,692,1563,1582,1876 'includ':198,258,1741 'includedetail':252 'info':816 'inject':500,1416 'inner':1151,1159,1168 'input':112,214,1427,1857,1960 'insert':899,903,1073,1092,1744 'insert/update/delete':675 'instanc':385,961,982,1004,1011,1018,1178,1749,1781 'int':232,242,268,281,538,554,562,1639,1647,1703,1721 'integ':282 'intern':149,204,597,1597 'invalid':783,1392 'issu':1527 'item':327,467,524,539,547,564,1807,1809 'javascript':1325,1327 'join':1152,1160,1169,1176,1183,1678 'json':1123,1316 'keep':1473,1914 'key':182,661 'lang':1567 'languag':1570 'last':822,826,839 'lastsql':817,841 'libdir/externallib.php':154,1602 'limit':263,270,428,429,442,445,1142,1220,1922 'lms':19,46 'local':144,191,600,606,650,1309,1336,1592 'local/yourplugin':1543 'local/yourplugin/classes/external/your_api_name.php':615 'local_yourplugin.php':1569 'locat':139,591 'lock':738 'log':695,700,750,775,788,802,813,830,837,843,862,884,1499,1503,1506,1537,1763,1823,1825,1881 'logdir':705,712,714,718 'logic':119,351,772,856 'logstor':1821 'lower':1190 'lowercas':1886 'm':724 'manag':73,1248,1765 'match':505,569,1382,1400,1457,1931 'member':1086,1095,1818 'membership':1820 'messag':702,734 'metadata':1548 'method':106,178,818,1435,1916 'methodnam':612,667,1335 'miss':1968 'mkdir':713 'mobil':90,641,1292 'mod':195 'mode':1491 'modnam':196 'modul':936,940,945,958,978,1009,1017,1121,1746,1779,1784 'moduleid':941,959 'moodl':2,18,21,29,45,48,66,84,94,98,148,203,596,639,684,798,804,1226,1291,1432,1489,1532,1596,1769,1828,1835,1841,1883 'moodle-external-api-develop':1 'moodle/course':394,408,631 'moodledev.io':1833,1839,1845,1852 'moodledev.io/docs/5.2/apis/core/dml)':1844 'moodledev.io/docs/5.2/apis/subsystems/external)':1851 'moodledev.io/docs/5.2/apis/subsystems/external/functions)':1832 'moodledev.io/general/development/policies/codingstyle)':1838 'moodlewsrestformat':1315 'multichoic':1201 'multipl':323,527,637,1742 'must':185,333,568,949 'n':735 'n/a':828 'name':172,320,417,460,462,541,548,604,611,654,666,947,992,1059,1065,1313,1340,1379,1388,1884 'namespac':143,189,664,1385,1591 'nest':583,1411,1484 'never':1424,1873 'new':223,228,238,248,253,264,520,525,529,534,542,550,558,952,986,1630,1635,1643,1711,1717 'null':1006 'number':303,1723 'object':318,1062,1096 'offici':640 'op':1107 'oper':78,413,889,1868,1907 'option':247,274,276,337,340,344,359,377,378,444,643,687,1408,1557,1576 'output':1940 'param':231,241,256,267,280,283,289,295,299,304,366,386,402,437,440,443,537,545,553,561,760,1638,1646,1720 'paramet':110,113,161,215,221,226,278,332,338,365,369,372,477,480,576,759,763,766,784,790,1393,1398,1409,1422,1609,1628,1633,1660,1663,1858,1861,1890 'parameter':495,1870 'paramnam':1423 'password':1287 'pattern':97,107,886 'permiss':1442,1460,1526,1901,1961 'permissions/capabilities':381 'php':141,142,216,352,513,593,594,696,890,937,1033,1133,1535,1589,1590 'pitfal':1361 'placehold':1421 'plain':285 'plugin':67,646,1245,1540,1545 'pluginnam':192 'point':183,302 'post':1279,1299 'practic':854 'preserv':874 'prevent':498 'privat':697,1134 'process':447 'promis':1333,1346 'protocol':1242,1249 'provid':335 'public':217,353,514,740,1624,1650,1705 'purg':1369,1374,1908 'purpos':1772,1921 'put':730 'q':1150,1680 'q.course':1688 'q.id':1147,1158,1182,1683 'q.qtype':1200 'qa':1677 'qa.quiz':1682 'qa.userid':1685 'qbe':1164 'qbe.id':1166 'qbe.questioncategoryid':1175 'qc':1172 'qc.id':1174,1194 'qid':1202,1214,1218 'queri':496,870,1513,1872 'question':1129,1139,1149,1153,1161,1170,1198,1752,1799,1800,1802,1804 'quick':1766 'quiz':77,948,984,985,988,991,994,995,999,1002,1005,1587,1619,1672,1675,1679,1701,1715,1725,1731,1748,1788,1791,1792,1794,1796 'quizid':1001,1019 'qv':1155 'qv.questionbankentryid':1167 'qv.questionid':1157 'random':1128,1138,1751 'raw':290,291 're':880 're-throw':879 'read':625,671,1584 'real':1581 'recommend':1578 'record':430,433,453,455,458,461,464,900,904,910,1074,1083,1093,1437,1439,1798 'recordid':901,918,919 'refer':1767 'regist':588 'registr':660,1912 'relat':909 'report':82,1502 'requir':151,177,206,236,246,331,392,406,491,683,1328,1406,1455,1599,1959 'resourc':1827 'respons':1350,1353 'rest':1241 'rest/ajax':69 'restrict':1031,1034,1106,1125,1760 'restrictedus':655 'result':269,451,456,468,471,781 'return':122,124,222,449,466,502,506,511,518,519,565,573,780,1215,1629,1700,1709,1710,1892 'review':1496,1952 'role':1464,1899 'rollback':929,1479 'rule':567,1524 'safeti':1904,1962 'scope':1933 'section':1026 'secur':200 'see':1734 'select':415,672,1130,1146,1669,1753 'self':367,370,388,749,761,764,774,787,801,829,836,842,1447,1658,1661 'separ':635 'server':1255,1539 'servic':15,42,64,590,638,642,644,648,659,686,688,1228,1234,1247,1252,1257,1259,1267,1290,1498,1552,1848,1911 'services.php':1381,1550 'set':677,912,1014,1103,1118,1516 'short':1475 'show':1108 'showc':1115 'shuffl':1213 'simpl':1583 'singl':164,250,313,316,522,531,1612,1713,1920 'single-purpos':1919 'site':1235,1243,1253,1263,1371,1492,1500 'skill':7,34,60,1925 'skill-moodle-external-api-development' 'slice':1217 'solut':1362,1368,1396,1418,1445,1472 'source-sickn33' 'specif':1037,1947 'sql':414,434,435,499,823,827,840,869,1145,1206,1207,1415,1429,1668,1694,1695,1871 'stack':845,875 'standard':28,55,1822 'start':895 'static':218,354,515,698,741,1135,1625,1651,1706 'status':916 'stdclass':953,987 'step':127,129,131,211,347,473,508,586,690 'step-by-step':126 'stop':1953 'strict':103 'string':1430,1571,1874 'strip':288 'strtolow':1211 'structur':114,125,165,251,310,317,324,503,512,523,528,532,566,584,1412,1541,1613,1714 'student':1812 'style':1837 'substitut':1943 'success':779,1352,1965 't.id':1187 't.name':1191 'tabl':421,906,915,1770,1771 'tag':1132,1177,1184,1755 'tagnam':1141,1192,1210,1212 'task':1929 'test':1221,1229,1261,1268,1519,1572,1575,1895,1949 'text':284,286,292,546 'three':105,176 'three-method':104 'throw':795,808,849,881,931 'ti':1179 'ti.itemid':1181 'ti.itemtype':1197 'ti.tagid':1188 'time':556,1068,1070,1102 'timead':1101 'timecr':418,465,1067 'timemodifi':1069 'timestamp':463,549,720,733,865 'token':1275,1306 'topic-agent-skills' 'topic-agentic-skills' 'topic-ai-agent-skills' 'topic-ai-agents' 'topic-ai-coding' 'topic-ai-workflows' 'topic-antigravity' 'topic-antigravity-skills' 'topic-claude-code' 'topic-claude-code-skills' 'topic-codex-cli' 'topic-codex-skills' 'total':563,1722 'trace':846,876 'track':80 'transact':891,893,897,921,922,928,1470,1474,1485,1764,1903 'treat':1938 'tri':748,859,898 'true':629,678,716,1518 'try-catch':858 'type':279,577,624,670,1111,1399,1787 'underscor':1887 'unit':1574 'updat':908,965,1007 'usag':1404 'use':58,93,155,158,162,166,478,484,490,494,574,1420,1431,1514,1603,1606,1610,1614,1859,1869,1923 'user':79,233,362,399,404,1038,1044,1078,1426,1458,1462,1522,1640,1773,1774,1863,1898 'userid':227,357,373,374,403,423,424,436,438,744,754,755,767,768,1045,1089,1090,1099,1100,1318,1342,1634,1654,1664,1665,1686,1696,1697 'userlog':1593 'usernam':1284 'valid':364,368,389,476,479,485,758,762,791,1410,1448,1659,1856,1860,1948 'valu':168,230,235,240,245,255,260,266,271,275,298,312,314,328,330,336,341,536,544,552,560,1394,1616,1637,1645,1719,1893 'var':1332 'verifi':398,488,1377,1461,1511 'version':1154,1546 'version.php':1544 'via':1039,1225,1271,1324 'view':395,632 'viewhiddenact':409 'visibl':970 'vs':1407 'vulner':1417 'web':14,41,63,647,1227,1233,1246,1256,1266,1497,1847 'work':933 'wrap':855 'write':627,674,1728,1906 'wsfunction':1308 'wstoken':1304 'x':1278,1298 'y':723 'y-m-d':722 'your_api_name.php':1560 'yourmoodle.com':1281,1301 'yourmoodle.com/login/token.php':1280 'yourmoodle.com/webservice/rest/server.php':1300 'yourpassword':1288 'yourplugin':145,601,607,651,1310,1337","prices":[{"id":"aa62941f-daa6-4f49-8c59-41ff8eb8a4bb","listingId":"229fc902-b51b-4e9c-b6d8-1d8a43e258f4","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"sickn33","category":"antigravity-awesome-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T21:41:03.794Z"}],"sources":[{"listingId":"229fc902-b51b-4e9c-b6d8-1d8a43e258f4","source":"github","sourceId":"sickn33/antigravity-awesome-skills/moodle-external-api-development","sourceUrl":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/moodle-external-api-development","isPrimary":false,"firstSeenAt":"2026-04-18T21:41:03.794Z","lastSeenAt":"2026-04-23T06:51:35.725Z"}],"details":{"listingId":"229fc902-b51b-4e9c-b6d8-1d8a43e258f4","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"sickn33","slug":"moodle-external-api-development","github":{"repo":"sickn33/antigravity-awesome-skills","stars":34666,"topics":["agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity","antigravity-skills","claude-code","claude-code-skills","codex-cli","codex-skills","cursor","cursor-skills","developer-tools","gemini-cli","gemini-skills","kiro","mcp","skill-library"],"license":"mit","html_url":"https://github.com/sickn33/antigravity-awesome-skills","pushed_at":"2026-04-23T06:41:03Z","description":"Installable GitHub library of 1,400+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections.","skill_md_sha":"fdbe4cb96f84254444792fbe7445ecbff66a5047","skill_md_path":"skills/moodle-external-api-development/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/moodle-external-api-development"},"layout":"multi","source":"github","category":"antigravity-awesome-skills","frontmatter":{"name":"moodle-external-api-development","description":"This skill guides you through creating custom external web service APIs for Moodle LMS, following Moodle's external API framework and coding standards."},"skills_sh_url":"https://skills.sh/sickn33/antigravity-awesome-skills/moodle-external-api-development"},"updatedAt":"2026-04-23T06:51:35.725Z"}}