Back to skills
SkillHub ClubShip Full StackFull StackBackend

google-calendar

Interact with Google Calendar via the Google Calendar API – list upcoming events, create new events, update or delete them. Use this skill when you need programmatic access to your calendar from OpenClaw.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,129
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
A88.4

Install command

npx @skill-hub/cli install openclaw-skills-google-calendar-0-1-0

Repository

openclaw/skills

Skill path: skills/amanbhandula/moltarxiv/google-calendar-0.1.0

Interact with Google Calendar via the Google Calendar API – list upcoming events, create new events, update or delete them. Use this skill when you need programmatic access to your calendar from OpenClaw.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Backend.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install google-calendar into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding google-calendar to shared team environments
  • Use google-calendar for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: google-calendar
description: Interact with Google Calendar via the Google Calendar API – list upcoming events, create new events, update or delete them. Use this skill when you need programmatic access to your calendar from OpenClaw.
---

# Google Calendar Skill

## Overview
This skill provides a thin wrapper around the Google Calendar REST API. It lets you:
- **list** upcoming events (optionally filtered by time range or query)
- **add** a new event with title, start/end time, description, location, and attendees
- **update** an existing event by its ID
- **delete** an event by its ID

The skill is implemented in Python (`scripts/google_calendar.py`). It expects the following environment variables to be set (you can store them securely with `openclaw secret set`):
```
GOOGLE_CLIENT_ID=…
GOOGLE_CLIENT_SECRET=…
GOOGLE_REFRESH_TOKEN=…   # obtained after OAuth consent
GOOGLE_CALENDAR_ID=primary   # or the ID of a specific calendar
```
The first time you run the skill you may need to perform an OAuth flow to obtain a refresh token – see the **Setup** section below.

## Commands
```
google-calendar list [--from <ISO> --to <ISO> --max <N>]
google-calendar add   --title <title> [--start <ISO> --end <ISO>]
                     [--desc <description> --location <loc> --attendees <email1,email2>]
google-calendar update --event-id <id> [--title <title> ... other fields]
google-calendar delete --event-id <id>
```
All commands return a JSON payload printed to stdout. Errors are printed to stderr and cause a non‑zero exit code.

## Setup
1. **Create a Google Cloud project** and enable the *Google Calendar API*.
2. **Create OAuth credentials** (type *Desktop app*). Note the `client_id` and `client_secret`.
3. Run the helper script to obtain a refresh token:
   ```bash
   GOOGLE_CLIENT_ID=… GOOGLE_CLIENT_SECRET=… python3 -m google_calendar.auth
   ```
   It will open a browser (or print a URL you can open elsewhere) and ask you to grant access. After you approve, copy the `refresh_token` it prints.
4. Store the credentials securely:
   ```bash
   openclaw secret set GOOGLE_CLIENT_ID <value>
   openclaw secret set GOOGLE_CLIENT_SECRET <value>
   openclaw secret set GOOGLE_REFRESH_TOKEN <value>
   openclaw secret set GOOGLE_CALENDAR_ID primary   # optional
   ```
5. Install the required Python packages (once):
   ```bash
   pip install --user google-auth google-auth-oauthlib google-api-python-client
   ```

## How it works (brief)
The script loads the credentials from the environment, refreshes the access token using the refresh token, builds a `service = build('calendar', 'v3', credentials=creds)`, and then calls the appropriate API method.

## References
- Google Calendar API reference: https://developers.google.com/calendar/api/v3/reference
- OAuth 2.0 for installed apps: https://developers.google.com/identity/protocols/oauth2/native-app

---

**Note:** This skill does not require a GUI; it works entirely via HTTP calls, so it is suitable for headless servers.


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/google_calendar.py

