Google Apps Script system for automating volunteer shift scheduling at Caféen?. Manages shift templates, personal volunteer pages, automatic schedule generation, Google Calendar sync, and email reminders — all from a sidebar panel inside Google Sheets.
- Overview
- File structure
- Setup
- Local development with clasp
- Configuration sheet
- Using the admin sidebar
- How the scheduler works
- Personal pages
- Calendar sync
- Email reminders
- KU schedule preview
- Troubleshooting
The spreadsheet has three permanent sheets:
| Sheet | Purpose |
|---|---|
| Configuration | All settings: year, week range, shift hours, admin emails |
| Endeligt Skema | The live schedule filled in by admins; volunteer list lives here too |
| Template | Blank per-volunteer preference sheet; copied when personal pages are created |
When the season starts, an admin:
- Fills in the Configuration sheet
- Generates a fresh schedule template
- Creates personal pages (one per volunteer)
- Volunteers fill in their availability on their own page
- Admin runs the auto-scheduler or fills the schedule manually
- Admin syncs shifts to Google Calendar and sends reminder emails
AutoSkema/
├── main.gs Entry points for menu actions and spreadsheet events
├── schedule.gs Template generation, hours calculation, CSP auto-scheduler
├── calendar.gs Google Calendar sync and KU schedule preview
├── email.gs Reminder email sending
├── ics.gs iCalendar (.ics) parser for university class schedules
├── utils.gs Constants, sheet getters, shared utilities
├── sidebar.gs Admin sidebar server functions and access control
└── sidebar.html Admin sidebar UI
All magic numbers and cell references live here. If the spreadsheet layout changes, this is the only file that needs updating.
VOLUNTEER_LIST_START_ROW Row where the volunteer list begins (default 5)
VOLUNTEER_LIST_COL Column N (14) — where volunteer names are stored
SCHEDULE_START_ROW Row 4 — top of the schedule grid
SCHEDULE_END_ROW Row 71 — bottom of the schedule grid
MAX_VOLUNTEER_HOURS Hard cap on hours any one volunteer can be assigned
AVERAGE_HOURS_CELL B21 — target hours per volunteer for the auto-scheduler
ADMIN_EMAILS_CELL B3 — comma-separated list of admin email addresses
The script is bound to the Google Spreadsheet. Open the sheet, go to Extensions → Apps Script to view the code.
In Apps Script: Services → Add a service and enable:
- Admin SDK Directory API — used to verify volunteer email addresses when protecting personal pages
In calendar.gs, update the constant with your shared Google Calendar ID:
const CALENDAR_ID = 'your-calendar-id@group.calendar.google.com';Find the Calendar ID in Google Calendar under Settings → [calendar name] → Integrate calendar.
In the Configuration sheet, put the label Admin e-mails in cell A3 and a comma-separated list of admin email addresses in B3:
freja@cafeen.org,kr0ll@cafeen.org,charlotte@cafeen.org
Only these users can open the admin sidebar or call any sidebar server function.
From the Apps Script editor, run the onOpen function manually once to register the custom menu. After that it runs automatically every time the spreadsheet is opened.
clasp (Command Line Apps Script Projects) is Google's official CLI for Apps Script. It lets you edit .gs files locally in any editor, version-control them with Git, and push changes to the live script without touching the browser-based editor.
npm install -g @google/claspRequires Node.js 12+.
clasp loginThis opens a browser window to authorise clasp with your Google account. Credentials are stored in ~/.clasprc.json.
The Apps Script project is bound to the spreadsheet. To get the Script ID:
- Open the spreadsheet
- Go to Extensions → Apps Script
- Click Project Settings (gear icon)
- Copy the Script ID
Create a .clasp.json file in the root of this repository:
{
"scriptId": "YOUR_SCRIPT_ID_HERE",
"rootDir": "."
}
.clasp.jsoncontains your personal script ID. It is already listed in.gitignore— do not commit it.
| Task | Command |
|---|---|
| Pull latest code from Apps Script to local | clasp pull |
| Push local changes to Apps Script | clasp push |
| Open the script editor in the browser | clasp open |
| Watch for local changes and auto-push | clasp push --watch |
Typical session:
# Make sure you have the latest before editing
clasp pull
# ... edit .gs / .html files locally ...
# Push when ready
clasp pushclasp pushes every file in rootDir that matches a supported extension. For this project that means all .gs files and sidebar.html. The README.md and .clasp.json are ignored by Apps Script.
Apps Script stores project metadata (timezone, OAuth scopes, enabled APIs) in appsscript.json. After running clasp pull for the first time you will see this file appear. Commit it alongside the source files — it defines which Google APIs the script is allowed to call and should be kept in sync with the live project.
A minimal example for this project:
{
"timeZone": "Europe/Copenhagen",
"dependencies": {
"enabledAdvancedServices": [
{
"userSymbol": "AdminDirectory",
"serviceId": "admin",
"version": "directory_v1"
}
]
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}| Cell | Contents |
|---|---|
| A3 / B3 | Label / Admin email addresses (comma-separated) — controls sidebar access and personal page editor permissions |
| B4 | Year (e.g. 2025) |
| B5 | Open–close week range (e.g. 36-2) |
| B6 | Cleaning week(s) — single week or two comma-separated weeks |
| A7 / B7 | Label / Shared Google Calendar ID for shift sync |
| A9:D16 | Shift hours by weekday — columns: Weekday, Åbne hours, Mellem hours, Lukke hours |
| B18 | Total hours in the schedule (auto-calculated) |
| B21 | Target hours per volunteer — used by the auto-scheduler to distribute shifts fairly |
Each row defines how many hours each shift type is worth on a given weekday:
| A (Weekday) | B (Åbne) | C (Mellem) | D (Lukke) |
|---|---|---|---|
| MON | 4 | — | 4 |
| TUE | 4 | — | 4 |
| WED | 4 | — | 3 |
| THU | 4 | — | 5 |
| FRI | 8 | 4 | 7 |
| SUN | 4 | — | — |
| CLEANING WEEK | 4 | — | 4 |
Open it from Caféen? → Åbn Admin Panel in the menu bar.
![Sidebar sections: Skabelon, Frivillige, Skema, Kalender, E-mails, Eksport]
Access is restricted to the emails listed in B3. Any other user sees an "Adgang nægtet" alert and the sidebar never opens. Each server function also re-checks access independently, so the restriction holds even if someone calls google.script.run directly from the browser console.
Skabelon
- Generer skabelon — Clears the schedule and template sheets and rebuilds them from the Configuration sheet (week range, cleaning weeks, year). Shows an inline confirmation before running.
Frivillige
- Opret personlige sider — Creates one sheet per volunteer (named after the volunteer) by copying the Template sheet. Protects each sheet so only that volunteer and the admins can edit it. Requires confirmation.
- Slet personlige sider — Permanently deletes all personal pages. Requires confirmation.
Skema
- Opdater dropdowns — Rebuilds the dropdown menus in the schedule grid so each cell only shows volunteers who are available for that specific shift on that day.
- Opdater timer — Recalculates the total hours and days worked for every volunteer and writes the results back to the schedule sheet.
- Auto-udfyld skema — Runs the CSP auto-scheduler (see How the scheduler works). Can take up to a few seconds; results appear in a popup with a per-volunteer hour summary.
Kalender
- Synkroniser kalender — Creates Google Calendar events for every assigned shift and sends email invitations to the volunteers. Only deletes events previously created by AutoSkema (tagged
[AutoSkema]in the event description).
E-mails
- Send påmindelser — Sends a personalised HTML reminder email to every volunteer whose schedule status is
Nej.
Eksport
- Eksportér til Wiki — Saves the schedule as a Wiki-formatted
.txtfile in Google Drive.
- Loading spinner — shown while a server function is running; all buttons are disabled
- Green status bar — shown on success (auto-dismisses after 6 seconds)
- Red status bar — shown on error with the error message
- Inline confirmation panel — shown for destructive actions before the server call is made
fillSchedule() in schedule.gs uses a Constraint Satisfaction Problem (CSP) solver with backtracking search and two heuristics.
| Constraint | Rule |
|---|---|
| Availability | Volunteer must have marked the day as anything other than Kan Ikke |
| Shift match | Volunteer's preference must cover the shift type (Åbne / Mellem / Lukke), including compound preferences like Åbne & Lukke |
| Max hours | A volunteer's total assigned hours cannot exceed MAX_VOLUNTEER_HOURS |
| One shift per week | A volunteer can only work one shift per week |
| No consecutive days | A volunteer cannot work on two rows that are adjacent in the schedule (prevents back-to-back days across week boundaries) |
| Shift capacity | Each shift slot has a required number of volunteers (1 for most, 2 for Friday Mellem) |
MCV — Most Constrained Variable Shifts are sorted before search begins: shifts with the fewest possible volunteers are assigned first. This catches impossible or near-impossible slots early rather than discovering them deep in the search tree.
Fairness + LCV — Volunteer ordering For each shift, volunteers are ordered by two keys:
- Fairness (primary) — volunteers furthest below their target hours (
B21) go first. This directly prevents some volunteers ending up with 0 shifts: under-scheduled volunteers are always tried before those who already have enough hours. - LCV tiebreak — among volunteers with similar hours, prefer the one whose assignment removes the fewest options from remaining shifts (Least Constraining Value). Uses a pre-computed
applicableShiftslist per volunteer to keep this fast.
If a complete schedule is found, it is applied directly. If the time limit is reached (5 minutes), the best partial schedule found so far is applied. The result popup shows:
- Time taken
- Any volunteers who still have 0 hours after scheduling
Each volunteer has their own sheet (named after them) copied from the Template sheet. The sheet is protected so only that volunteer and the admins listed in B3 can edit it.
Column E (rows 4–71) holds the volunteer's availability preferences for each row in the schedule:
| Value | Meaning |
|---|---|
Kan |
Available for any shift |
Åbne |
Opening shift only |
Lukke |
Closing shift only |
Mellem |
Middle shift only (Fridays) |
Åbne & Lukke |
Either opening or closing (Fridays) |
Åbne & Mellem |
Either opening or middle (Fridays) |
Mellem & Lukke |
Either middle or closing (Fridays) |
Kan Ikke |
Not available |
Fridays have an extended preference list because they have three shift types (Åbne, Mellem, Lukke).
When an admin selects a volunteer name in cell O1 of the schedule sheet, the cells that volunteer is available for are highlighted green. Clearing O1 removes the highlight.
syncShiftsToSharedCalendar() in calendar.gs:
- Reads all assigned volunteer names from the schedule grid
- Looks up each volunteer's email address from the volunteer list
- Creates a Google Calendar event for each shift with the correct start/end time
- Invites the volunteer as a guest (sends an email invitation)
- Tags each created event with
[AutoSkema]in the description
On re-sync, only events tagged [AutoSkema] are deleted first — manually added events on the same calendar are left untouched.
| Day | Åbner | Lukker |
|---|---|---|
| Mon / Tue | 12:00–16:00 | 16:00–20:00 |
| Wed | 12:00–16:00 | 17:00–20:00 |
| Thu | 12:00–16:00 | 17:00–22:00 |
| Fri | 12:00–20:00 | 20:00–03:00 (Saturday) |
| Sun | 12:00–16:00 | 12:00–16:00 |
sendReminderEmails() in email.gs sends a personalised HTML email to every volunteer whose status column (index 6 in the volunteer list) is Nej.
The email contains a direct link to the volunteer's personal sheet and is sent from the account running the script, displayed as Caféen?.
KU Skema → Vis MitSkema opens a dialog where a volunteer can enter their KU student ID. Their university timetable is then displayed in an embedded calendar view (fetched via personligtskema.ku.dk).
ics.gs also provides fetchClassSchedule(userId) and isDateInBusyTimes(date, busyTimes) which can be used to check whether a volunteer has a class clash on a given date — useful for future integration with the availability system.
"Required sheet X not found"
A sheet has been renamed or deleted. Check that the three sheets are named exactly Configuration, Endeligt Skema, and Template.
"Adgang nægtet" when opening the sidebar Your email is not in the admin list in cell B3 of the Configuration sheet.
"Ingen admin-e-mails konfigureret" Cell B3 in the Configuration sheet is empty. Add at least one email address.
Calendar sync creates duplicate events
Events are de-duplicated by checking existing events in the same time window with the same title. If you see duplicates, check that the calendar ID in calendar.gs is correct and that previous events were tagged with [AutoSkema].
Auto-scheduler gives some volunteers 0 hours
- Check that those volunteers have filled in their personal page —
Kan Ikkeon every row means the scheduler has nowhere to place them. - Verify that
B21in the Configuration sheet contains a reasonable target (e.g.10). If the cell is empty the scheduler falls back toMAX_VOLUNTEER_HOURSas the target, which may be too high to drive the fairness ordering effectively.
Dates in the Wiki export are off by one
The export uses the spreadsheet's own timezone (getSpreadsheetTimeZone()). If dates still look wrong, verify the spreadsheet timezone in File → Settings → Time zone matches Denmark (Europe/Copenhagen).