Quick overview
This workflow runs weekly to compare a Google Sheets roster to actual Discord member roles, writes a row-by-row drift report back to Google Sheets, and posts a concise summary to a Discord channel.
How it works
- Runs every week on a schedule (Monday at 08:00).
- Loads reconciliation settings (Discord server and channel IDs, Google Sheets URLs/tab names, gated role names, status rules, and grace period).
- Fetches the Discord server’s roles via the Discord API and reads the roster tab from Google Sheets.
- Normalizes and validates roster rows, resolving entitled role names to live Discord role IDs and flagging unusable rows or missing roles.
- Retrieves all Discord server members (excluding bots and any ignored user IDs) along with their current role IDs.
- Reconciles roster entitlements against Discord roles, bucketing mismatches (for example missing roles, unexpected gated roles, missing roster rows, grace-period joiners, and configuration/roster issues).
- Appends all drift rows to the drift report tab in Google Sheets and posts a formatted summary message to the configured Discord channel.
Setup
- Create and invite a Discord bot to your server, enable Server Members Intent, and add a Discord Bot credential in n8n.
- Add Google Sheets OAuth credentials in n8n and provide the roster and report spreadsheet URLs and tab names in the settings step.
- Ensure the roster sheet has columns like
discord_user_id, display_name, entitled_role, and optionally status, and set gated_roles to the exact Discord role names you want to enforce.
- Create a drift report tab with headers
checked_at, run_id, bucket, discord_user_id, display_name, discord_username, role, and note, and set the target Discord channel ID for the summary post.
- Optionally tune
active_status_values, inactive_status_values, grace_period_days, ignore_user_ids, and max_examples_in_summary to match your membership process.
Requirements
- A Discord server you can add a bot to. The bot needs no role permissions at all, because this never calls roleAdd or roleRemove.
- A bot in fewer than 100 servers, where SERVER MEMBERS is a self-serve toggle rather than an approval queue.
- A Google account with a roster tab and a report tab. They can live in the same spreadsheet.
- A roster that already holds Discord user IDs. The join runs on snowflakes, so names and emails will not match anything.
- n8n with Discord Bot API and Google Sheets credentials. No paid plan, no AI model, no third-party service.
Customization
- Extend the status lists rather than working around them. A value in neither list is deliberately reported as unjudgeable instead of assumed inactive.
- Swap the Discord summary node for Slack or email. It consumes one formatted string, so nothing upstream changes.
- Move to a daily schedule for a cohort that changes fast, and shorten the grace period to match.
- Point the report tab at a separate spreadsheet if the roster owner should not see the drift history.
- Carry extra roster columns into the report by extending the row builder in the reconcile step.
Additional info
This is deliberately read only. There is no fix-it branch and the workflow never adds or removes a role, so it is safe to point at a live server with thousands of members and decide for yourself what to do about what it finds. That is also what separates it from role bots like Carl-bot or Dyno, which manage roles from inside Discord but have no idea what your spreadsheet says. The one thing to watch is the discord_user_id column, because nobody hand-maintains a column of 18 digit snowflakes for long: capture the ID at signup by adding the field to your intake form rather than backfilling it later. Rows with a blank or malformed ID are not dropped quietly, they land in the report as unusable and are counted on their own line, which is a signal that the roster needs attention rather than a signal that the workflow failed.