```python
#!/usr/bin/env python3
import os, sys, json, urllib.request, urllib.parse, argparse

BASE_URL = 'https://www.googleapis.com/calendar/v3'

def get_access_token():
    token = os.getenv('GOOGLE_ACCESS_TOKEN')
    if not token:
        sys.stderr.write('Error: GOOGLE_ACCESS_TOKEN env var not set\n')
        sys.exit(1)
    return token

def get_calendar_ids():
    # Support multiple IDs via env var (comma‑separated) or single ID fallback
    ids = os.getenv('GOOGLE_CALENDAR_IDS')
    if ids:
        return [c.strip() for c in ids.split(',') if c.strip()]
    # fallback to single ID for backward compatibility
    single = os.getenv('GOOGLE_CALENDAR_ID')
    return [single] if single else []

def request(method, url, data=None):
    req = urllib.request.Request(url, data=data, method=method)
    req.add_header('Authorization', f'Bearer {get_access_token()}')
    req.add_header('Accept', 'application/json')
    if data:
        req.add_header('Content-Type', 'application/json')
    try:
        with urllib.request.urlopen(req) as resp:
            return json.load(resp)
    except urllib.error.HTTPError as e:
        sys.stderr.write(f'HTTP error {e.code}: {e.read().decode()}\n')
        sys.exit(1)

def list_events(args):
    parser = argparse.ArgumentParser()
    parser.add_argument('--from', dest='time_min', help='ISO start time')
    parser.add_argument('--to', dest='time_max', help='ISO end time')
    parser.add_argument('--max', dest='max_results', type=int, default=10)
    parsed = parser.parse_args(args)
    results = {}
    for cal_id in get_calendar_ids():
        params = {
            'maxResults': parsed.max_results,
            'singleEvents': 'true',
            'orderBy': 'startTime',
        }
        if parsed.time_min:
            params['timeMin'] = parsed.time_min
        if parsed.time_max:
            params['timeMax'] = parsed.time_max
        url = f"{BASE_URL}/calendars/{urllib.parse.quote(cal_id)}/events?{urllib.parse.urlencode(params)}"
        resp = request('GET', url)
        results[cal_id] = resp.get('items', [])
    # Output a combined JSON mapping calendar ID -> list of events
    print(json.dumps(results, indent=2))

# The other commands (add, update, delete) remain single‑calendar for simplicity
def add_event(args):
    parser = argparse.ArgumentParser()
    parser.add_argument('--title', required=True)
    parser.add_argument('--start', required=True, help='ISO datetime')
    parser.add_argument('--end', required=True, help='ISO datetime')
    parser.add_argument('--desc', default='')
    parser.add_argument('--location', default='')
    parser.add_argument('--attendees', default='')
    parsed = parser.parse_args(args)
    cal_id = get_calendar_ids()[0] if get_calendar_ids() else None
    if not cal_id:
        sys.stderr.write('No calendar ID configured\n')
        sys.exit(1)
    event = {
        'summary': parsed.title,
        'start': {'dateTime': parsed.start},
        'end': {'dateTime': parsed.end},
        'description': parsed.desc,
        'location': parsed.location,
    }
    if parsed.attendees:
        event['attendees'] = [{'email': e.strip()} for e in parsed.attendees.split(',') if e.strip()]
    url = f"{BASE_URL}/calendars/{urllib.parse.quote(cal_id)}/events"
    data = json.dumps(event).encode()
    resp = request('POST', url, data=data)
    print(json.dumps(resp, indent=2))

def update_event(args):
    parser = argparse.ArgumentParser()
    parser.add_argument('--event-id', required=True)
    parser.add_argument('--title')
    parser.add_argument('--start')
    parser.add_argument('--end')
    parser.add_argument('--desc')
    parser.add_argument('--location')
    parser.add_argument('--attendees')
    parsed = parser.parse_args(args)
    cal_id = get_calendar_ids()[0] if get_calendar_ids() else None
    if not cal_id:
        sys.stderr.write('No calendar ID configured\n')
        sys.exit(1)
    get_url = f"{BASE_URL}/calendars/{urllib.parse.quote(cal_id)}/events/{urllib.parse.quote(parsed.event_id)}"
    event = request('GET', get_url)
    if parsed.title:
        event['summary'] = parsed.title
    if parsed.start:
        event.setdefault('start', {})['dateTime'] = parsed.start
    if parsed.end:
        event.setdefault('end', {})['dateTime'] = parsed.end
    if parsed.desc is not None:
        event['description'] = parsed.desc
    if parsed.location is not None:
        event['location'] = parsed.location
    if parsed.attendees:
        event['attendees'] = [{'email': e.strip()} for e in parsed.attendees.split(',') if e.strip()]
    resp = request('PUT', get_url, data=json.dumps(event).encode())
    print(json.dumps(resp, indent=2))

def delete_event(args):
    parser = argparse.ArgumentParser()
    parser.add_argument('--event-id', required=True)
    parsed = parser.parse_args(args)
    cal_id = get_calendar_ids()[0] if get_calendar_ids() else None
    if not cal_id:
        sys.stderr.write('No calendar ID configured\n')
        sys.exit(1)
    url = f"{BASE_URL}/calendars/{urllib.parse.quote(cal_id)}/events/{urllib.parse.quote(parsed.event_id)}"
    resp = request('DELETE', url)
    print(json.dumps(resp, indent=2))

if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.stderr.write('Usage: google_calendar.py <command> [options]\n')
        sys.exit(1)
    cmd = sys.argv[1]
    args = sys.argv[2:]
    if cmd == 'list':
        list_events(args)
    elif cmd == 'add':
        add_event(args)
    elif cmd == 'update':
        update_event(args)
    elif cmd == 'delete':
        delete_event(args)
    else:
        sys.stderr.write(f'Unknown command: {cmd}\n')
        sys.exit(1)

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### scripts/refresh_token.py

```python
#!/usr/bin/env python3
import os, sys, json, urllib.request, urllib.parse

