google-calendar

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
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.
通过
curl + jq
操作Google Calendar。用户的OAuth bearer token存储在
$GOOGLE_CALENDAR_TOKEN
中;每次调用都需要将其作为
Authorization: Bearer $GOOGLE_CALENDAR_TOKEN
传入。该令牌至少包含
calendar.readonly
权限以及身份范围(
openid email profile
);如果用户在安装时选择了写入权限,它还会包含更广泛的
calendar
权限(读写权限)。
Calendar API返回标准JSON格式;失败时会返回
{"error": {"code": 401|403|..., "message": "..."}}
—— 请直接显示该错误信息。
401
表示令牌已过期(需要重新安装)。如果写入操作返回
403 insufficientPermissions
,说明用户仅授予了
calendar.readonly
权限 —— 请提示用户重新安装连接器并勾选读写权限选项。
**始终先调用
users/me/calendarList
**以了解该账户可查看的日历(包括用户的主日历以及任何订阅/共享的日历),同时调用
users/me/settings/timezone
,以便按照用户的本地时区而非UTC时区显示时间。
在执行任何破坏性写入操作之前(创建、移动或取消有参会者的事件),请显示确切的事件详情并请求用户确认。当涉及参会者时,还需确认是否希望Google向参会者发送邮件 —— 这由
sendUpdates
查询参数控制。

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

可选:用于日程查看和创建的Google Workspace CLI (
gws
)

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.
gws
是Google官方的CLI工具(非官方支持 —— 由
googleworkspace
组织的社区维护)。它会根据Google的Discovery Document动态构建命令界面,API错误时返回非零退出码,并提供针对时间相关工作流的手工编写的辅助命令(以
+
为前缀)。
在以下两种特定场景中使用
gws
  • +agenda
    会从
    Settings.timezone
    读取用户账户的时区(缓存24小时),并以该时区显示今日事件,因此你无需在格式化时间前自行获取时区。
  • +insert
    会帮你生成创建事件的JSON结构(参会者、sendUpdates、提醒),因此只需一行命令即可生成格式正确的请求。
对于其他所有操作(events.list / patch / move / delete、freebusy、calendarList),下面的curl方案功能等效且更简洁 —— 请使用这些方案。

Install

安装

sh
npm install -g @googleworkspace/cli   # or: brew install googleworkspace-cli
sh
npm install -g @googleworkspace/cli   # 或:brew install googleworkspace-cli
gws --version
undefined
gws --version
undefined

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
:
sh
export GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_CALENDAR_TOKEN"
gws
GOOGLE_WORKSPACE_CLI_TOKEN
环境变量读取其OAuth bearer token。本技能中使用的Calendar token存储在
$GOOGLE_CALENDAR_TOKEN
中,因此在每个调用
gws
的shell代码块顶部,需重新导出一次:
sh
export GOOGLE_WORKSPACE_CLI_TOKEN="$GOOGLE_CALENDAR_TOKEN"

Agenda + create

日程查看与创建

sh
undefined
sh
undefined

Today on the primary calendar, in the account's own timezone

主日历的今日日程,使用账户自身的时区

gws calendar +agenda
gws calendar +agenda

Today / week, with explicit overrides

今日/本周日程,使用显式覆盖参数

gws calendar +agenda --today --tz America/New_York gws calendar +agenda --range week
gws calendar +agenda --today --tz America/New_York gws calendar +agenda --range week

Create an event (auto-shapes attendees + sendUpdates JSON)

创建事件(自动生成参会者+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":"alice@example.com"}] }'
--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.
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":"alice@example.com"}] }'
--params '{"sendUpdates":"all"}'

如果Google拒绝请求,这两个辅助命令都会返回非零退出码,并在stderr输出结构化JSON错误 —— 请直接显示该错误信息。针对参会者执行`+insert`操作需要更广泛的`calendar`权限;如果返回`403 insufficientPermissions`,请提示用户重新安装并勾选读写权限选项。

Recipes

操作方案

Verify auth + discover calendars (always run first)

验证认证并发现日历(始终先执行)

sh
undefined
sh
undefined

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}'
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
`team-monday@group.calendar.google.com`) is what subsequent
`calendars/{id}/events` calls take.
curl -sS -H "Authorization: Bearer $GOOGLE_CALENDAR_TOKEN"
"https://www.googleapis.com/calendar/v3/users/me/settings/timezone"
| jq -r .value

每个日历的`id`(`primary`,或类似`team-monday@group.calendar.google.com`的邮箱格式id)是后续`calendars/{id}/events`调用所需的参数。

Today's agenda on the primary calendar

主日历的今日日程

sh
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.
sh
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
会将重复会议展开为单独的实例 —— 这几乎总是日程查看所需的模式。如果不设置该参数,你只会获取一次重复规则,需要在客户端自行展开。

This week's meetings (Mon–Sun)

本周会议(周一至周日)

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

Bash日期计算:本周一

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"'
undefined
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兼容方案 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"'
undefined

