> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Google Sheets

> Mount Google Sheets spreadsheets as JSON-backed files that Python agents can read through Mirage shell commands.

The Google Sheets resource exposes Sheets spreadsheets as a virtual
filesystem mounted at some prefix such as `/gsheets/`.

For Google OAuth setup, see [Google Workspace Setup](/python/setup/google).

## Config

```python theme={null}
import os

from mirage import MountMode, Workspace
from mirage.resource.gsheets import GSheetsConfig, GSheetsResource

config = GSheetsConfig(
    client_id=os.environ["GOOGLE_CLIENT_ID"],
    client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
    refresh_token=os.environ["GOOGLE_REFRESH_TOKEN"],
)
resource = GSheetsResource(config=config)
ws = Workspace({"/gsheets": resource}, mode=MountMode.READ)
```

## Filesystem Layout

```text theme={null}
/gsheets/
  owned/
    <YYYY-MM-DD>_<sanitized-title>__<spreadsheet-id>.gsheet.json
    ...
  shared/
    <YYYY-MM-DD>_<sanitized-title>__<spreadsheet-id>.gsheet.json
    ...
```

Example:

```text theme={null}
/gsheets/
  owned/
    2026-04-04_Roadmap__1AbCdEf.gsheet.json
    2026-04-10_Expenses__2BcDeFg.gsheet.json
  shared/
    2026-04-03_Budget__9XyZ.gsheet.json
```

Spreadsheets are split into `owned` (spreadsheets you created) and
`shared` (spreadsheets shared with you). The filename shape is:

```text theme={null}
<YYYY-MM-DD>_<sanitized-title>__<spreadsheet-id>.gsheet.json
```

If the modified date is unavailable, the date prefix is omitted.
Reading a spreadsheet file returns the full Google Sheets API JSON
for that spreadsheet, including sheet metadata.

### Shared Drives

The listing covers every corpus the account can reach, so a spreadsheet that
lives in a Shared Drive appears here too. A Shared Drive spreadsheet has no
owner (the drive owns it), so it lands under `shared`, which is what that
directory means. This matches the `gdrive` mount, where Shared Drives are
top-level directories: without it the same account would see a spreadsheet
under one Google mount and not the other, with no error explaining the
difference.

Drive answers an all-corpora search best-effort. When it reports that it
skipped a corpus, the short listing is still returned but is not cached as
the directory, so the next `ls` re-lists instead of serving the gap until
the cache expires.

## Cache

The Google Sheets resource uses `IndexCacheStore`. Index entries
store spreadsheet IDs and metadata. There is no separate content
cache -- file content caching is handled by the workspace `IOResult`
mechanism.

## Example

```python theme={null}
import asyncio
import os

from dotenv import load_dotenv

from mirage import MountMode, Workspace
from mirage.commands.cli.builtin.gws import GWS
from mirage.resource.gsheets import GSheetsConfig, GSheetsResource

load_dotenv(".env.development")

config = GSheetsConfig(
    client_id=os.environ["GOOGLE_CLIENT_ID"],
    client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
    refresh_token=os.environ["GOOGLE_REFRESH_TOKEN"],
)
resource = GSheetsResource(config=config)


async def main():
    ws = Workspace({"/gsheets": resource}, mode=MountMode.READ)
    ws.register_cli("gws", GWS, config.model_dump())

    # List structure
    r = await ws.execute("ls /gsheets/")
    print(await r.stdout_str())

    # List owned spreadsheets
    r = await ws.execute("ls /gsheets/owned/")
    print(await r.stdout_str())

    # Read a spreadsheet
    r = await ws.execute(
        "cat /gsheets/owned/2026-04-04_Roadmap__1AbCdEf.gsheet.json")
    print(await r.stdout_str())

    # Extract title with jq
    r = await ws.execute(
        'jq ".properties.title"'
        " /gsheets/owned/2026-04-04_Roadmap__1AbCdEf.gsheet.json")
    print(await r.stdout_str())

    # Search across all spreadsheets
    r = await ws.execute('rg "revenue" /gsheets/owned/')
    print(await r.stdout_str())

    # Tree view
    r = await ws.execute("tree -L 1 /gsheets/")
    print(await r.stdout_str())

    # Read cell values
    r = await ws.execute(
        'gws sheets read --spreadsheet 1AbCdEf --range "Sheet1!A1:C3"')
    print(await r.stdout_str())

    # Append rows
    r = await ws.execute(
        "gws sheets append --spreadsheet 1AbCdEf --values Alice,30,NYC")
    print(await r.stdout_str())

    # Create a new spreadsheet
    r = await ws.execute(
        'gws sheets spreadsheets create'
        ' --json \'{"properties":{"title":"MIRAGE Sheet"}}\'')
    print(await r.stdout_str())


if __name__ == "__main__":
    asyncio.run(main())
```

See `examples/gsheets/gsheets.py` for the full working example.

## Shell Commands

Standard commands available on the mounted Google Sheets tree:

| Command                             | Notes                          |
| ----------------------------------- | ------------------------------ |
| `ls`                                | List owned/shared spreadsheets |
| `cat`                               | Read spreadsheet JSON          |
| `head` / `tail`                     | First/last N lines             |
| `grep` / `rg`                       | Pattern search                 |
| `jq`                                | Query JSON fields              |
| `wc`                                | Line/word/byte counts          |
| `stat`                              | File metadata                  |
| `find`                              | Recursive search               |
| `tree`                              | Directory tree view            |
| `basename` / `dirname` / `realpath` | Path utilities                 |
| `nl`                                | Number lines                   |

Acting on spreadsheets (read/write/append ranges, raw API calls)
goes through the [gws CLI](/python/cli/gws) when installed.
