{"id":5649,"date":"2026-06-04T11:41:31","date_gmt":"2026-06-04T11:41:31","guid":{"rendered":"https:\/\/www.cmsgalaxy.com\/blog\/?p=5649"},"modified":"2026-06-04T11:41:32","modified_gmt":"2026-06-04T11:41:32","slug":"automating-moodle-cohort-access-expiry-add-user-by-email-and-remove-after-1-year","status":"publish","type":"post","link":"https:\/\/www.cmsgalaxy.com\/blog\/automating-moodle-cohort-access-expiry-add-user-by-email-and-remove-after-1-year\/","title":{"rendered":"Automating Moodle Cohort Access Expiry: Add User by Email and Remove After 1 Year"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">Automating Moodle Cohort Access Expiry: Add User by Email and Remove After 1 Year<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Managing Moodle access manually works for a few users, but it becomes risky when memberships are time-based. For example, if a user is added to a Moodle cohort called <strong>OneMembership<\/strong>, and that membership should expire after <strong>1 year<\/strong>, you should not depend on manual reminders.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A better approach is to automate it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this tutorial, we will create a complete Moodle cohort automation workflow:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Add a user to a Moodle cohort using email ID.<\/li>\n\n\n\n<li>Track the cohort membership date using Moodle\u2019s <code>cohort_members.timeadded<\/code>.<\/li>\n\n\n\n<li>Find users whose membership is older than 1 year.<\/li>\n\n\n\n<li>Remove expired users from the cohort using Moodle Web Service API.<\/li>\n\n\n\n<li>Run the script automatically using cron.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Moodle provides web service functions for cohort operations such as adding cohort members, getting cohort members, and deleting cohort members. These include <code>core_cohort_add_cohort_members<\/code>, <code>core_cohort_get_cohort_members<\/code>, and <code>core_cohort_delete_cohort_members<\/code>. (<a href=\"https:\/\/docs.moodle.org\/dev\/Web_service_API_functions?utm_source=chatgpt.com\">Moodle Docs<\/a>) Moodle REST API calls normally use <code>\/webservice\/rest\/server.php<\/code> with parameters like <code>wstoken<\/code>, <code>wsfunction<\/code>, and <code>moodlewsrestformat=json<\/code>. (<a href=\"https:\/\/docs.moodle.org\/dev\/Creating_a_web_service_client?utm_source=chatgpt.com\">Moodle Docs<\/a>)<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Use Case<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Assume we have a Moodle cohort called:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>OneMembership\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">From the Moodle URL:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:&#47;&#47;www.devopsschool.xyz\/cohort\/assign.php?id=122&amp;returnurl=...\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The cohort ID is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>122\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">So our target cohort is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Cohort Name: OneMembership\nCohort ID: 122\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The goal is:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">When a user has completed 1 year from the date they were added to the OneMembership cohort, automatically remove that user from the cohort.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 1: Add User to Moodle Cohort by Email<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Moodle cohort API does not directly add a user by email. It needs the <strong>Moodle user ID<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So the process is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Email ID \u2192 Find Moodle User ID \u2192 Add User ID to Cohort\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Get Moodle User ID from Email<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use this API call:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST \"https:\/\/www.devopsschool.xyz\/webservice\/rest\/server.php\" \\\n  -d \"wstoken=YOUR_MOODLE_TOKEN\" \\\n  -d \"wsfunction=core_user_get_users_by_field\" \\\n  -d \"moodlewsrestformat=json\" \\\n  -d \"field=email\" \\\n  -d \"values&#91;0]=USER_EMAIL_HERE\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST \"https:\/\/www.devopsschool.xyz\/webservice\/rest\/server.php\" \\\n  -d \"wstoken=YOUR_MOODLE_TOKEN\" \\\n  -d \"wsfunction=core_user_get_users_by_field\" \\\n  -d \"moodlewsrestformat=json\" \\\n  -d \"field=email\" \\\n  -d \"values&#91;0]=student@example.com\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;\n  {\n    \"id\": 12345,\n    \"username\": \"student\",\n    \"email\": \"student@example.com\",\n    \"firstname\": \"Student\",\n    \"lastname\": \"Example\"\n  }\n]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Here the Moodle user ID is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>12345\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Add User to OneMembership Cohort<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>core_cohort_add_cohort_members<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST \"https:\/\/www.devopsschool.xyz\/webservice\/rest\/server.php\" \\\n  -d \"wstoken=YOUR_MOODLE_TOKEN\" \\\n  -d \"wsfunction=core_cohort_add_cohort_members\" \\\n  -d \"moodlewsrestformat=json\" \\\n  -d \"members&#91;0]&#91;cohorttype]&#91;type]=id\" \\\n  -d \"members&#91;0]&#91;cohorttype]&#91;value]=122\" \\\n  -d \"members&#91;0]&#91;usertype]&#91;type]=id\" \\\n  -d \"members&#91;0]&#91;usertype]&#91;value]=12345\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Replace:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>YOUR_MOODLE_TOKEN\n12345\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">with your actual Moodle token and user ID.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 2: Why We Need Expiry Automation<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">When users are added manually to a cohort, it is easy to forget who should be removed later.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For membership-based learning platforms, this creates problems:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>User paid for 1 year\nUser added to cohort\n1 year completed\nBut user still has access\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This can cause:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Unauthorized course access<\/li>\n\n\n\n<li>Revenue leakage<\/li>\n\n\n\n<li>Manual admin burden<\/li>\n\n\n\n<li>Confusion in course enrolments<\/li>\n\n\n\n<li>Incorrect active user reports<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">So instead of manually checking users, we can automate the removal.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 3: How Moodle Stores Cohort Membership Date<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Moodle stores cohort membership records in the cohort members table. The table links users to cohorts; Moodle schema references describe <code>cohort_members<\/code> as the table that links a user to a cohort. (<a href=\"https:\/\/examulator.com\/er\/4.3\/index.html?utm_source=chatgpt.com\">examulator.com<\/a>)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Usually the table is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mdl_cohort_members\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Important fields:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cohortid\nuserid\ntimeadded\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>timeadded<\/code> field stores when the user was added to the cohort.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before writing automation, verify your table structure:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>DESCRIBE mdl_cohort_members;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see fields similar to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>id\ncohortid\nuserid\ntimeadded\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If your Moodle table prefix is different, replace <code>mdl_<\/code> with your actual prefix.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 4: SQL to Find Expired OneMembership Users<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This query finds users who were added to cohort ID <code>122<\/code> more than 1 year ago.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT \n    cm.cohortid,\n    c.name AS cohort_name,\n    u.id AS userid,\n    u.email,\n    FROM_UNIXTIME(cm.timeadded) AS added_on\nFROM mdl_cohort_members cm\nJOIN mdl_cohort c ON c.id = cm.cohortid\nJOIN mdl_user u ON u.id = cm.userid\nWHERE cm.cohortid = 122\n  AND cm.timeadded &lt;= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 1 YEAR));\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Example output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cohortid | cohort_name   | userid | email               | added_on\n122      | OneMembership | 12345  | student@example.com | 2025-06-01 10:30:22\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This means the user has completed 1 year in the cohort and should be removed.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 5: Remove One User from Moodle Cohort Using API<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Use <code>core_cohort_delete_cohort_members<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -X POST \"https:\/\/www.devopsschool.xyz\/webservice\/rest\/server.php\" \\\n  -d \"wstoken=YOUR_MOODLE_TOKEN\" \\\n  -d \"wsfunction=core_cohort_delete_cohort_members\" \\\n  -d \"moodlewsrestformat=json\" \\\n  -d \"members&#91;0]&#91;cohortid]=122\" \\\n  -d \"members&#91;0]&#91;userid]=12345\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If successful, Moodle may return an empty response or no error.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 6: Full Automation Script<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Now let us create a script that:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Connects to the Moodle database.<\/li>\n\n\n\n<li>Finds users in OneMembership older than 1 year.<\/li>\n\n\n\n<li>Calls Moodle API to remove each expired user.<\/li>\n\n\n\n<li>Logs every action.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Create a script file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p \/home\/scripts\nnano \/home\/scripts\/remove_expired_onemembership.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Paste this script:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\n\n# Moodle details\nMOODLE_URL=\"https:\/\/www.devopsschool.xyz\"\nTOKEN=\"YOUR_MOODLE_TOKEN\"\nCOHORT_ID=\"122\"\n\n# Moodle database details\nDB_HOST=\"localhost\"\nDB_NAME=\"YOUR_MOODLE_DB_NAME\"\nDB_USER=\"YOUR_DB_USER\"\nDB_PASS=\"YOUR_DB_PASSWORD\"\nDB_PREFIX=\"mdl_\"\n\n# Log file\nLOG_FILE=\"\/home\/scripts\/remove_expired_onemembership.log\"\n\necho \"===== Run started: $(date) =====\" &gt;&gt; \"$LOG_FILE\"\n\nmysql -h \"$DB_HOST\" -u \"$DB_USER\" -p\"$DB_PASS\" \"$DB_NAME\" -N -B -e \"\nSELECT \n    u.id,\n    u.email,\n    FROM_UNIXTIME(cm.timeadded)\nFROM ${DB_PREFIX}cohort_members cm\nJOIN ${DB_PREFIX}user u ON u.id = cm.userid\nWHERE cm.cohortid = ${COHORT_ID}\n  AND cm.timeadded &lt;= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 1 YEAR));\n\" | while read USER_ID EMAIL ADDED_DATE ADDED_TIME\ndo\n    echo \"Expired user found: $EMAIL | UserID: $USER_ID | Added: $ADDED_DATE $ADDED_TIME\" &gt;&gt; \"$LOG_FILE\"\n\n    RESPONSE=$(curl -s -X POST \"$MOODLE_URL\/webservice\/rest\/server.php\" \\\n      -d \"wstoken=$TOKEN\" \\\n      -d \"wsfunction=core_cohort_delete_cohort_members\" \\\n      -d \"moodlewsrestformat=json\" \\\n      -d \"members&#91;0]&#91;cohortid]=$COHORT_ID\" \\\n      -d \"members&#91;0]&#91;userid]=$USER_ID\")\n\n    echo \"Remove API response for $EMAIL: $RESPONSE\" &gt;&gt; \"$LOG_FILE\"\ndone\n\necho \"===== Run completed: $(date) =====\" &gt;&gt; \"$LOG_FILE\"\necho \"\" &gt;&gt; \"$LOG_FILE\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Save and exit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now make it executable:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>chmod +x \/home\/scripts\/remove_expired_onemembership.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run it manually:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/home\/scripts\/remove_expired_onemembership.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Check the log:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cat \/home\/scripts\/remove_expired_onemembership.log\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 7: Safer Version with Dry Run Mode<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Before removing real users, it is better to test with a dry run.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create another safer version:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>nano \/home\/scripts\/remove_expired_onemembership_dryrun.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Paste this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\n\nMOODLE_URL=\"https:\/\/www.devopsschool.xyz\"\nTOKEN=\"YOUR_MOODLE_TOKEN\"\nCOHORT_ID=\"122\"\n\nDB_HOST=\"localhost\"\nDB_NAME=\"YOUR_MOODLE_DB_NAME\"\nDB_USER=\"YOUR_DB_USER\"\nDB_PASS=\"YOUR_DB_PASSWORD\"\nDB_PREFIX=\"mdl_\"\n\nDRY_RUN=\"true\"\n\nLOG_FILE=\"\/home\/scripts\/remove_expired_onemembership_dryrun.log\"\n\necho \"===== Dry run started: $(date) =====\" &gt;&gt; \"$LOG_FILE\"\n\nmysql -h \"$DB_HOST\" -u \"$DB_USER\" -p\"$DB_PASS\" \"$DB_NAME\" -N -B -e \"\nSELECT \n    u.id,\n    u.email,\n    FROM_UNIXTIME(cm.timeadded)\nFROM ${DB_PREFIX}cohort_members cm\nJOIN ${DB_PREFIX}user u ON u.id = cm.userid\nWHERE cm.cohortid = ${COHORT_ID}\n  AND cm.timeadded &lt;= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 1 YEAR));\n\" | while read USER_ID EMAIL ADDED_DATE ADDED_TIME\ndo\n    echo \"Would remove: $EMAIL | UserID: $USER_ID | Added: $ADDED_DATE $ADDED_TIME\" &gt;&gt; \"$LOG_FILE\"\n\n    if &#91; \"$DRY_RUN\" = \"false\" ]; then\n        RESPONSE=$(curl -s -X POST \"$MOODLE_URL\/webservice\/rest\/server.php\" \\\n          -d \"wstoken=$TOKEN\" \\\n          -d \"wsfunction=core_cohort_delete_cohort_members\" \\\n          -d \"moodlewsrestformat=json\" \\\n          -d \"members&#91;0]&#91;cohortid]=$COHORT_ID\" \\\n          -d \"members&#91;0]&#91;userid]=$USER_ID\")\n\n        echo \"Remove API response for $EMAIL: $RESPONSE\" &gt;&gt; \"$LOG_FILE\"\n    fi\ndone\n\necho \"===== Dry run completed: $(date) =====\" &gt;&gt; \"$LOG_FILE\"\necho \"\" &gt;&gt; \"$LOG_FILE\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Make it executable:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>chmod +x \/home\/scripts\/remove_expired_onemembership_dryrun.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/home\/scripts\/remove_expired_onemembership_dryrun.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Check dry-run results:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cat \/home\/scripts\/remove_expired_onemembership_dryrun.log\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Once everything looks correct, change:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>DRY_RUN=\"true\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>DRY_RUN=\"false\"\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 8: Automate with Cron<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Open crontab:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>crontab -e\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">To run daily at 1 AM:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>0 1 * * * \/home\/scripts\/remove_expired_onemembership.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">To run every hour:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>0 * * * * \/home\/scripts\/remove_expired_onemembership.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Daily is usually enough for membership expiry. Hourly is useful if you want expiry to happen closer to the exact completion time.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 9: Required Moodle Web Service Setup<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">In Moodle admin, make sure Web Services and REST protocol are enabled. Moodle\u2019s official web service guide describes creating an external service, adding functions to it, generating a token, and calling the service using POST requests. (<a href=\"https:\/\/supportus.moodle.com\/support\/solutions\/articles\/80001016973-using-the-web-services-application-programming-interface-api-in-moodle?utm_source=chatgpt.com\">Moodle US<\/a>)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your Moodle service token should have access to these functions:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>core_user_get_users_by_field\ncore_cohort_add_cohort_members\ncore_cohort_get_cohort_members\ncore_cohort_delete_cohort_members\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For this automation, the most important one is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>core_cohort_delete_cohort_members\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Without this function, the removal API call will fail.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 10: Recommended Folder Structure<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">A clean structure:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/home\/scripts\/\n\u251c\u2500\u2500 remove_expired_onemembership.sh\n\u251c\u2500\u2500 remove_expired_onemembership_dryrun.sh\n\u2514\u2500\u2500 remove_expired_onemembership.log\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Recommended permissions:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>chmod 700 \/home\/scripts\/remove_expired_onemembership.sh\nchmod 700 \/home\/scripts\/remove_expired_onemembership_dryrun.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Do not expose this script publicly because it contains database credentials and Moodle API token.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 11: Troubleshooting<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">1. API returns invalid token<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Check:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>wstoken\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Make sure the token is correct and belongs to a user who has permission to manage cohorts.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. API function not allowed<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you see an error saying the function is not available, add this function to your Moodle external service:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>core_cohort_delete_cohort_members\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. No users are removed<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run the SQL manually:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT \n    u.id,\n    u.email,\n    FROM_UNIXTIME(cm.timeadded) AS added_on\nFROM mdl_cohort_members cm\nJOIN mdl_user u ON u.id = cm.userid\nWHERE cm.cohortid = 122;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This will show all users in the cohort and their added date.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then check expired users:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT \n    u.id,\n    u.email,\n    FROM_UNIXTIME(cm.timeadded) AS added_on\nFROM mdl_cohort_members cm\nJOIN mdl_user u ON u.id = cm.userid\nWHERE cm.cohortid = 122\n  AND cm.timeadded &lt;= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 1 YEAR));\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If no rows are returned, no user has completed 1 year yet.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4. Wrong table prefix<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Some Moodle installations do not use <code>mdl_<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Check your Moodle <code>config.php<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>grep prefix \/path\/to\/moodle\/config.php\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$CFG-&gt;prefix = 'mdl_';\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Use that prefix in your script.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">5. Database login issue<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Test database login manually:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mysql -h localhost -u YOUR_DB_USER -p YOUR_MOODLE_DB_NAME\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If login fails, fix DB username, password, host, or database name.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Part 12: Best Practices<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Do not directly delete users from <code>mdl_cohort_members<\/code> using SQL. Use SQL only for reading and reporting. Use Moodle\u2019s official web service API to remove users from cohorts because API-based changes are safer and more compatible with Moodle\u2019s internal processes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Also:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Test first on staging.<\/li>\n\n\n\n<li>Run dry-run mode before enabling real removal.<\/li>\n\n\n\n<li>Keep logs.<\/li>\n\n\n\n<li>Use a dedicated Moodle API user.<\/li>\n\n\n\n<li>Give that API user only the minimum required permissions.<\/li>\n\n\n\n<li>Protect the script because it contains secrets.<\/li>\n\n\n\n<li>Backup the database before first production run.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Final Workflow Summary<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The full automation looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>User buys 1-year membership\n        \u2193\nAdmin\/API adds user email to Moodle\n        \u2193\nEmail is converted to Moodle user ID\n        \u2193\nUser is added to OneMembership cohort ID 122\n        \u2193\nMoodle stores cohort membership date\n        \u2193\nDaily cron checks users older than 1 year\n        \u2193\nExpired users are removed using Moodle API\n        \u2193\nUser loses cohort-based course access\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Conclusion<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Moodle cohorts are very useful for giving course access to a group of users. But when access is subscription-based, cohort membership should not remain forever.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">By combining:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Moodle Web Service API\nMoodle cohort_members table\nMySQL query\nBash script\nCron job\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">you can build a clean automation that removes users from the <strong>OneMembership<\/strong> cohort exactly after their 1-year access period is completed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This approach reduces manual work, prevents forgotten access, keeps Moodle clean, and makes your membership system more reliable.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Automating Moodle Cohort Access Expiry: Add User by Email and Remove After 1 Year Managing Moodle access manually works for a few users, but it becomes risky&#8230; <\/p>\n","protected":false},"author":13,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-5649","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.cmsgalaxy.com\/blog\/wp-json\/wp\/v2\/posts\/5649","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cmsgalaxy.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cmsgalaxy.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cmsgalaxy.com\/blog\/wp-json\/wp\/v2\/users\/13"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cmsgalaxy.com\/blog\/wp-json\/wp\/v2\/comments?post=5649"}],"version-history":[{"count":1,"href":"https:\/\/www.cmsgalaxy.com\/blog\/wp-json\/wp\/v2\/posts\/5649\/revisions"}],"predecessor-version":[{"id":5650,"href":"https:\/\/www.cmsgalaxy.com\/blog\/wp-json\/wp\/v2\/posts\/5649\/revisions\/5650"}],"wp:attachment":[{"href":"https:\/\/www.cmsgalaxy.com\/blog\/wp-json\/wp\/v2\/media?parent=5649"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cmsgalaxy.com\/blog\/wp-json\/wp\/v2\/categories?post=5649"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cmsgalaxy.com\/blog\/wp-json\/wp\/v2\/tags?post=5649"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}