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

# Google Drive

> Connect a personal or Google Workspace Drive account over OAuth and index the folders you pick into a dataset of its own.

Connect one **Google account** — personal or Workspace — to Cognee through an ordinary per-user OAuth consent, pick the folders and shared drives you want covered, and have their files become searchable memory in a dataset of that account's own.

<Info>
  This integration is for self-hosted Cognee deployments — you create and own the Google Cloud OAuth client, so it lives in your own Google Cloud project and talks only to your own Cognee backend.
</Info>

**Unreleased.** The connector landed after `v1.6.0` and is not tagged yet, so it needs a `dev` checkout until the next release. See the [changelog](/changelog).

## What It Does

* **One Google account, one Cognee user.** The credential is owned by whoever completed the consent, and the account's files are indexed into that user's memory. There is no Workspace-wide install: domain-wide delegation (one service account impersonating every member) is a different entry point the generic OAuth callback cannot express, and is deliberately not attempted.
* **Personal and Workspace are the same code path.** They differ only by the hosted-domain (`hd`) claim Google returns, which is recorded on the connection as `account_type: workspace` or `personal`.
* **Read-only.** The requested scopes are `openid email https://www.googleapis.com/auth/drive.readonly` — Cognee never writes to Drive.
* **Drive's grant stays its own.** The authorize URL deliberately omits `include_granted_scopes`, so Drive's grant is never folded together with the Gmail connector's — or a later Calendar one's. Google's revoke endpoint kills a whole grant, so sharing one would mean disconnecting one product silently disconnects the others.
* **Only supported file types are read.** Google Docs, Google Sheets, PDFs, and plain text, Markdown and CSV files become memory; everything else in Drive is skipped. See [What gets indexed](#what-gets-indexed).
* **Incremental after the first pass.** The first sync lists the selected folders in full and records a Drive Changes-API cursor; later syncs carry only what changed, including deletions.

## Prerequisites

* A Cognee backend on `dev` — see the version note above.
* The **`google-drive` extra** installed on that backend:

  ```bash theme={null}
  pip install "cognee[google-drive]"
  ```

  It pulls in `google-api-python-client`, `google-auth` and `google-auth-oauthlib`, which the sync path builds its Drive client with. OAuth itself works without them, so a deployment that skips this connects fine and then fails on its first sync with an `ImportError`.
* A **Google Cloud project** you can create an OAuth client in.
* A backend URL Google can redirect a browser to. Unlike webhook-driven connectors, Google only ever redirects the *user's browser* back, so `http://localhost:8000` is fine for local development — no tunnel is required.

## Setup

### 1. Create the Google Cloud OAuth client

In the [Google Cloud Console](https://console.cloud.google.com/), pick (or create) a project, then:

| Step                                                   | Value                                                                                                                        |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| **OAuth consent screen**                               | Configure it for the audience you need. While the app is in **Testing**, add every account that will connect as a test user. |
| **Scopes**                                             | `openid`, `email`, and `https://www.googleapis.com/auth/drive.readonly`.                                                     |
| **Credentials → Create credentials → OAuth client ID** | Application type **Web application**.                                                                                        |
| **Authorized redirect URI**                            | `https://<your-host>/api/v1/integrations/google_drive/callback` — must match `GOOGLE_DRIVE_REDIRECT_URI` verbatim.           |

Then collect the client's **Client ID** and **Client secret**.

<Warning>
  `drive.readonly` is a **restricted scope**. Google requires OAuth verification (including a security assessment) before an app using it can be published to the public. Test users work without verification, which is enough for an internal or single-team deployment.
</Warning>

### 2. Configure Cognee

Copy the client's values into your backend's `.env` (the same block is in `.env.template`):

| Variable                         | Value                                                                                                                                                                                                                                                                                                                                               |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GOOGLE_DRIVE_CLIENT_ID`         | From the OAuth client.                                                                                                                                                                                                                                                                                                                              |
| `GOOGLE_DRIVE_CLIENT_SECRET`     | From the OAuth client.                                                                                                                                                                                                                                                                                                                              |
| `GOOGLE_DRIVE_STATE_SECRET`      | Any long random string. It signs the OAuth `state` parameter — unlike Slack, GitHub and Linear this secret has exactly one use, because Google sends no signed webhooks on this path and there is no inbound HMAC to key.                                                                                                                           |
| `GOOGLE_DRIVE_REDIRECT_URI`      | The callback registered on the client, e.g. `http://localhost:8000/api/v1/integrations/google_drive/callback`. It is sent on both the authorize request and the token exchange, so it must match exactly.                                                                                                                                           |
| `GOOGLE_DRIVE_FRONTEND_BASE_URL` | Your frontend's origin, e.g. `http://localhost:3000` — where the callback sends the browser when the install finishes.                                                                                                                                                                                                                              |
| `INTEGRATION_CREDENTIALS_KEY`    | Not Drive-specific — the AES-256-GCM key (32 raw bytes, base64-encoded) the integrations framework encrypts stored credentials under. Drive stores a real refresh token, so this is what protects it at rest. Generate one with `openssl rand -base64 32`; `INTEGRATION_CREDENTIALS_KEYS` (a JSON keyring) is the preferred, rotation-capable form. |

The prefix is `GOOGLE_DRIVE_` rather than `GOOGLE_` on purpose: the latter is already taken by unrelated settings, and the Gmail connector — like a later Calendar one — is a separate provider with its own scopes and its own consent screen. They share the Cloud project, not the configuration block.

Every one of these is checked at **use time, not at boot**: a deployment with Drive unconfigured still starts normally, and the first call that needs a missing value fails naming it rather than degrading into a token exchange with empty credentials. `POST /authorize` turns that into a **503** (`google_drive integration is not configured on this server.`) instead of a 500.

Restart the backend after any `.env` change.

### 3. Connect

```bash theme={null}
# Mint the authorize URL (authenticated as the Cognee user who will own the data)
curl -X POST https://<your-host>/api/v1/integrations/google_drive/authorize \
  -H "Authorization: Bearer <your-cognee-token>"
# {"authorizeUrl":"https://accounts.google.com/o/oauth2/v2/auth?...&access_type=offline&prompt=consent"}
```

`/authorize` also sets a short-lived cookie that the callback checks alongside the signed state, so **the install has to be finished in the browser that started it, within 10 minutes** — which means a frontend calling `/authorize` with `credentials: "include"`, not a detached `curl`. The response headers of a `curl` call carry the cookie if you want to drive the flow by hand, but you then have to present it back to the callback yourself.

Approve the consent screen and Google redirects to `/api/v1/integrations/google_drive/callback`, which binds the account to your Cognee user and then sends the browser to `<GOOGLE_DRIVE_FRONTEND_BASE_URL>/integrations?google_drive=<outcome>`:

| Outcome                   | Meaning                                                                                                                    |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `connected`               | The account is bound. Nothing is indexed yet — see [Choosing what to index](#choosing-what-to-index).                      |
| `cancelled`               | The consent screen was cancelled, or Google rejected the request. Nothing was stored.                                      |
| `error_invalid_state`     | The state was missing, tampered with, expired, or the install nonce cookie did not match.                                  |
| `error_already_connected` | This Google account is already connected to a different Cognee user. Disconnect it there first.                            |
| `error_exchange_failed`   | The token exchange or identity lookup failed. The traceback is in the backend log; the browser only learns that it failed. |

The authorize URL carries `access_type=offline` **and** `prompt=consent`. Both are required for Google to issue a refresh token — without them an account that has consented before gets an access token only, and the connector goes dark about an hour after install with no way back short of reconnecting.

<Warning>
  **A split-domain deployment needs the cookie loosened.** That cookie is `SameSite=Lax`, which requires your API and your app to share a registrable domain. Where they do not, it has to be `SameSite=None; Secure` instead — otherwise every connect ends in `error_invalid_state`, logged as `google_drive callback arrived without a matching install nonce; refusing`.
</Warning>

## Choosing What to Index

**A fresh connection selects nothing.** It is stored with an empty folder selection, so no files are imported until you choose what to cover — connecting only grants authorization. Reconnecting an account that already has a selection keeps it; only a first-time connection starts empty.

List what is selectable:

```bash theme={null}
curl https://<your-host>/api/v1/integrations/google_drive/resources \
  -H "Authorization: Bearer <your-cognee-token>"
```

The list is `My Drive` (id `root`, which covers the account root including nested folders), every **shared drive** visible to the account, and every folder, paginated through in full. Then persist a selection:

```bash theme={null}
curl -X PUT https://<your-host>/api/v1/integrations/google_drive/resources \
  -H "Authorization: Bearer <your-cognee-token>" \
  -H "Content-Type: application/json" \
  -d '{"resourceIds": ["root"]}'
```

`/folders` is a readable alias for `/resources` on both verbs. The selection is **three-state**:

| `resourceIds` | Meaning                                                  |
| ------------- | -------------------------------------------------------- |
| `null`        | Every resource — `My Drive` plus every shared drive.     |
| `[]`          | Nothing. This is what a newly connected account holds.   |
| `["<id>", …]` | An allowlist of exactly those folders and shared drives. |

A selection that is neither `null` nor a list of non-empty strings fails the sync with `Drive selection must be a list of folder IDs or null`.

<Warning>
  **Narrowing a selection deletes data.** The next sync after you drop a folder from `resourceIds` retires what that folder put in the dataset — the rows go through an explicit replacement and Cognee's ordinary orphan cleanup purges them from the graph, vector and relational stores. Setting `resourceIds` to `[]` retires everything the account has indexed. That reconciliation is skipped whenever the sync recorded a failure, so a transient error never takes data with it, and re-selecting a folder re-indexes it from scratch.
</Warning>

## Syncing and Status

Connecting does **not** start a sync — `on_installed` is an explicit no-op, so the first pass happens when you ask for one:

```bash theme={null}
curl -X POST https://<your-host>/api/v1/integrations/google_drive/sync \
  -H "Authorization: Bearer <your-cognee-token>"
# {"accepted": true}
```

The sync runs **detached** — the response returns immediately, it does not carry the result. It fans out over the selected folders (or `root` plus every shared drive when the selection is `null`), and each one is ingested through the ordinary [`remember()`](/python-api/remember) path with `write_disposition="merge"` on the file id, so re-syncing updates rows instead of duplicating them. It runs with `self_improvement=False`: whole-graph [`improve()`](/python-api/improve) enrichment carries LLM cost far too heavy to fire on every Drive refresh, so it stays a human or scheduled decision.

**Only the first sync reads everything.** It lists the selected folders in full and, before doing so, captures a Drive **Changes API** cursor; every later sync asks Drive only what changed since that cursor and carries just those files. The cursor lives in the per-folder ingestion state, and it is advanced only when the run recorded no failures — a file whose content could not be extracted is therefore retried on the next sync rather than lost behind a cursor that moved past it. A sync that finds no changes still re-reads the folder's already-staged files, so if their processing failed, or the dataset was emptied locally, the next `/sync` restores them without anything changing in Drive.

Changes include **deletions**: a file deleted, trashed, or moved out of a selected folder is emitted as a tombstone and forgotten from memory on the next sync, graph and vector rows included. Drive's Changes feed is account-wide and carries no metadata on deleted files, so an incremental run may also emit harmless no-op deletes for files removed elsewhere in the account.

`{"accepted": true}` means the request was taken, not that a sync started: while one is already in flight for that account the call returns the same body and does nothing, so a manual refresh can never interleave two runs over one cursor. That guard lives in the API process, so a deployment running more than one worker needs its own job queue or distributed lease to get the same protection.

Files land in **one dataset per connected account**, named `google_drive_<email-slug>_<hash>` — the email with every run of characters outside `A-Z a-z 0-9 _` collapsed to `_`, trimmed of leading and trailing `_` and lowercased (an email that slugifies to nothing becomes `account`), plus the first 10 hex characters of the SHA-256 of the Google account id. The hash suffix is what keeps two accounts whose emails slugify identically (`a.b@x.com` and `a-b@x.com`) in separate datasets. One dataset per account rather than one shared per Workspace domain is deliberate: Cognee's permissions are dataset-scoped, so a shared dataset would let one colleague's questions answer from another's private files.

```python theme={null}
import cognee

results = await cognee.recall(
    "What did we agree in the Q3 planning doc?",
    datasets=["google_drive_jane_example_com_3f2a1b9c4d"],
)
```

Because this connector receives no webhooks and Cognee runs no durable scheduler, **nothing refreshes it on its own** — call `/sync` again (or wire it to your own scheduler) to pick up changes.

### What gets indexed

A selected folder is not read wholesale. Only file types the connector knows how to turn into text are ingested; everything else is skipped with a warning and counted under `skipped_unsupported_type`, which is the usual answer to "why isn't this file in memory?".

| Drive file                                | How it is read                                                                                                                        |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Google Docs                               | Exported as `text/plain`.                                                                                                             |
| Google Sheets                             | Exported as CSV — **the first sheet only**. Drive's export gives no more than that; multi-sheet extraction would need the Sheets API. |
| PDFs                                      | Text extracted with `pypdf`. A scanned PDF with no text layer yields nothing and is skipped as empty.                                 |
| `text/plain`, `text/markdown`, `text/csv` | Downloaded as-is.                                                                                                                     |
| Everything else                           | **Skipped** — Slides, Drawings, Forms, images, and Office formats such as `.docx` and `.xlsx` included.                               |

Two more files never make it in: anything larger than **25 MB** (per Drive's reported size, configurable with `GOOGLE_DRIVE_MAX_FILE_SIZE_MB`) is skipped before it is read, and a file whose extracted text is empty is skipped as well. A file that fails to parse — a corrupt PDF, an export error — is logged and skipped rather than failing the folder, but it does count as a failure and marks the sync [degraded](#reading-the-connection-state).

### Reading the connection state

```bash theme={null}
curl https://<your-host>/api/v1/integrations/google_drive/connection \
  -H "Authorization: Bearer <your-cognee-token>"
```

| Field                                | Meaning                                                                                                                                                                                                                                                                                                     |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `connected`                          | Whether an active credential exists for the calling user.                                                                                                                                                                                                                                                   |
| `accountLabel` / `providerAccountId` | The connected account's email and its stable Google subject id.                                                                                                                                                                                                                                             |
| `connectedAt`                        | When the credential was first stored.                                                                                                                                                                                                                                                                       |
| `datasetId` / `storedItems`          | The account's dataset and how much it currently holds.                                                                                                                                                                                                                                                      |
| `syncStatus`                         | `ok`, `degraded`, `syncing`, or absent when the account has never synced.                                                                                                                                                                                                                                   |
| `lastSyncedAt`                       | When the last sync finished.                                                                                                                                                                                                                                                                                |
| `syncCounts`                         | Per-run counters from the last sync: `scanned`, `skipped`, `failed` and `deleted`, alongside one breakdown key per reason — `skipped_too_large`, `skipped_unsupported_type`, `skipped_empty_content`, `failed_content_extraction`, `failed_ingestion`, `failed_rate_limit`. Counts only; never a file name. |

`GET /api/v1/integrations/status` carries the same per-provider fields for every registered provider in one call — that is what powers an integrations page.

<Warning>
  **`connected: true` does not mean healthy.** A connection whose last sync failed or came back partial still reads as connected, so check `syncStatus` too — otherwise a half-empty memory looks exactly like a working one.
</Warning>

## Token Lifecycle

Google access tokens last about an hour, so the adapter rotates them: an access token is refreshed in place **5 minutes before it expires**, which keeps a long sync from dying halfway through its own file list. Google returns no new refresh token on a rotation, so the original is carried forward until the account revokes it.

Two failure modes are worth knowing:

* **Google rejects the refresh with `invalid_grant`.** The account revoked Cognee's access on Google's side ([myaccount.google.com/permissions](https://myaccount.google.com/permissions)), or the refresh token expired. Nothing else tells Cognee that — there are no webhooks on this path — so the local credential is revoked too, and the connection stops reading as connected instead of showing healthy forever while every sync fails. Reconnect to fix it.
* **The connection was minted without a refresh token.** Google withholds it when a prior consent is reused, which is exactly what `prompt=consent` exists to prevent. It is logged at connect time (`connected without a refresh token`) and can only be fixed by reconnecting.

## Disconnect

```bash theme={null}
# Revoke the connection, keep the indexed data
curl -X DELETE "https://<your-host>/api/v1/integrations/google_drive/connection" \
  -H "Authorization: Bearer <your-cognee-token>"

# Revoke it and forget the account's dataset as well
curl -X DELETE "https://<your-host>/api/v1/integrations/google_drive/connection?delete_data=true" \
  -H "Authorization: Bearer <your-cognee-token>"
```

Disconnecting marks the stored credential revoked — that local revoke is the actual access cut-off — and **best-effort** asks Google to kill the token on its own side. Best-effort by design: a network blip must never block a disconnect. Because Drive's grant was kept separate from any other Google product's, that remote revoke cannot take a sibling Google connector down with it.

Without `delete_data=true` the `google_drive_…` dataset stays exactly as it is; use [`forget()`](/python-api/forget) later if you change your mind. With it, the dataset is forgotten after the revoke, and a deletion that fails answers **502** (`google_drive disconnected, but its data could not be deleted`) — the disconnect itself has already happened at that point.

## Limitations

* **Only some file types are indexed, and large files are skipped.** Google Docs, Sheets (first sheet), PDFs and plain text/Markdown/CSV are read; everything else is skipped, as is any file over 25 MB. See [What gets indexed](#what-gets-indexed).
* **No automatic refresh.** No webhooks, no scheduler — `/sync` is the only thing that updates memory.
* **No Workspace-wide install.** One consent per user; domain-wide delegation is not supported.
* **A Google account belongs to exactly one Cognee user.** Connecting an account that another user already holds is refused (`error_already_connected`) rather than silently reassigned — the original owner has to disconnect first.
* **`drive.readonly` is a restricted scope** — public distribution needs Google's OAuth verification.

## Troubleshooting

<AccordionGroup>
  <Accordion title="`POST /authorize` returns 503 `google_drive integration is not configured on this server.`">
    One of `GOOGLE_DRIVE_CLIENT_ID`, `GOOGLE_DRIVE_REDIRECT_URI` or `GOOGLE_DRIVE_STATE_SECRET` is unset. The backend log names the exact variable. A missing `GOOGLE_DRIVE_FRONTEND_BASE_URL` surfaces later, as the same 503 raised from the callback instead — the browser is already on your domain by then, so there is nowhere valid left to redirect it.
  </Accordion>

  <Accordion title="Google shows `redirect_uri_mismatch`">
    `GOOGLE_DRIVE_REDIRECT_URI` and the Authorized redirect URI on the OAuth client differ. They must match character for character, scheme and trailing slash included.
  </Accordion>

  <Accordion title="Redirected to `?google_drive=error_invalid_state`">
    The install did not start from a freshly minted `/authorize` URL, more than 10 minutes passed before approving it, `GOOGLE_DRIVE_STATE_SECRET` changed in between, or the install finished in a different browser than the one that called `/authorize`. On a split-domain deployment, check the cookie's `SameSite` note in [step 3](#3-connect). Mint a new URL and retry.
  </Accordion>

  <Accordion title="Redirected to `?google_drive=error_exchange_failed`">
    The callback never 500s — it catches everything and redirects, and the traceback is in the backend log. Usual causes: mismatched client id/secret, a redirect URI that does not match the registered one, a missing `INTEGRATION_CREDENTIALS_KEY`, or a userinfo response with no subject (`Google userinfo response carries no subject`).
  </Accordion>

  <Accordion title="Redirected to `?google_drive=error_already_connected`">
    This Google account is bound to another Cognee user. Disconnect it there first.
  </Accordion>

  <Accordion title="Connected, but the dataset is empty">
    Expected until you make a selection *and* call `/sync` — connecting alone stores an empty selection and starts nothing. Check `GET /resources` for what is selected and `GET /connection` for `syncStatus` and `lastSyncedAt`. A `/sync` that answered `{"accepted": true}` while `syncStatus` was already `syncing` did nothing — one run per account at a time.
  </Accordion>

  <Accordion title="`syncStatus: &#x22;degraded&#x22;`">
    Something failed; everything else was indexed. Read `syncCounts` to tell which: `failed_content_extraction` counts individual unreadable files, logged as `Skipping Drive file '<name>' (<id>): content extraction failed`, while a whole folder that failed logs `Google Drive sync failed for account <id> folder <id>`. A single bad file is enough to mark the run degraded. Re-running `/sync` retries what failed — the changes cursor is not advanced past a failed run.
  </Accordion>

  <Accordion title="`syncStatus: &#x22;syncing&#x22;` that never settles">
    The status is derived from an in-flight run, so it clears when the detached sync ends. If it persists across a restart, look for the sync's traceback in the backend log.
  </Accordion>

  <Accordion title="Everything 401s about an hour after connecting">
    The connection has no refresh token (see [Token Lifecycle](#token-lifecycle)). Reconnect — `prompt=consent` forces Google to issue one.
  </Accordion>

  <Accordion title="The connection silently stops being connected">
    An `invalid_grant` on refresh revoked it locally, which means access was revoked at Google. The log line is `Google Drive access for account <id> was revoked at the provider`.
  </Accordion>

  <Accordion title="A document is missing entirely">
    Either its type is not supported or it is over the size limit — see [What gets indexed](#what-gets-indexed). The backend log says which: `Skipping unsupported Drive file '<name>' (<id>): mime type '<type>'.` or `Skipping Drive file '<name>' (<id>): size exceeds max_file_size_mb=25.`
  </Accordion>

  <Accordion title="`ImportError` on the first sync">
    The `google-drive` extra is not installed on the backend. `pip install "cognee[google-drive]"` and restart — OAuth works without it, so the connection itself looks healthy.
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Linear" icon="square-kanban" href="/integrations/linear-integration">
    The other first-party OAuth connector, sharing the same credential store, state signing and connect flow.
  </Card>

  <Card title="GitHub" icon="github" href="/integrations/github-integration">
    Install a GitHub App into an org and index its repositories into the code graph.
  </Card>
</CardGroup>
