
Google Calendar
- 416 installs
- 3 repo stars
- Updated January 23, 2026
- dotneet/claude-code-marketplace
google-calendar is a Claude Code marketplace skill that connects applications and agents to Google Calendar for scheduling, availability checks, and automated event creation for developers building calendar-aware automat
About
google-calendar is a dotneet/claude-code-marketplace skill for wiring Google Calendar into apps and AI agents. It helps developers implement scheduling flows, read free-busy availability, and create or update events programmatically through Google's Calendar API rather than manual UI steps. Reach for it when building meeting bots, booking assistants, standup schedulers, or workflow agents that must propose times, confirm conflicts, and persist events to a user's calendar. The skill targets OAuth-connected automation where calendar state must stay synchronized with conversational or backend triggers.
- Google Calendar OAuth
- Event CRUD automation
- Availability lookups
- Agent scheduling hooks
Google Calendar by the numbers
- 416 all-time installs (skills.sh)
- Ranked #447 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dotneet/claude-code-marketplace --skill google-calendarAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 416 |
|---|---|
| repo stars | ★ 3 |
| Last updated | January 23, 2026 |
| Repository | dotneet/claude-code-marketplace ↗ |
How do agents create Google Calendar events automatically?
Connect apps or agents to Google Calendar for scheduling, availability checks, and automated event creation.
Who is it for?
Developers building scheduling agents, booking tools, or workflow automations that must read availability and write Google Calendar events.
Skip if: Teams standardized on Microsoft Outlook or CalDAV calendars without Google Workspace or Calendar API access.
When should I use this skill?
User asks to schedule meetings, check Google Calendar availability, or automate event creation from an agent or backend service.
What you get
Google Calendar API integrations with OAuth auth, availability queries, and programmatically created or updated calendar events.
- Calendar API integration code
- availability query handlers
- automated event records
Files
Google Calendar + Tasks Access
Quick start
- Create a Google Cloud OAuth client (Desktop app) and save the credentials JSON to
~/.config/google-calendar/credentials.json. - Install dependencies (venv recommended):
python3 -m venv ~/.config/google-calendar/venv
~/.config/google-calendar/venv/bin/pip install -r <skill_dir>/scripts/requirements.txt- Authenticate and store a token:
- Calendar only:
<skill_dir>/scripts/gcal-auth- Tasks only:
<skill_dir>/scripts/gcal-auth --scopes https://www.googleapis.com/auth/tasks- Calendar + Tasks (single token):
<skill_dir>/scripts/gcal-auth --scopes https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/tasks- Call the API:
<skill_dir>/scripts/gcal list-calendars
Credentials and tokens
- Credentials are expected at
~/.config/google-calendar/credentials.jsonby default. - Tokens are stored at
~/.config/google-calendar/token.jsonby default. - Use
--tokenorGCAL_TOKEN_PATHto switch accounts or isolate environments.
Date/time rules
- Use RFC3339 for timed events (e.g.,
2026-01-22T10:00:00-08:00). - Use
YYYY-MM-DDfor all-day events and setendto the next day (exclusive). - Set
--time-zonewhen the datetime has no offset.
Common commands
- List calendars:
<skill_dir>/scripts/gcal list-calendars- List events:
<skill_dir>/scripts/gcal list-events --calendar-id primary --time-min 2026-01-22T00:00:00-08:00 --time-max 2026-01-22T23:59:59-08:00- Search events:
<skill_dir>/scripts/gcal list-events --calendar-id primary --q "standup" --time-min ... --time-max ...- Create event (simple):
<skill_dir>/scripts/gcal create-event --calendar-id primary --summary "Review" --start 2026-01-22T10:00:00-08:00 --end 2026-01-22T11:00:00-08:00- Update event (simple):
<skill_dir>/scripts/gcal update-event --calendar-id primary --event-id <id> --summary "New title"- Delete event:
<skill_dir>/scripts/gcal delete-event --calendar-id primary --event-id <id>- Free/busy:
<skill_dir>/scripts/gcal freebusy --calendars primary,team@company.com --time-min ... --time-max ...- List task lists:
<skill_dir>/scripts/gcal list-tasklists- List tasks:
<skill_dir>/scripts/gcal list-tasks --tasklist <tasklist_id>- Create task:
<skill_dir>/scripts/gcal create-task --tasklist <tasklist_id> --title "Buy milk" --due 2026-01-22T10:00:00-08:00- Update task:
<skill_dir>/scripts/gcal update-task --tasklist <tasklist_id> --task-id <id> --notes "Bring receipt"- Delete task:
<skill_dir>/scripts/gcal delete-task --tasklist <tasklist_id> --task-id <id>
Advanced usage
- Use
callto hit any Calendar API endpoint: <skill_dir>/scripts/gcal call GET /users/me/calendarList<skill_dir>/scripts/gcal call POST /calendars/primary/events --body-file /path/to/event.json- Use
--body-fileor--body(JSON string) for complex payloads (attendees, recurrence, conferenceData). - Run multiple accounts by using different token files with
--token.
Safety
- Confirm intent before creating, updating, or deleting events.
- Keep OAuth credentials and token files out of git.
Google Calendar + Tasks CLI
This skill provides a lightweight Python CLI for Google Calendar and Google Tasks API access using OAuth 2.0. It is useful for listing/creating/updating events and tasks without using the MCP server.
Requirements
- Python 3.9+ recommended
- Google Cloud project with the Google Calendar API and Google Tasks API enabled
- OAuth client credentials (Desktop app)
Install dependencies (venv recommended):
python3 -m venv ~/.config/google-calendar/venv
~/.config/google-calendar/venv/bin/pip install -r <skill_dir>/scripts/requirements.txtThe wrapper scripts (gcal, gcal-auth) automatically use the venv at ~/.config/google-calendar/venv if it exists.
Credentials (OAuth client)
1. Open Google Cloud Console and select/create a project. 2. Enable Google Calendar API and Google Tasks API. 3. Configure the OAuth consent screen (External or Internal). 4. Create credentials:
- Create Credentials → OAuth client ID → Desktop app
5. Download the JSON file (client secret) and save it to ~/.config/google-calendar/credentials.json.
Keep this file out of git and in a secure location.
Authentication (token)
Authenticate and store a token:
- Calendar only:
<skill_dir>/scripts/gcal-auth- Tasks only:
<skill_dir>/scripts/gcal-auth \
--scopes https://www.googleapis.com/auth/tasks- Calendar + Tasks (single token):
<skill_dir>/scripts/gcal-auth \
--scopes https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/tasksBy default, the token is stored at:
~/.config/google-calendar/token.jsonYou can override the token path with either:
--token /custom/path/token.jsonGCAL_TOKEN_PATH=/custom/path/token.json
Token management tips
- Use different token files for different Google accounts.
- Remove the token file to force re-authentication.
- Tokens contain refresh tokens; keep them secure and out of version control.
Environment variables
GCAL_CREDENTIALS: path to the client secret JSON (default:~/.config/google-calendar/credentials.json).GCAL_TOKEN_PATH: default path for the OAuth token JSON.
Common commands
List calendars:
<skill_dir>/scripts/gcal list-calendarsList events:
<skill_dir>/scripts/gcal list-events \
--calendar-id primary \
--time-min 2026-01-01T00:00:00-08:00 \
--time-max 2026-02-01T00:00:00-08:00Create an event:
<skill_dir>/scripts/gcal create-event \
--calendar-id primary \
--summary "Review" \
--start 2026-01-22T10:00:00-08:00 \
--end 2026-01-22T11:00:00-08:00List task lists:
<skill_dir>/scripts/gcal list-tasklistsList tasks:
<skill_dir>/scripts/gcal list-tasks \
--tasklist <tasklist_id>Create a task:
<skill_dir>/scripts/gcal create-task \
--tasklist <tasklist_id> \
--title "Buy milk" \
--due 2026-01-22T10:00:00-08:00Date/time rules
- Timed events/tasks use RFC3339 (e.g.,
2026-01-22T10:00:00-08:00). - All-day events use
YYYY-MM-DDand end on the next day (exclusive). - Use
--time-zonewhen the datetime lacks an offset.
Safety
- Confirm intent before creating/updating/deleting events or tasks.
- Keep OAuth credentials and token files out of git.
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="${HOME}/.config/google-calendar/venv"
if [[ -x "${VENV_DIR}/bin/python" ]]; then
exec "${VENV_DIR}/bin/python" "${SCRIPT_DIR}/gcal_api.py" "$@"
else
exec python3 "${SCRIPT_DIR}/gcal_api.py" "$@"
fi
#!/usr/bin/env python3
import argparse
import json
import os
import pathlib
import sys
from typing import Any, Dict, Optional
from google.auth.transport.requests import AuthorizedSession, Request
from google.oauth2.credentials import Credentials
CALENDAR_BASE_URL = "https://www.googleapis.com/calendar/v3"
TASKS_BASE_URL = "https://tasks.googleapis.com/tasks/v1"
DEFAULT_CALENDAR_SCOPES = ["https://www.googleapis.com/auth/calendar"]
DEFAULT_TASKS_SCOPES = ["https://www.googleapis.com/auth/tasks"]
def load_json_input(raw: Optional[str], path: Optional[str]) -> Optional[Dict[str, Any]]:
if raw and path:
raise ValueError("Use only one of --body/--params or --body-file/--params-file")
if path:
with open(path, "r", encoding="utf-8") as handle:
return json.load(handle)
if raw:
return json.loads(raw)
return None
def load_credentials(token_path: str, scopes: Optional[list]) -> Credentials:
resolved = pathlib.Path(token_path).expanduser().resolve()
if not resolved.exists():
raise FileNotFoundError(f"Token file not found: {resolved}")
creds = Credentials.from_authorized_user_file(str(resolved), scopes=scopes)
if creds.expired and creds.refresh_token:
creds.refresh(Request())
resolved.write_text(creds.to_json())
return creds
def session_from_token(token_path: str, scopes: Optional[list]) -> AuthorizedSession:
creds = load_credentials(token_path, scopes)
return AuthorizedSession(creds)
def request(
session: AuthorizedSession,
method: str,
path: str,
params: Optional[Dict[str, Any]],
body: Any,
base_url: str = CALENDAR_BASE_URL,
) -> Any:
if path.startswith("http://") or path.startswith("https://"):
url = path
else:
if not path.startswith("/"):
path = "/" + path
url = base_url + path
response = session.request(method=method, url=url, params=params, json=body)
content_type = response.headers.get("Content-Type", "")
if response.status_code >= 400:
if "application/json" in content_type:
error_payload = response.json()
else:
error_payload = {"error": response.text}
raise RuntimeError(f"HTTP {response.status_code}: {json.dumps(error_payload, indent=2)}")
if response.status_code == 204:
return {"status": "deleted"}
if "application/json" in content_type:
return response.json()
return {"text": response.text}
def time_object(value: str, time_zone: Optional[str]) -> Dict[str, str]:
if "T" in value:
obj = {"dateTime": value}
if time_zone and ("Z" not in value and "+" not in value and "-" not in value[10:]):
obj["timeZone"] = time_zone
return obj
return {"date": value}
def parse_bool(value: str) -> bool:
normalized = value.strip().lower()
if normalized in {"true", "1", "yes", "y"}:
return True
if normalized in {"false", "0", "no", "n"}:
return False
raise argparse.ArgumentTypeError("Expected a boolean value (true/false).")
def add_common_auth_args(parser: argparse.ArgumentParser, default_scopes: Optional[list] = None) -> None:
parser.add_argument(
"--token",
default=os.environ.get(
"GCAL_TOKEN_PATH",
os.path.expanduser("~/.config/google-calendar/token.json"),
),
help="Path to OAuth token JSON.",
)
if default_scopes is None:
default_scopes = DEFAULT_CALENDAR_SCOPES
parser.add_argument(
"--scopes",
nargs="*",
default=default_scopes,
help="OAuth scopes (space separated).",
)
def cmd_call(args: argparse.Namespace) -> Any:
params = load_json_input(args.params, args.params_file)
body = load_json_input(args.body, args.body_file)
session = session_from_token(args.token, args.scopes)
return request(session, args.method.upper(), args.path, params, body)
def cmd_list_calendars(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
params = {}
if args.min_access_role:
params["minAccessRole"] = args.min_access_role
if args.max_results:
params["maxResults"] = args.max_results
if args.page_token:
params["pageToken"] = args.page_token
return request(session, "GET", "/users/me/calendarList", params, None)
def cmd_list_events(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
params: Dict[str, Any] = {
"timeMin": args.time_min,
"timeMax": args.time_max,
}
if args.q:
params["q"] = args.q
if args.single_events:
params["singleEvents"] = True
if args.order_by:
params["orderBy"] = args.order_by
if args.max_results:
params["maxResults"] = args.max_results
if args.time_zone:
params["timeZone"] = args.time_zone
if args.page_token:
params["pageToken"] = args.page_token
if args.fields:
params["fields"] = args.fields
path = f"/calendars/{args.calendar_id}/events"
return request(session, "GET", path, params, None)
def cmd_get_event(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
params = {"fields": args.fields} if args.fields else None
path = f"/calendars/{args.calendar_id}/events/{args.event_id}"
return request(session, "GET", path, params, None)
def cmd_create_event(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
body = load_json_input(args.body, args.body_file)
if body is None:
if not args.summary or not args.start or not args.end:
raise ValueError("--summary, --start, and --end are required unless --body is provided")
body = {
"summary": args.summary,
"start": time_object(args.start, args.time_zone),
"end": time_object(args.end, args.time_zone),
}
if args.location:
body["location"] = args.location
if args.description:
body["description"] = args.description
if args.attendees:
body["attendees"] = [{"email": email} for email in args.attendees.split(",") if email]
if args.recurrence:
body["recurrence"] = args.recurrence
path = f"/calendars/{args.calendar_id}/events"
params = {"sendUpdates": args.send_updates} if args.send_updates else None
return request(session, "POST", path, params, body)
def cmd_update_event(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
body = load_json_input(args.body, args.body_file)
if body is None:
body = {}
if args.summary:
body["summary"] = args.summary
if args.start:
body["start"] = time_object(args.start, args.time_zone)
if args.end:
body["end"] = time_object(args.end, args.time_zone)
if args.location:
body["location"] = args.location
if args.description:
body["description"] = args.description
if args.attendees:
body["attendees"] = [{"email": email} for email in args.attendees.split(",") if email]
if args.recurrence:
body["recurrence"] = args.recurrence
if not body:
raise ValueError("Provide fields to update or pass --body/--body-file")
path = f"/calendars/{args.calendar_id}/events/{args.event_id}"
params = {"sendUpdates": args.send_updates} if args.send_updates else None
return request(session, "PATCH", path, params, body)
def cmd_delete_event(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
path = f"/calendars/{args.calendar_id}/events/{args.event_id}"
params = {"sendUpdates": args.send_updates} if args.send_updates else None
return request(session, "DELETE", path, params, None)
def cmd_freebusy(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
items = [{"id": cal_id} for cal_id in args.calendars.split(",") if cal_id]
body = {
"timeMin": args.time_min,
"timeMax": args.time_max,
"items": items,
}
if args.time_zone:
body["timeZone"] = args.time_zone
return request(session, "POST", "/freeBusy", None, body)
def cmd_list_tasklists(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
params: Dict[str, Any] = {}
if args.max_results:
params["maxResults"] = args.max_results
if args.page_token:
params["pageToken"] = args.page_token
if args.fields:
params["fields"] = args.fields
return request(session, "GET", "/users/@me/lists", params, None, TASKS_BASE_URL)
def cmd_get_tasklist(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
params = {"fields": args.fields} if args.fields else None
path = f"/users/@me/lists/{args.tasklist}"
return request(session, "GET", path, params, None, TASKS_BASE_URL)
def cmd_create_tasklist(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
body = load_json_input(args.body, args.body_file)
if body is None:
if not args.title:
raise ValueError("--title is required unless --body is provided")
body = {"title": args.title}
return request(session, "POST", "/users/@me/lists", None, body, TASKS_BASE_URL)
def cmd_update_tasklist(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
body = load_json_input(args.body, args.body_file)
if body is None:
body = {}
if args.title:
body["title"] = args.title
if not body:
raise ValueError("Provide fields to update or pass --body/--body-file")
path = f"/users/@me/lists/{args.tasklist}"
return request(session, "PATCH", path, None, body, TASKS_BASE_URL)
def cmd_delete_tasklist(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
path = f"/users/@me/lists/{args.tasklist}"
return request(session, "DELETE", path, None, None, TASKS_BASE_URL)
def task_body_from_args(args: argparse.Namespace) -> Dict[str, Any]:
body: Dict[str, Any] = {}
if args.title:
body["title"] = args.title
if args.notes:
body["notes"] = args.notes
if args.due:
body["due"] = args.due
if args.status:
body["status"] = args.status
if args.completed:
body["completed"] = args.completed
return body
def cmd_list_tasks(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
params: Dict[str, Any] = {}
if args.completed_max:
params["completedMax"] = args.completed_max
if args.completed_min:
params["completedMin"] = args.completed_min
if args.due_max:
params["dueMax"] = args.due_max
if args.due_min:
params["dueMin"] = args.due_min
if args.max_results:
params["maxResults"] = args.max_results
if args.page_token:
params["pageToken"] = args.page_token
if args.show_completed is not None:
params["showCompleted"] = "true" if args.show_completed else "false"
if args.show_deleted is not None:
params["showDeleted"] = "true" if args.show_deleted else "false"
if args.show_hidden is not None:
params["showHidden"] = "true" if args.show_hidden else "false"
if args.show_assigned is not None:
params["showAssigned"] = "true" if args.show_assigned else "false"
if args.updated_min:
params["updatedMin"] = args.updated_min
if args.fields:
params["fields"] = args.fields
path = f"/lists/{args.tasklist}/tasks"
return request(session, "GET", path, params, None, TASKS_BASE_URL)
def cmd_get_task(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
params = {"fields": args.fields} if args.fields else None
path = f"/lists/{args.tasklist}/tasks/{args.task_id}"
return request(session, "GET", path, params, None, TASKS_BASE_URL)
def cmd_create_task(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
body = load_json_input(args.body, args.body_file)
if body is None:
if not args.title:
raise ValueError("--title is required unless --body is provided")
body = task_body_from_args(args)
params: Dict[str, Any] = {}
if args.parent:
params["parent"] = args.parent
if args.previous:
params["previous"] = args.previous
path = f"/lists/{args.tasklist}/tasks"
return request(session, "POST", path, params, body, TASKS_BASE_URL)
def cmd_update_task(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
body = load_json_input(args.body, args.body_file)
if body is None:
body = task_body_from_args(args)
if not body:
raise ValueError("Provide fields to update or pass --body/--body-file")
path = f"/lists/{args.tasklist}/tasks/{args.task_id}"
return request(session, "PATCH", path, None, body, TASKS_BASE_URL)
def cmd_delete_task(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
path = f"/lists/{args.tasklist}/tasks/{args.task_id}"
return request(session, "DELETE", path, None, None, TASKS_BASE_URL)
def cmd_move_task(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
params: Dict[str, Any] = {}
if args.parent:
params["parent"] = args.parent
if args.previous:
params["previous"] = args.previous
if args.destination_tasklist:
params["destinationTasklist"] = args.destination_tasklist
path = f"/lists/{args.tasklist}/tasks/{args.task_id}/move"
return request(session, "POST", path, params, None, TASKS_BASE_URL)
def cmd_clear_tasks(args: argparse.Namespace) -> Any:
session = session_from_token(args.token, args.scopes)
path = f"/lists/{args.tasklist}/clear"
return request(session, "POST", path, None, None, TASKS_BASE_URL)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Google Calendar + Tasks API CLI.")
subparsers = parser.add_subparsers(dest="command", required=True)
call_parser = subparsers.add_parser("call", help="Call an arbitrary Calendar API endpoint")
add_common_auth_args(call_parser)
call_parser.add_argument("method", help="HTTP method (GET/POST/PATCH/DELETE)")
call_parser.add_argument("path", help="API path, e.g., /calendars/primary/events")
call_parser.add_argument("--params", help="Query params as JSON string")
call_parser.add_argument("--params-file", help="Path to query params JSON file")
call_parser.add_argument("--body", help="Request body as JSON string")
call_parser.add_argument("--body-file", help="Path to request body JSON file")
call_parser.set_defaults(func=cmd_call)
list_cal_parser = subparsers.add_parser("list-calendars", help="List calendars")
add_common_auth_args(list_cal_parser)
list_cal_parser.add_argument("--min-access-role", help="Filter by minimum access role")
list_cal_parser.add_argument("--max-results", type=int, help="Max results")
list_cal_parser.add_argument("--page-token", help="Page token")
list_cal_parser.set_defaults(func=cmd_list_calendars)
list_events_parser = subparsers.add_parser("list-events", help="List events")
add_common_auth_args(list_events_parser)
list_events_parser.add_argument("--calendar-id", required=True, help="Calendar ID (or 'primary')")
list_events_parser.add_argument("--time-min", required=True, help="RFC3339 start time")
list_events_parser.add_argument("--time-max", required=True, help="RFC3339 end time")
list_events_parser.add_argument("--q", help="Free text search query")
list_events_parser.add_argument("--single-events", action="store_true", help="Expand recurring events")
list_events_parser.add_argument("--order-by", help="Order by (e.g., startTime)")
list_events_parser.add_argument("--max-results", type=int, help="Max results")
list_events_parser.add_argument("--time-zone", help="IANA timezone")
list_events_parser.add_argument("--page-token", help="Page token")
list_events_parser.add_argument("--fields", help="Partial response fields")
list_events_parser.set_defaults(func=cmd_list_events)
get_event_parser = subparsers.add_parser("get-event", help="Get an event")
add_common_auth_args(get_event_parser)
get_event_parser.add_argument("--calendar-id", required=True, help="Calendar ID")
get_event_parser.add_argument("--event-id", required=True, help="Event ID")
get_event_parser.add_argument("--fields", help="Partial response fields")
get_event_parser.set_defaults(func=cmd_get_event)
create_parser = subparsers.add_parser("create-event", help="Create an event")
add_common_auth_args(create_parser)
create_parser.add_argument("--calendar-id", required=True, help="Calendar ID")
create_parser.add_argument("--summary", help="Event title")
create_parser.add_argument("--start", help="Start time (RFC3339 or date)")
create_parser.add_argument("--end", help="End time (RFC3339 or date)")
create_parser.add_argument("--time-zone", help="Timezone if start/end lack offset")
create_parser.add_argument("--location", help="Location")
create_parser.add_argument("--description", help="Description")
create_parser.add_argument("--attendees", help="Comma-separated attendee emails")
create_parser.add_argument("--recurrence", action="append", help="Repeat rule (RRULE:...)", default=[])
create_parser.add_argument("--send-updates", help="all|externalOnly|none")
create_parser.add_argument("--body", help="Request body as JSON string")
create_parser.add_argument("--body-file", help="Path to request body JSON file")
create_parser.set_defaults(func=cmd_create_event)
update_parser = subparsers.add_parser("update-event", help="Update an event")
add_common_auth_args(update_parser)
update_parser.add_argument("--calendar-id", required=True, help="Calendar ID")
update_parser.add_argument("--event-id", required=True, help="Event ID")
update_parser.add_argument("--summary", help="Event title")
update_parser.add_argument("--start", help="Start time (RFC3339 or date)")
update_parser.add_argument("--end", help="End time (RFC3339 or date)")
update_parser.add_argument("--time-zone", help="Timezone if start/end lack offset")
update_parser.add_argument("--location", help="Location")
update_parser.add_argument("--description", help="Description")
update_parser.add_argument("--attendees", help="Comma-separated attendee emails")
update_parser.add_argument("--recurrence", action="append", help="Repeat rule (RRULE:...)", default=[])
update_parser.add_argument("--send-updates", help="all|externalOnly|none")
update_parser.add_argument("--body", help="Request body as JSON string")
update_parser.add_argument("--body-file", help="Path to request body JSON file")
update_parser.set_defaults(func=cmd_update_event)
delete_parser = subparsers.add_parser("delete-event", help="Delete an event")
add_common_auth_args(delete_parser)
delete_parser.add_argument("--calendar-id", required=True, help="Calendar ID")
delete_parser.add_argument("--event-id", required=True, help="Event ID")
delete_parser.add_argument("--send-updates", help="all|externalOnly|none")
delete_parser.set_defaults(func=cmd_delete_event)
freebusy_parser = subparsers.add_parser("freebusy", help="Free/busy query")
add_common_auth_args(freebusy_parser)
freebusy_parser.add_argument("--calendars", required=True, help="Comma-separated calendar IDs")
freebusy_parser.add_argument("--time-min", required=True, help="RFC3339 start time")
freebusy_parser.add_argument("--time-max", required=True, help="RFC3339 end time")
freebusy_parser.add_argument("--time-zone", help="IANA timezone")
freebusy_parser.set_defaults(func=cmd_freebusy)
list_tasklists_parser = subparsers.add_parser("list-tasklists", help="List task lists")
add_common_auth_args(list_tasklists_parser, DEFAULT_TASKS_SCOPES)
list_tasklists_parser.add_argument("--max-results", type=int, help="Max results")
list_tasklists_parser.add_argument("--page-token", help="Page token")
list_tasklists_parser.add_argument("--fields", help="Partial response fields")
list_tasklists_parser.set_defaults(func=cmd_list_tasklists)
get_tasklist_parser = subparsers.add_parser("get-tasklist", help="Get a task list")
add_common_auth_args(get_tasklist_parser, DEFAULT_TASKS_SCOPES)
get_tasklist_parser.add_argument("--tasklist", required=True, help="Task list ID")
get_tasklist_parser.add_argument("--fields", help="Partial response fields")
get_tasklist_parser.set_defaults(func=cmd_get_tasklist)
create_tasklist_parser = subparsers.add_parser("create-tasklist", help="Create a task list")
add_common_auth_args(create_tasklist_parser, DEFAULT_TASKS_SCOPES)
create_tasklist_parser.add_argument("--title", help="Task list title")
create_tasklist_parser.add_argument("--body", help="Request body as JSON string")
create_tasklist_parser.add_argument("--body-file", help="Path to request body JSON file")
create_tasklist_parser.set_defaults(func=cmd_create_tasklist)
update_tasklist_parser = subparsers.add_parser("update-tasklist", help="Update a task list")
add_common_auth_args(update_tasklist_parser, DEFAULT_TASKS_SCOPES)
update_tasklist_parser.add_argument("--tasklist", required=True, help="Task list ID")
update_tasklist_parser.add_argument("--title", help="Task list title")
update_tasklist_parser.add_argument("--body", help="Request body as JSON string")
update_tasklist_parser.add_argument("--body-file", help="Path to request body JSON file")
update_tasklist_parser.set_defaults(func=cmd_update_tasklist)
delete_tasklist_parser = subparsers.add_parser("delete-tasklist", help="Delete a task list")
add_common_auth_args(delete_tasklist_parser, DEFAULT_TASKS_SCOPES)
delete_tasklist_parser.add_argument("--tasklist", required=True, help="Task list ID")
delete_tasklist_parser.set_defaults(func=cmd_delete_tasklist)
list_tasks_parser = subparsers.add_parser("list-tasks", help="List tasks in a task list")
add_common_auth_args(list_tasks_parser, DEFAULT_TASKS_SCOPES)
list_tasks_parser.add_argument("--tasklist", required=True, help="Task list ID")
list_tasks_parser.add_argument("--completed-max", help="RFC3339 completion time upper bound")
list_tasks_parser.add_argument("--completed-min", help="RFC3339 completion time lower bound")
list_tasks_parser.add_argument("--due-max", help="RFC3339 due time upper bound")
list_tasks_parser.add_argument("--due-min", help="RFC3339 due time lower bound")
list_tasks_parser.add_argument("--max-results", type=int, help="Max results")
list_tasks_parser.add_argument("--page-token", help="Page token")
list_tasks_parser.add_argument("--show-completed", type=parse_bool, help="true/false")
list_tasks_parser.add_argument("--show-deleted", type=parse_bool, help="true/false")
list_tasks_parser.add_argument("--show-hidden", type=parse_bool, help="true/false")
list_tasks_parser.add_argument("--show-assigned", type=parse_bool, help="true/false")
list_tasks_parser.add_argument("--updated-min", help="RFC3339 updated time lower bound")
list_tasks_parser.add_argument("--fields", help="Partial response fields")
list_tasks_parser.set_defaults(func=cmd_list_tasks)
get_task_parser = subparsers.add_parser("get-task", help="Get a task")
add_common_auth_args(get_task_parser, DEFAULT_TASKS_SCOPES)
get_task_parser.add_argument("--tasklist", required=True, help="Task list ID")
get_task_parser.add_argument("--task-id", required=True, help="Task ID")
get_task_parser.add_argument("--fields", help="Partial response fields")
get_task_parser.set_defaults(func=cmd_get_task)
create_task_parser = subparsers.add_parser("create-task", help="Create a task")
add_common_auth_args(create_task_parser, DEFAULT_TASKS_SCOPES)
create_task_parser.add_argument("--tasklist", required=True, help="Task list ID")
create_task_parser.add_argument("--title", help="Task title")
create_task_parser.add_argument("--notes", help="Task notes")
create_task_parser.add_argument("--due", help="RFC3339 due time")
create_task_parser.add_argument("--status", help="Task status (needsAction|completed)")
create_task_parser.add_argument("--completed", help="RFC3339 completion time")
create_task_parser.add_argument("--parent", help="Parent task ID")
create_task_parser.add_argument("--previous", help="Previous sibling task ID")
create_task_parser.add_argument("--body", help="Request body as JSON string")
create_task_parser.add_argument("--body-file", help="Path to request body JSON file")
create_task_parser.set_defaults(func=cmd_create_task)
update_task_parser = subparsers.add_parser("update-task", help="Update a task")
add_common_auth_args(update_task_parser, DEFAULT_TASKS_SCOPES)
update_task_parser.add_argument("--tasklist", required=True, help="Task list ID")
update_task_parser.add_argument("--task-id", required=True, help="Task ID")
update_task_parser.add_argument("--title", help="Task title")
update_task_parser.add_argument("--notes", help="Task notes")
update_task_parser.add_argument("--due", help="RFC3339 due time")
update_task_parser.add_argument("--status", help="Task status (needsAction|completed)")
update_task_parser.add_argument("--completed", help="RFC3339 completion time")
update_task_parser.add_argument("--body", help="Request body as JSON string")
update_task_parser.add_argument("--body-file", help="Path to request body JSON file")
update_task_parser.set_defaults(func=cmd_update_task)
delete_task_parser = subparsers.add_parser("delete-task", help="Delete a task")
add_common_auth_args(delete_task_parser, DEFAULT_TASKS_SCOPES)
delete_task_parser.add_argument("--tasklist", required=True, help="Task list ID")
delete_task_parser.add_argument("--task-id", required=True, help="Task ID")
delete_task_parser.set_defaults(func=cmd_delete_task)
move_task_parser = subparsers.add_parser("move-task", help="Move a task")
add_common_auth_args(move_task_parser, DEFAULT_TASKS_SCOPES)
move_task_parser.add_argument("--tasklist", required=True, help="Task list ID")
move_task_parser.add_argument("--task-id", required=True, help="Task ID")
move_task_parser.add_argument("--parent", help="New parent task ID")
move_task_parser.add_argument("--previous", help="New previous sibling task ID")
move_task_parser.add_argument("--destination-tasklist", help="Destination task list ID")
move_task_parser.set_defaults(func=cmd_move_task)
clear_tasks_parser = subparsers.add_parser("clear-tasks", help="Clear completed tasks")
add_common_auth_args(clear_tasks_parser, DEFAULT_TASKS_SCOPES)
clear_tasks_parser.add_argument("--tasklist", required=True, help="Task list ID")
clear_tasks_parser.set_defaults(func=cmd_clear_tasks)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
try:
result = args.func(args)
except Exception as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
print(json.dumps(result, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env python3
import argparse
import os
import pathlib
import sys
from google_auth_oauthlib.flow import InstalledAppFlow
DEFAULT_SCOPES = [
"https://www.googleapis.com/auth/calendar",
]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Authenticate Google Calendar API and store token.")
parser.add_argument(
"--credentials",
help="Path to OAuth client credentials JSON (Desktop app).",
default=os.environ.get(
"GCAL_CREDENTIALS",
os.path.expanduser("~/.config/google-calendar/credentials.json"),
),
)
parser.add_argument(
"--token",
help="Path to store OAuth token JSON.",
default=os.environ.get(
"GCAL_TOKEN_PATH",
os.path.expanduser("~/.config/google-calendar/token.json"),
),
)
parser.add_argument(
"--scopes",
nargs="*",
default=DEFAULT_SCOPES,
help="OAuth scopes (space separated).",
)
parser.add_argument(
"--no-browser",
action="store_true",
help="Use console-based auth flow (prints a URL).",
)
parser.add_argument(
"--port",
type=int,
default=0,
help="Local server port for auth (0 = auto).",
)
return parser.parse_args()
def ensure_parent(path: str) -> None:
parent = pathlib.Path(path).expanduser().resolve().parent
parent.mkdir(parents=True, exist_ok=True)
def main() -> int:
args = parse_args()
credentials_path = pathlib.Path(args.credentials).expanduser().resolve()
if not credentials_path.exists():
print(f"Credentials file not found: {credentials_path}", file=sys.stderr)
return 2
ensure_parent(args.token)
flow = InstalledAppFlow.from_client_secrets_file(str(credentials_path), scopes=args.scopes)
if args.no_browser:
creds = flow.run_console()
else:
creds = flow.run_local_server(port=args.port)
token_path = pathlib.Path(args.token).expanduser().resolve()
token_path.write_text(creds.to_json())
print(f"Token saved to: {token_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="${HOME}/.config/google-calendar/venv"
if [[ -x "${VENV_DIR}/bin/python" ]]; then
exec "${VENV_DIR}/bin/python" "${SCRIPT_DIR}/gcal_auth.py" "$@"
else
exec python3 "${SCRIPT_DIR}/gcal_auth.py" "$@"
fi
google-auth>=2.0.0
google-auth-oauthlib>=1.0.0
requests>=2.0.0
Related skills
How it compares
Pick this over generic CRM skills when the integration target is specifically Google Calendar scheduling and availability rather than email or contacts alone.
FAQ
What can the google-calendar skill automate?
google-calendar automates Google Calendar scheduling, availability checks, and event creation from apps or agents. Developers use it to query free-busy slots and persist confirmed meetings through the Calendar API instead of manual calendar UI steps.
When should developers use google-calendar?
Developers should use google-calendar when building booking assistants, meeting bots, or backend workflows that propose times, detect conflicts, and write events to Google Calendar with OAuth-authenticated API calls.