# Migrate gdocs to Confluence

> Google Docs is two APIs — Drive knows which documents exist, the Docs API knows what is inside them — and most of the work is rebuilding structure that Google stores only as formatting.

Source: https://migratly.dev/migrate/gdocs-to-confluence/

Migratly migrates gdocs to Confluence by reading both APIs live, deriving the mapping between them, and
presenting it for approval before anything is written. This pair covers **8 entity types**
and **42 field mappings**: 10 carry across intact, 32 are flagged as lossy,
partial, dropped or needing another route, and 3 need a decision from you. The gdocs side is
read-only for the whole run.

## How the structure changes

- A selected Drive folder becomes a Confluence space. My Drive's top level is offered as one too, because most personal documents sit loose in it rather than in a folder.
- Each Google Doc becomes a page in that space. Only native Google Docs are listed — uploaded .docx and PDF files have no Docs-API body and would arrive empty.
- The document body is converted to Confluence storage-format XHTML by the connector spec, not field by field.
- Structure that Google keeps only as formatting is recovered: list nesting from a per-paragraph depth number, code blocks from runs of monospaced paragraphs, block quotes from indentation, checklists from the bullet glyph.
- Drive comments become footer comments. Google's anchors are revision-scoped, so a comment arrives unanchored with the text it pointed at quoted into it.
- Links from one document to another are queued and resolved in a second pass, once both pages exist in the target.

## Entity map

| gdocs | Confluence | Wave | Fields |
|---|---|---|---|
| Folder | Space | 0 | 5 |
| User | User | 0 | 3 |
| Document | Page | 1 | 11 |
| DocumentBody | InlineXHTML | 1 | 11 |
| Comment | FooterComment | 2 | 7 |
| DocumentLink | InlinePageLink | 3 | 3 |
| Permission | — | 3 | 1 |
| Revision | — | 3 | 1 |

## Field coverage

### Folder → Space

| gdocs field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `name` | `name` | Full |  |
| `id` | `key` | Workaround | Confluence space keys must be uppercase alphanumeric and unique site-wide. Derived from the Drive folder id: strip non-alphanumerics, uppercase, take the first 8 characters, prefix GD. |
| `webViewLink` | `description` | Partial | Drive folders have no description field. The space description is generated provenance text pointing back at the source folder. |
| `parents` | no equivalent | Dropped | Every selected folder becomes a TOP-LEVEL space, so nesting between two selected folders is lost. Confluence spaces cannot contain spaces. Sub-folder contents migrate only when the sub-folder is itself selected — see migration_guide.md. |
| `owners` | no equivalent | Dropped | Space ownership follows the API token, not the source folder owner. Confluence cannot assign a space to another user at create time. |

### User → User

| gdocs field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `emailAddress` | `email` | Full | Email is the join key AND the source-side user id — Google has no opaque user identifier to reconcile, unlike Notion. Build the email → Confluence accountId table before any page is written. |
| `displayName` | `displayName` | Full |  |
| `permissionId` | no equivalent | Dropped | Drive's opaque per-user id appears only inside permissions objects and has no meaning outside Google. |

### Document → Page

| gdocs field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `name` | `title` | Full |  |
| `id` | `_page_property:Google Doc ID` | Full | Keep it: this is what makes a re-run idempotent and what a later delta sync joins on. |
| `webViewLink` | `_page_property:Source URL` | Full |  |
| `_body` | `body.value` | Partial | The Docs structural model carries formatting Confluence storage XHTML has no node for, and structure Google expresses only as formatting. See gdocs/weaknesses.md for the full ledger. |
| `owners.0.emailAddress` | `_page_property:Original Author` | Workaround | Confluence records the API token's owner as the author of every created page and cannot impersonate. The original owner survives only as a page property. |
| `lastModifyingUser.emailAddress` | `_page_property:Last Edited By` | Workaround | Same impersonation limit as author. |
| `createdTime` | `_page_property:Original Created` | Workaround | Confluence sets createdAt server-side. The Google timestamp is preserved as a page property so provenance survives. |
| `modifiedTime` | `_page_property:Original Modified` | Workaround |  |
| `parents` | `spaceId` | Full |  |
| `starred` | no equivalent | Dropped | Starring is per-user Drive state, not document data. Confluence favourites are equally per-user and cannot be set for someone else. |
| `description` | `_page_property:Drive Description` | Full | Drive file description — almost always empty, and unrelated to document content. |

### DocumentBody → InlineXHTML