def refresh():
    client_id = os.getenv('GOOGLE_CLIENT_ID')
    client_secret = os.getenv('GOOGLE_CLIENT_SECRET')
    refresh_token = os.getenv('GOOGLE_REFRESH_TOKEN')
    if not all([client_id, client_secret, refresh_token]):
        sys.stderr.write('Missing one of GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN\n')
        sys.exit(1)
    data = urllib.parse.urlencode({
        'client_id': client_id,
        'client_secret': client_secret,
        'refresh_token': refresh_token,
        'grant_type': 'refresh_token',
    }).encode()
    req = urllib.request.Request('https://oauth2.googleapis.com/token', data=data, method='POST')
    req.add_header('Content-Type', 'application/x-www-form-urlencoded')
    try:
        with urllib.request.urlopen(req) as resp:
            resp_data = json.load(resp)
    except urllib.error.HTTPError as e:
        sys.stderr.write(f'HTTP error {e.code}: {e.read().decode()}\n')
        sys.exit(1)
    access_token = resp_data.get('access_token')
    if not access_token:
        sys.stderr.write('No access_token in response\n')
        sys.exit(1)
    # Update the secrets.env file
    env_path = os.path.expanduser('~/.config/google-calendar/secrets.env')
    # Read existing lines, replace or add GOOGLE_ACCESS_TOKEN
    lines = []
    if os.path.exists(env_path):
        with open(env_path, 'r') as f:
            lines = f.readlines()
    new_lines = []
    token_set = False
    for line in lines:
        if line.startswith('export GOOGLE_ACCESS_TOKEN='):
            new_lines.append(f'export GOOGLE_ACCESS_TOKEN={access_token}\n')
            token_set = True
        else:
            new_lines.append(line)
    if not token_set:
        new_lines.append(f'export GOOGLE_ACCESS_TOKEN={access_token}\n')
    with open(env_path, 'w') as f:
        f.writelines(new_lines)
    print(json.dumps(resp_data, indent=2))

if __name__ == '__main__':
    refresh()

```