agentsclimarketplace

Google calendar

Skill AceDataCloud/Skills/skills/google-calendar

Read and manage Google Calendar events / agenda / free-busy / invitations via the Calendar v3 REST API. Use when the user mentions Google Calendar events, today's agenda, this week's meetings, finding conflicts, listing invitations, checking free time, or scheduling / rescheduling / cancelling a meeting.From its SKILL.md

Install
npx -y skills add AceDataCloud/Skills --skill google-calendar

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its file declares

Copied from the file, not written here

The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

15.5 KB, ~4.2k tokens by cl100k_base, as published. Nobody here has run it

Drive Google Calendar via curl + jq. The user's OAuth bearer token is in $GOOGLE_CALENDAR_TOKEN; every call needs it as Authorization: Bearer $GOOGLE_CALENDAR_TOKEN. At minimum the token carries calendar.readonly plus the identity scopes (openid email profile); if the user opted in to write at install time it also carries the broader calendar scope (read + write).

The Calendar API returns standard JSON; failures surface as {"error": {"code": 401|403|..., "message": "..."}} — show that error verbatim. 401 means the token expired (re-install). 403 insufficientPermissions on a write means the user only granted calendar.readonly — ask them to re-install the connector with the read+write box checked.

Always start with users/me/calendarList to learn which calendars the account can see (the user's primary plus any subscribed / shared ones), AND with users/me/settings/timezone so you render times in the user's local zone instead of UTC.

Before any destructive write (creating, moving, or cancelling an event that has attendees) show the exact event details and ask the user to confirm. When attendees are involved, also confirm whether they want Google to email the attendees — that's controlled by the sendUpdates query parameter.

Optional: Google Workspace CLI (gws) for agenda + create

gws is Google's official CLI (not officially supported — community-maintained on the googleworkspace org). It dynamically builds its command surface from Google's Discovery Document, exits non-zero on API errors, and ships hand-crafted helper commands (prefixed +) for time-aware workflows.

Use gws for two specific cases:

  • +agenda reads the user's account timezone from Settings.timezone (cached for 24 h) and renders today's events in that zone, so you don't have to fetch the timezone yourself before formatting times.
  • +insert shapes the create-event JSON for you (attendees, sendUpdates, reminders) so a one-line invocation produces a well-formed request.

For everything else (events.list / patch / move / delete, freebusy, calendarList) the curl recipes below are equivalent and shorter — stay on those.

Install

npm install -g @googleworkspace/cli   # or: brew install googleworkspace-cli
# Pre-built binaries also at https://github.com/googleworkspace/cli/releases
gws --version

Auth

gws reads its OAuth bearer token from the GOOGLE_WORKSPACE_CLI_TOKEN environment variable. The Calendar token used in this skill is in $GOOGLE_CALENDAR_TOKEN, so re-export it once at the top of every shell block that calls gws:

export GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_CALENDAR_TOKEN"

Agenda + create

# Today on the primary calendar, in the account's own timezone
gws calendar +agenda

# Today / week, with explicit overrides
gws calendar +agenda --today --tz America/New_York
gws calendar +agenda --range week

# Create an event (auto-shapes attendees + sendUpdates JSON)
gws calendar +insert --calendar primary \
  --json '{
    "summary":"Standup",
    "start":{"dateTime":"2026-05-06T10:00:00-04:00"},
    "end":  {"dateTime":"2026-05-06T10:30:00-04:00"},
    "attendees":[{"email":"[email protected]"}]
  }' \
  --params '{"sendUpdates":"all"}'

Both helpers exit non-zero with a structured JSON error on stderr if Google rejects the request — surface that verbatim. +insert against attendees requires the broader calendar scope; on 403 insufficientPermissions ask the user to re-install with read+write checked.

Recipes

Verify auth + discover calendars (always run first)

# Account confirmation + calendars the user can read
curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  "https://www.googleapis.com/calendar/v3/users/me/calendarList" \
  | jq '.items[] | {id, summary, primary, accessRole, timeZone}'

# User's preferred display zone (use this when formatting times)
curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  "https://www.googleapis.com/calendar/v3/users/me/settings/timezone" \
  | jq -r .value

The id of each calendar (primary, or an email-shaped id like [email protected]) is what subsequent calendars/{id}/events calls take.

Today's agenda on the primary calendar

TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  "https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)
TODAY=$(TZ=$TZ date +%Y-%m-%d)
START="${TODAY}T00:00:00Z"
END="${TODAY}T23:59:59Z"

curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  --get "https://www.googleapis.com/calendar/v3/calendars/primary/events" \
  --data-urlencode "timeMin=$START" \
  --data-urlencode "timeMax=$END" \
  --data-urlencode 'singleEvents=true' \
  --data-urlencode 'orderBy=startTime' \
  --data-urlencode "timeZone=$TZ" \
  | jq '.items[] | {summary, start: (.start.dateTime // .start.date), end: (.end.dateTime // .end.date), location, attendees: [.attendees[]?.email], hangout: .hangoutLink, status, htmlLink}'

singleEvents=true flattens recurring meetings into individual instances — almost always what you want for an agenda. Without it, you'd get the recurrence rule once and have to expand it client-side.

This week's meetings (Mon–Sun)

TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  "https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)
# Bash date math: Monday-of-this-week
MON=$(TZ=$TZ date -d "$(TZ=$TZ date +%Y-%m-%d) -$(($(TZ=$TZ date +%u) - 1)) days" +%Y-%m-%d 2>/dev/null \
  || TZ=$TZ date -v-mondayw +%Y-%m-%d)  # macOS fallback
SUN=$(TZ=$TZ date -d "$MON +6 days" +%Y-%m-%d 2>/dev/null \
  || TZ=$TZ date -v+6d -j -f %Y-%m-%d "$MON" +%Y-%m-%d)

curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  --get "https://www.googleapis.com/calendar/v3/calendars/primary/events" \
  --data-urlencode "timeMin=${MON}T00:00:00Z" \
  --data-urlencode "timeMax=${SUN}T23:59:59Z" \
  --data-urlencode 'singleEvents=true' \
  --data-urlencode 'orderBy=startTime' \
  | jq -r '.items[] | "\(.start.dateTime // .start.date)\t\(.summary)\t\((.attendees // []) | length) attendees"'

Search events by query

Q='quarterly review'
curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  --get "https://www.googleapis.com/calendar/v3/calendars/primary/events" \
  --data-urlencode "q=$Q" \
  --data-urlencode 'singleEvents=true' \
  --data-urlencode 'maxResults=20' \
  | jq '.items[] | {start: .start.dateTime, summary, htmlLink}'

q matches against summary, description, location, attendee emails, and creator/organizer.

Get one event's full details (incl. attendees, location, link)

EVENT_ID='abc123def4567890ghijklmnop'
curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID" \
  | jq '{summary, start, end, location, description, attendees, organizer, hangoutLink, conferenceData}'

Free / busy across multiple calendars (next 7 days)

TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  "https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)
NOW=$(TZ=$TZ date -u +%Y-%m-%dT%H:%M:%SZ)
NEXT_WEEK=$(TZ=$TZ date -u -d "+7 days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \
  || TZ=$TZ date -u -v+7d +%Y-%m-%dT%H:%M:%SZ)

cat > /tmp/freebusy.json <<JSON
{
  "timeMin": "$NOW",
  "timeMax": "$NEXT_WEEK",
  "timeZone": "$TZ",
  "items": [
    {"id": "primary"},
    {"id": "[email protected]"}
  ]
}
JSON

curl -sS -X POST -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  -H 'Content-Type: application/json' \
  --data @/tmp/freebusy.json \
  "https://www.googleapis.com/calendar/v3/freeBusy" \
  | jq '.calendars'

Each calendar's response is {"busy": [{"start": "...", "end": "..."}]} — gaps between are free.

List events on a non-primary calendar

CAL_ID='[email protected]'
# URL-encode the @ in the path
CAL_ENCODED=$(printf %s "$CAL_ID" | jq -sRr @uri)
curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  --get "https://www.googleapis.com/calendar/v3/calendars/$CAL_ENCODED/events" \
  --data-urlencode 'singleEvents=true' \
  --data-urlencode 'orderBy=startTime' \
  --data-urlencode 'maxResults=20' \
  | jq '.items[] | {start: .start.dateTime, summary}'

Pagination

PAGE_TOKEN=''
while : ; do
  RESP=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
    --get "https://www.googleapis.com/calendar/v3/calendars/primary/events" \
    --data-urlencode 'singleEvents=true' \
    --data-urlencode 'orderBy=startTime' \
    --data-urlencode 'maxResults=250' \
    ${PAGE_TOKEN:+--data-urlencode "pageToken=$PAGE_TOKEN"})
  echo "$RESP" | jq -c '.items[]?'
  PAGE_TOKEN=$(echo "$RESP" | jq -r '.nextPageToken // empty')
  [ -z "$PAGE_TOKEN" ] && break
done

Write recipes

These all need the broader calendar scope. If the user only granted calendar.readonly you'll get 403 insufficientPermissions — surface that and ask them to re-install with the read+write box checked. Always echo the event summary, time and attendee list back to the user before creating or cancelling anything.

Create a single event (with optional attendees + Google Meet link)

TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  "https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)