Search events by query

按查询条件搜索事件

sh
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.
sh
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
会匹配摘要、描述、地点、参会者邮箱以及创建者/组织者信息。

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

获取单个事件的完整详情(包括参会者、地点、链接)

sh
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}'
sh
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)

多个日历的忙闲状态(未来7天)

sh
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": "team-monday@group.calendar.google.com"}
  ]
}
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.
sh
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": "team-monday@group.calendar.google.com"}
  ]
}
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'
每个日历的响应格式为
{"busy": [{"start": "...", "end": "..."}]}
—— 时间段之间的间隙即为空闲时间。

List events on a non-primary calendar

列出非主日历的事件

sh
CAL_ID='team-monday@group.calendar.google.com'
sh
CAL_ID='team-monday@group.calendar.google.com'

URL-encode the @ in the path

对路径中的@进行URL编码

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}'
undefined
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}'
undefined

Pagination

分页查询

sh
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
sh
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.
这些操作都需要更广泛的
calendar
权限。如果用户仅授予了
calendar.readonly
权限,你会收到
403 insufficientPermissions
错误 —— 请显示该错误并提示用户重新安装连接器并勾选读写权限选项。在创建或取消任何事件之前,务必向用户回显事件摘要、时间和参会者列表。

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

创建单个事件(可选参会者+Google Meet链接)

sh
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": "alice@example.com"},
    {"email": "bob@example.com"}
  ],
  "reminders": {"useDefault": true},
  "conferenceData": {
    "createRequest": {
      "requestId": "meet-$(date +%s)",
      "conferenceSolutionKey": {"type": "hangoutsMeet"}
    }
  }
}
JSON
sh
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": "alice@example.com"},
    {"email": "bob@example.com"}
  ],
  "reminders": {"useDefault": true},
  "conferenceData": {
    "createRequest": {
      "requestId": "meet-$(date +%s)",
      "conferenceSolutionKey": {"type": "hangoutsMeet"}
    }
  }
}
JSON

sendUpdates: 'all' = email all attendees; 'externalOnly' = only non-org; 'none' = silent

sendUpdates: 'all' = 向所有参会者发送邮件; 'externalOnly' = 仅向外部参会者发送; 'none' = 不发送邮件

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.
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}'

如果用户不需要Meet链接,可删除`conferenceData`块 —— 事件会退化为普通事件。

Create a recurring event

创建重复事件

sh
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.
sh
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遵循RFC 5545标准。常见模式:
FREQ=DAILY
FREQ=WEEKLY;BYDAY=MO,WE,FR
FREQ=MONTHLY;BYMONTHDAY=15
。可添加
UNTIL=20261231T235959Z
COUNT=12
设置结束条件。

Update an existing event (PATCH — partial update)

更新现有事件(PATCH —— 部分更新)

sh
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
.
sh
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
仅修改你传入的字段;
PUT
会替换整个事件 payload。优先使用
PATCH

Reschedule an event

重新安排事件

sh
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}'
sh
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:
sh
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":"carol@example.com"}]')
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}'
Google要求在修改参会者时发送完整的参会者列表 —— 先获取当前列表,修改后再发送回去:
sh
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":"carol@example.com"}]')
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

取消/删除事件

sh
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).
sh
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
表示操作成功。要取消重复事件中的某一次,需先通过
events.instances
获取该实例,然后
DELETE
对应的实例id(格式为较长的
EVENT_ID_YYYYMMDDTHHMMSSZ
)。

Common error codes

常见错误码

HTTPmeaningwhat to tell the user
401 UNAUTHENTICATED
token expired / revoked"Reconnect the Google Calendar connector on the Connections page."
403 insufficientPermissions
write 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 forbidden
calendar id not visible to this accountcheck
calendarList
first; if it's a shared calendar, the owner needs to share it.
404 notFound
wrong event / calendar iddouble-check the id and try
calendarList
to confirm the calendar exists.
409 conflict
recurring event id collisionappend a UUID to your
requestId
and retry.
429 quotaExceeded
quota / throttlingback off ~5s, then retry once.
Never log or echo
$GOOGLE_CALENDAR_TOKEN
— treat it as a secret.
HTTP含义提示用户的内容
401 UNAUTHENTICATED
令牌过期/已撤销"在连接页面重新连接Google Calendar连接器。"
403 insufficientPermissions
缺少写入权限"此操作需要Calendar读写权限,但当前仅授予了
calendar.readonly
权限。请重新安装连接器并勾选读写权限选项。"
403 forbidden
该账户无法查看此日历id先检查
calendarList
;如果是共享日历,需要所有者共享该日历。
404 notFound
事件/日历id错误仔细检查id,并尝试调用
calendarList
确认日历是否存在。
409 conflict
重复事件id冲突
requestId
后附加UUID并重试。
429 quotaExceeded
配额/限流等待约5秒后重试一次。
切勿记录或显示
$GOOGLE_CALENDAR_TOKEN
—— 将其视为机密信息。