| gdocs field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `body.content` | `body.value` | Partial | StructuralElement → storage XHTML. paragraph → /, table → , tableOfContents → toc macro, sectionBreak → dropped. |
| `tabs` | `body.value` | Partial | Confluence pages have no tabs. Tabs are concatenated in reading order with an  per tab; childTabs flatten into the same page. A document with many tabs becomes one very long page. |
| `lists` | `body.value` | Full | glyphType decides  vs ; nestingLevel rebuilds containment; a ballot-box glyphSymbol becomes an . |
| `inlineObjects` | `body.value` | Partial | imageProperties.contentUri expires in ~30 minutes and needs Google credentials, so an  reference works during the migration and breaks afterwards. sourceUri is preferred when present. A durable result needs a rehost pass (download with source auth, upload as a Confluence attachment, rewrite to ) — not yet implemented for this pair. |
| `footnotes` | `body.value` | Partial | Confluence storage format has no footnote node. Referenced footnotes are collected into a 'Footnotes' section appended to the page and linked by anchor; numbering follows reference order, which diverges from Google's when a footnote is referenced twice. |
| `positionedObjects` | no equivalent | Dropped | Floating (text-wrapped) images are anchored to a paragraph rather than sitting in its element list, so a body walk never encounters them. Inline images are unaffected. |
| `headers` | no equivalent | Dropped | Running page headers are a print concept with no wiki equivalent. |
| `footers` | no equivalent | Dropped | Running page footers are a print concept with no wiki equivalent. |
| `documentStyle` | no equivalent | Dropped | Page size, margins and column layout do not apply to a wiki page. |
| `namedRanges` | no equivalent | Dropped | Add-on defined text ranges. Confluence has no positional-range concept to hold them. |
| `suggestionsViewMode` | no equivalent | Dropped | Reads fold pending suggestions in as though accepted. Unaccepted edits therefore ship as approved text. Warn the user to resolve suggestions BEFORE the run. |

### Comment → FooterComment

| gdocs field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `content` | `body.value` | Partial | Google comments are plain text; Confluence footer comments are storage XHTML. Text converts cleanly, but the comment lands unanchored. |
| `author.displayName` | no equivalent | Workaround | The API token's owner is recorded as the author of every migrated comment. The original name survives only in the comment text prefix. |
| `createdTime` | no equivalent | Workaround | Server-set on the target. Preserved in the comment text prefix. |
| `quotedFileContent.value` | no equivalent | Workaround | The text the comment was anchored to. Since the anchor itself cannot be reproduced, quoting this into the comment body is the only way a reader can tell what was being discussed. |
| `replies` | no equivalent | Partial | Confluence footer comments support threading, but the executor writes each reply as its own top-level footer comment with an author prefix. Thread shape flattens. |
| `anchor` | no equivalent | Dropped | Opaque, revision-scoped region reference. Meaningless against a different document in a different system. |
| `resolved` | no equivalent | Dropped | Confluence's v2 footer-comment create takes no resolved flag. A resolved Google comment migrates as an open one; prefix it with '[Resolved]' if that distinction matters. |

### DocumentLink → InlinePageLink

| gdocs field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `_doc_to_doc_link` | `body.value` | Full | A link to https://docs.google.com/document/d//… is an intra-source reference, not an external URL. The converter routes both textRun links and richLink elements through the target's ref_resolution policy: inline when the referenced page already exists, otherwise a placeholder that the resolution wave swaps for  once it does. |
| `link.headingId` | no equivalent | Dropped | In-document anchors point at an opaque Google heading id. Confluence derives its own anchors from heading TEXT, so the id cannot be translated — the link text survives, the jump does not. |
| `link.bookmarkId` | no equivalent | Dropped | Same as headingId — an opaque Google identifier with no target equivalent. |

### Permission → null

| gdocs field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `*` | no equivalent | Pending | Drive grants access per file; Confluence grants it per space. There is no faithful translation — a per-document permission set collapses into one space-level policy. Surface the source permissions to the admin and let them set space permissions deliberately rather than guessing. |

### Revision → null

| gdocs field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `*` | no equivalent | Dropped | Google exposes only pinned revisions and recent autosaves, and no wiki target can accept another system's edit history. Migrated pages start at version 1 with the migration as their only author. |

## FAQ

**Does a Google Docs to Confluence migration keep tables and formatting?**

Headings, lists, tables, links, footnotes and inline styling all carry across. Merged cells, column widths and cell shading do not, and a header row survives only when Google actually marked it as one — many documents just bold the first row, which arrives as ordinary bold text.

**What happens to images in a Google Doc?**

Google serves inline images from a URL that expires in about 30 minutes and needs the original credentials. Migratly reports every image before the run rather than letting them silently break afterwards; a durable result needs the image re-hosted as a Confluence attachment, which is flagged as follow-up work rather than claimed as done.

**Are tracked changes and suggestions migrated?**

No. The Docs API returns a document with pending suggestions folded in as though they were accepted, and discards the suggestion metadata. Resolve or reject suggestions before the run, or unapproved edits ship as approved text.

**Can Migratly migrate Word documents stored in Google Drive?**

Not through this pair. An uploaded .docx is a different file type with no Docs-API body, so it is excluded deliberately rather than migrated as an empty page. Moving those files is the Google Drive connector's job.

**How long does a Google Docs migration take?**

The Google Docs API allows 60 reads per minute per user and every page costs exactly one read, so throughput tops out near one document per second regardless of parallelism. A few thousand documents is over an hour of pure API time, which Migratly estimates before the run starts.

---

Start a migration: https://app.migratly.dev/