cat > /tmp/_cal_event.json <<JSON
{
  "summary": "Sync — Q2 OKR review",
  "location": "Online",
  "description": "Drafted by AceDataCloud.",
  "start": {"dateTime": "2026-05-12T10:00:00", "timeZone": "$TZ"},
  "end":   {"dateTime": "2026-05-12T10:30:00", "timeZone": "$TZ"},
  "attendees": [
    {"email": "[email protected]"},
    {"email": "[email protected]"}
  ],
  "reminders": {"useDefault": true},
  "conferenceData": {
    "createRequest": {
      "requestId": "meet-$(date +%s)",
      "conferenceSolutionKey": {"type": "hangoutsMeet"}
    }
  }
}
JSON

# sendUpdates: 'all' = email all attendees; 'externalOnly' = only non-org; 'none' = silent
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  -H 'Content-Type: application/json' \
  --data @/tmp/_cal_event.json \
  "https://www.googleapis.com/calendar/v3/calendars/primary/events?conferenceDataVersion=1&sendUpdates=all" \
  | jq '{id, htmlLink, hangoutLink, summary, start, end, attendees}'

Drop the conferenceData block if the user didn't ask for a Meet link — it'll fall back to a plain event.

Create a recurring event

TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  "https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)
cat > /tmp/_cal_recur.json <<JSON
{
  "summary": "Weekly 1:1",
  "start": {"dateTime": "2026-05-12T15:00:00", "timeZone": "$TZ"},
  "end":   {"dateTime": "2026-05-12T15:30:00", "timeZone": "$TZ"},
  "recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=12"]
}
JSON
curl -sS -X POST -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  -H 'Content-Type: application/json' \
  --data @/tmp/_cal_recur.json \
  "https://www.googleapis.com/calendar/v3/calendars/primary/events" \
  | jq '{id, recurrence, summary}'

RRULE follows RFC 5545. Common patterns: FREQ=DAILY, FREQ=WEEKLY;BYDAY=MO,WE,FR, FREQ=MONTHLY;BYMONTHDAY=15. Add UNTIL=20261231T235959Z or COUNT=12 for a hard stop.

Update an existing event (PATCH — partial update)

EVENT_ID='abc123def4567890ghijklmnop'
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"location":"Conference Room 4","description":"Now in-person."}' \
  "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
  | jq '{id, summary, location, description}'

PATCH only changes the fields you send; PUT replaces the entire event payload. Prefer PATCH.

Reschedule an event

EVENT_ID='abc123def4567890ghijklmnop'
TZ=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  "https://www.googleapis.com/calendar/v3/users/me/settings/timezone" | jq -r .value)
cat > /tmp/_cal_resched.json <<JSON
{
  "start": {"dateTime": "2026-05-12T14:00:00", "timeZone": "$TZ"},
  "end":   {"dateTime": "2026-05-12T14:30:00", "timeZone": "$TZ"}
}
JSON
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  -H 'Content-Type: application/json' \
  --data @/tmp/_cal_resched.json \
  "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
  | jq '{id, summary, start, end}'

Add or change attendees

Google requires you to send the complete attendee list when patching attendees — fetch the current list, mutate, send back:

EVENT_ID='abc123def4567890ghijklmnop'
CURRENT=$(curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?fields=attendees" \
  | jq '.attendees // []')
NEW=$(echo "$CURRENT" | jq '. + [{"email":"[email protected]"}]')
curl -sS -X PATCH -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  -H 'Content-Type: application/json' \
  --data "{\"attendees\": $NEW}" \
  "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
  | jq '{id, attendees}'

Cancel / delete an event

EVENT_ID='abc123def4567890ghijklmnop'
curl -sS -X DELETE -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN" \
  "https://www.googleapis.com/calendar/v3/calendars/primary/events/$EVENT_ID?sendUpdates=all" \
  -o /dev/null -w 'HTTP %{http_code}\n'

204 = success. To cancel one occurrence of a recurring event, fetch the instance with events.instances first, then DELETE the specific instance id (it has a longer EVENT_ID_YYYYMMDDTHHMMSSZ shape).

Common error codes

HTTPmeaningwhat to tell the user
401 UNAUTHENTICATEDtoken expired / revoked"Reconnect the Google Calendar connector on the Connections page."
403 insufficientPermissionswrite scope missing"This action needs the Calendar read+write scope, but only calendar.readonly was granted. Re-install the connector with the read+write box checked."
403 forbiddencalendar id not visible to this accountcheck calendarList first; if it's a shared calendar, the owner needs to share it.
404 notFoundwrong event / calendar iddouble-check the id and try calendarList to confirm the calendar exists.
409 conflictrecurring event id collisionappend a UUID to your requestId and retry.
429 quotaExceededquota / throttlingback off ~5s, then retry once.

Never log or echo $GOOGLE_CALENDAR_TOKEN — treat it as a secret.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.