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.
v1.6.1. The connector landed after v1.6.0 and ships in v1.6.1 — see the 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 asaccount_type: workspaceorpersonal. - 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 or Google Calendar connector’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, Sheets and Slides, PDFs, text files (any
text/*type, plus JSON and XML), and Office and document files such as.docx,.xlsxand.pptxbecome memory; images, audio, video and a few other kinds are skipped. See 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
v1.6.1or later — see the version note above. -
The
google-driveextra installed on that backend:It pulls ingoogle-api-python-client,google-authandgoogle-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 anImportError. - 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:8000is fine for local development — no tunnel is required.
Setup
1. Create the Google Cloud OAuth client
In the Google Cloud Console, pick (or create) a project, then:
Then collect the client’s Client ID and Client secret.
2. Configure Cognee
Copy the client’s values into your backend’s.env (the same block is in .env.template):
The prefix is
GOOGLE_DRIVE_ rather than GOOGLE_ on purpose: the latter is already taken by unrelated settings, and the Gmail and Google Calendar connectors are each 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
/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>:
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.
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: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:
/folders is a readable alias for /resources on both verbs. The selection is three-state:
A blank or whitespace-only id is refused with 422 (
Resource ids must not be blank or whitespace only); duplicate ids are stored once. A selection stored before that check 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.
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:
root plus every shared drive when the selection is null), and each one is ingested through the ordinary 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() 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 failure that a rerun could cure — 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.
/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 and counted underskipped_unsupported_type, which is the usual answer to “why isn’t this file in memory?”. Files go through Cognee’s loader engine, the same loaders add() uses.
There is no file size limit by default. Downloads are streamed to a temporary file, so memory depends on the extracted text, not on the file size.
GOOGLE_DRIVE_MAX_FILE_SIZE_MB sets an opt-in limit, checked against the size Drive reports; Google Docs, Sheets and Slides, for which Drive reports no size, are never left out by it. A changed limit applies to a file the next time it is fetched — on its next change in Drive, or on a full scan — not to unedited files straight away.
A file whose extracted text is empty is skipped too. A file that fails — a download error, a Google Doc, Sheet or Slides file over Google’s export limit (about 10 MB, counted as failed_export_too_large) — is logged and counted rather than failing the folder, but it does count as a failure and marks the sync degraded.
A file that stops yielding content is forgotten. A file already in memory that later yields nothing to keep — it grew past your size limit, was edited to blank, changed to an unsupported type, became a password-protected or corrupt PDF, or went over Google’s export limit — is removed, like a file that left the folder. A file that was never ingested is only counted. The one exception is a missing loader extra: the file keeps its old content, because that is a problem with the environment, not the file.
Reading the connection state
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.
Reading sync progress
syncStatus: syncing only says that a run is in flight. To see how far it has got, read the connection’s sync progress:
/api/v1/integrations/{provider}/sync/progress) and only shows the calling user’s own active connection. It answers 404 Unknown integration for a provider that isn’t registered, and 404 Active connection not found when you have no active connection for it, when account_id isn’t one of yours, or when the connection went inactive while it was being read. Reconnecting the same account keeps its run history. If the connection changes owner, the previous owner’s runs are no longer shown.
Drive, Gmail, Google Calendar, Linear and Notion syncs report
checking, then processing. A finalizing phase follows only when the sync removes data from deselected folders, labels, calendars or scopes, and it counts those in scopes. When a connector ingests one source per selected scope, as Drive does per folder, processing counts those scopes (unit scopes). Gmail and Calendar ingest their whole selection in one pass, so their processing phase reports only counts, with no total. GitHub and Slack history report their own phases.
Progress is best effort. A failed progress write is logged and never changes the outcome of the sync. Updates within the same phase and state are written at most once a second, and a change of phase or state is written immediately. Responses carry only phases, counts and reason categories, never document text, tokens or provider error messages. A runnable client is in examples/guides/integration_sync_progress.py. It makes one read-only request, using COGNEE_API_TOKEN and an optional --account-id.
unknownActivityCount is not a live-job count, and it is not a failure count. It counts runs that have no finish recorded and that this API process isn’t executing itself. Such a run may still be running in another worker process, or its worker may have exited without finishing it. Behind several workers, a request can land on a process that doesn’t own the run. In that case the run shows up here and in latest with executionActive: null, not in activeRuns.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), 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=consentexists to prevent. It is logged at connect time (connected without a refresh token) and can only be fixed by reconnecting.
Disconnect
delete_data=true the google_drive_… dataset stays exactly as it is; use 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. Google Docs, Sheets (first sheet) and Slides, PDFs, text files and Office/document files are read (Office files,
.rtfand.epubneed thedoclingordocsextra); images, audio, video and a few other kinds are skipped. There is no size limit unless you setGOOGLE_DRIVE_MAX_FILE_SIZE_MB. See What gets indexed. - No automatic refresh. No webhooks, no scheduler —
/syncis 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.readonlyis a restricted scope — public distribution needs Google’s OAuth verification.
Troubleshooting
Google shows redirect_uri_mismatch
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.Redirected to ?google_drive=error_invalid_state
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. Mint a new URL and retry.Redirected to ?google_drive=error_exchange_failed
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).Redirected to ?google_drive=error_already_connected
Redirected to ?google_drive=error_already_connected
This Google account is bound to another Cognee user. Disconnect it there first.
Connected, but the dataset is empty
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.syncStatus: "degraded"
syncStatus: "degraded"
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> scope <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 transient failure, and the sync state records each fetched file’s modifiedTime, so the next run downloads only the failed and the changed files, not the whole folder. failed_unreadable, failed_loader_unavailable and failed_export_too_large do not hold the cursor, because a rerun cannot cure them: for failed_loader_unavailable install the extra; for an unreadable PDF or an export over Google’s limit, fix the file. Either way, edit the file in Drive so the next sync reads it again.failed_rate_limit (Drive kept answering 429 after its retries) and failed_auth (the token was refused) stop the run cleanly instead of failing it outright: the files already fetched are kept, nothing is forgotten on the strength of the incomplete listing, the cursor stays, and the next /sync goes on from there. A token refused before the run fetched anything fails that folder instead, still counted as failed_auth. A failed_auth that persists means the connection needs reconnecting.syncStatus: "syncing" that never settles
syncStatus: "syncing" 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.
Everything 401s about an hour after connecting
Everything 401s about an hour after connecting
The connection has no refresh token (see Token Lifecycle). Reconnect —
prompt=consent forces Google to issue one.The connection silently stops being connected
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.A document is missing entirely
A document is missing entirely
Either its type is not supported, it is over a size limit you set, or it needs a loader extra that is not installed — see What gets indexed. Check
syncCounts: skipped_unsupported_type, skipped_too_large or failed_loader_unavailable (install cognee[docling] on the backend). A file over your limit is named in the backend log as Skipping Drive file '<name>' (<id>): size exceeds max_file_size_mb=<limit>.; unsupported and empty files are only counted.ImportError on the first sync
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.Related
Linear
The other first-party OAuth connector, sharing the same credential store, state signing and connect flow.
GitHub
Install a GitHub App into an org and index its repositories into the code graph.
Google Calendar
The sibling Google OAuth integration for calendars, with the same connect flow and token handling and an explicit-list-only calendar selection.
Google Drive Ingestion
Sync a single folder from your own script with
google_drive_source(), using a service account or a desktop OAuth client instead of this per-user consent flow.