Two Google Apps Script projects for automated email reminders via Gmail labels.
The script has three ways to preview what would happen without actually sending:
previewReminders()— Logs status of all threads: ⏸ on-hold, 🔴 due now, ⏳ remaining days. Quickest check.dryRun()— Creates Gmail drafts instead of sending. See the exact AI-generated body.CONFIG.DRY_RUN = true— Logs everything, nothing created in Gmail. Full output in Execution log.
Default: CREATE_DRAFTS: true — all reminders are created as drafts first.
Set CREATE_DRAFTS = false only when ready to go live.
// Step 1: check status
previewReminders();
// Step 2: view drafts in Gmail → Drafts
dryRun();
// Step 3: go live
// Edit CONFIG.CREATE_DRAFTS = false in code OR
// just let the trigger run normally
checkReminders(); // actually sendsPurpose: Automated follow-up of AWV notification cases for the municipality of Merelbeke-Melle.
Scans Gmail for unanswered cases, sends summary digest emails, and escalates stubborn cases to the "big chief".
Flow:
AWV email arrives
│
▼
FollowUpReminder detects (search query + FollowUp/Active label)
│
▼
Every X days: collectPending()
│ ┌── skip closed (FollowUp/Closed)
│ └── skip replied (recipient has answered)
│
▼
processFollowUps()
│
├── pending with < ESCALATE_AFTER reminders → toDigest
│ └── sendDigest(): summary email with all open cases
│
└── pending with >= ESCALATE_AFTER reminders → toEscalate
└── sendEscalation(): separate email to big chief + FollowUp/Escalated label
✅ After fix: Escalated threads stay in the digest! They won't be re-escalated, but regular reminder digests continue until the case is closed.
Labels: FollowUp/Active, FollowUp/Closed, FollowUp/Escalated, FollowUp/1 through FollowUp/N
Recipient: mobiliteit@merelbeke-melle.be
Purpose: Apply a label to any email, get an AI reminder after a specified time.
Works on any inbox, not tied to specific recipients. Uses Gemini AI to generate reminders in the correct language (NL/EN).
Flow:
Apply remind-every/2weeks to any email in Gmail
│
▼
Every 6h: checkReminders()
│
├── Step 1: autoPauseOnReply()
│ └── Recipient replied? → apply remind-every/on-hold (pause)
│
└── Step 2: send reminders
└── Filter out on-hold
└── Interval elapsed since last message from Aldo?
└── Yes → AI reminder (Gemini) in language of original email
Your control:
| What you do | What happens |
|---|---|
| Recipient replies | on-hold auto-applied ⏸ |
You remove on-hold |
Resumes on next check |
You add on-hold |
Reminders stopped |
You remove remind-every/2weeks |
Thread out of scope 🗑 |
| You reply manually | Timer resets (last message from Aldo) |
Supported labels: remind-every/1week, remind-every/2weeks, remind-every/1month, remind-every/1year, etc. (any number + day/week/month/year combination)
Uses AI (Gemini → FreeLLMAPI waterfall) for generating personalized reminders.
Both projects need the following Script Properties set in the Apps Script editor:
- Go to script.google.com → open your project
- Click Project Settings (gear icon) → Script Properties
- Add:
| Property | Description |
|---|---|
GEMINI_API_KEY |
Required. Comma-separated for multi-key fallback. |
FREE_LLM_API_KEY |
Optional fallback. FreeLLMAPI key (self-hosted). |
1. Gemini (multi-key fallback) ← primary, tries keys in order
2. FreeLLMAPI (self-hosted) ← fallback if Gemini fails
3. Fallback template ← if both AI providers fail
Language: Google Apps Script (JavaScript V8 runtime)
APIs:
- Gmail API (read, labels, send)
- Gemini / FreeLLMAPI (AI via UrlFetchApp)
- Apps Script API (triggers)
Triggers: Time-based, every 6 hours (checkReminders / checkDigests / checkEscalations)
Scripts are exported from Google Apps Script and live in this repo. Edit locally, test in the Apps Script editor, and commit.
# Structure
gmail-reminder-scripts/
├── shared/
│ └── AIProviders.gs # Shared AI waterfall (deployed to both projects)
├── FollowUpReminder/
│ ├── Code.gs # Main script
│ ├── Test.gs # Tests
│ └── appsscript.json # Manifest
│ └── AIProviders.test.gs# AI integration tests
├── LabelReminder/
│ ├── Code.gs # Main script
│ ├── Test.gs # Tests
│ └── appsscript.json # Manifest
├── scripts/
│ └── validate.js # Local validation script
├── .github/
│ └── workflows/
│ └── validate.yml # CI syntax + function check
## 🚀 Local Development & CLI
Scripts are exported from Google Apps Script and live in this repo.
Edit locally, test with clasp, and commit.
### 🔌 Run Functions from the CLI (`clasp run`)
**Quick reference:**
```bash
# From project root
cd ~/dev/06-apps-script-google
# Navigate to project
cd LabelReminder
# Run any function
clasp run dryRun # Create drafts (test mode)
clasp run previewReminders # Show status log
clasp run testIrritationCombined # Test irritation ladder
# View logs
clasp tail-logs --simplified # Show recent logs
clasp tail-logs --watch # Watch logs in real-time
# Open in browser
clasp open-script # Open Apps Script editor
clasp open-logs # Open Cloud LoggingImportant: The first time you run clasp run, you must authorize in the browser:
- Open:
clasp open-script - Run the function manually (click
▶️ Run) - Accept permissions when prompted
- Then
clasp runwill work
Logs are automatically saved to ~/dev/06-apps-script-google/logs/ when run from this repository:
# Check recent logs
cat ~/dev/06-apps-script-google/logs/LabelReminder.log
# Watch logs
tail -f ~/dev/06-apps-script-google/logs/LabelReminder.log