---
title: "Guru to Notion Migration: The CTO's Technical Guide"
slug: guru-to-notion-migration-the-ctos-technical-guide
date: 2026-05-19
author: Raaj
categories: [Notion, Migration Guide, Guru]
excerpt: "A CTO-level technical guide to migrating from Guru to Notion — covering API limits, data mapping, block chunking, link rewriting, and migration approaches."
tldr: "Guru-to-Notion migration requires HTML-to-block parsing, handling Notion's 3 req/sec rate limit, 100-block append ceiling, and a two-pass approach for internal link rewriting."
canonical: https://clonepartner.com/blog/guru-to-notion-migration-the-ctos-technical-guide/
---

# Guru to Notion Migration: The CTO's Technical Guide


Migrating from Guru to Notion is a content-architecture translation problem, not a file export. Guru organizes knowledge as flat HTML Cards inside a Collection → Folder hierarchy. Notion stores every paragraph, heading, and list item as a discrete JSON block with its own ID, nested inside pages that can double as relational database rows. There is no native Guru-to-Notion importer in Notion. ([notion.com](https://www.notion.com/help/import-data-into-notion?assetsVersion=23.13.20251202.2151))

Your options are a manual ZIP/CSV export pipeline, a custom API-based ETL, middleware tools like Zapier or Make, or a managed migration service. The right choice depends on your content volume, structural complexity, and how much engineering time you can afford to burn.

This guide covers the architectural differences between both platforms, the exact API constraints that break migration scripts, data mapping strategies for wiki content and structured operational data, and the step-by-step process for a production-grade migration.

## Guru vs. Notion: Architecture Differences That Matter

**Guru** is a knowledge-sharing platform built around a three-level hierarchy: **Collections** contain **Folders** (up to three nested levels), and Folders contain **Cards**. Cards are the atomic unit — each holds HTML-formatted content, metadata (verification status, owner, tags, last verified date), and optional file attachments. Guru also has **Boards**, which are visual groupings layered on top of the Card structure. The platform is designed for bite-sized, verified knowledge articles, not long-form documentation or relational data. ([help.getguru.com](https://help.getguru.com/docs/creating-managing-and-deleting-a-collection))

**Notion** is a block-based workspace where every element — a paragraph, a toggle, a database row — is an individual block with a unique ID. Pages can nest infinitely inside other pages, and databases allow structured data with typed properties (text, select, date, relation, formula, rollup). Notion's power is in relational data modeling; its weakness is that it was never designed as a flat knowledge base with verification workflows.

**Why companies migrate:**

- **Cost consolidation** — teams already using Notion for project management want to eliminate a separate knowledge base tool
- **Unified workspace** — Notion's all-in-one model reduces context switching between wiki, project boards, and docs
- **Flexibility** — Notion databases enable structured metadata that Guru's Card model doesn't support natively
- **AI features** — Notion's integrated AI benefits from having all organizational knowledge in one platform

> [!WARNING]
> **Key architectural mismatch:** Guru Card content is stored as HTML. Notion's API expects block-level JSON objects. Every Card must be parsed, decomposed into individual blocks (paragraphs, headings, lists, code blocks, images), and reassembled via Notion's block API — which has hard limits on nesting depth, array size, and payload weight.

For a broader look at Notion's architecture compared to other platforms, see [Notion vs. Confluence (2026): Architecture, Limits, and Migration Guide](https://clonepartner.com/blog/blog/notion-vs-confluence-2026/).

## Migration Approaches

### Method 1: Native ZIP/CSV Export → Notion Import

**How it works:** Export Guru Collections as ZIP files containing HTML files, or export Cards via Card Manager as CSV with HTML content in each row. Convert the HTML to Markdown using a tool like `pandoc` or a Python script with `markdownify`. Import the Markdown files into Notion via Settings → Import → Text & Markdown.

**Steps:**
1. In Guru, navigate to Manage → Collections → Collection Settings (⋮) → Export
2. Receive the ZIP file via email (large exports can take up to an hour)
3. Convert HTML files to Markdown
4. ZIP the converted Markdown files
5. In Notion, go to Settings → Import → Text & Markdown → Upload ZIP

**When to use it:** Small teams with fewer than 200 Cards, simple text-only documentation, and no internal linking requirements.

**Pros:** No code required. Free.

**Cons:** All internal links between Cards break. Folder hierarchy is flattened — you'll need to manually reconstruct the page tree in Notion. Attachments are not included in the ZIP export. Guru's CSV export includes image references as URLs, not embedded files. HTML and Markdown imports are capped at 5 MB per file on Free plans and 50 MB on paid plans. Notion throttles imports to approximately 120 file imports per 12 hours. Large ZIPs with 10,000+ files are likely to fail or partially import. CSV imports do not carry relations, rollups, or formulas, and repeated CSV merges add rows instead of updating existing ones. Collection exports do not include archived Cards — restore them before export if they belong in scope. ([help.getguru.com](https://help.getguru.com/docs/exporting-content-from-guru), [notion.com](https://www.notion.com/help/import-data-into-notion?assetsVersion=23.13.20251202.2151))

**Complexity:** Low

If you're wondering why native import tools struggle in general, see [Notion export not working? Fix stuck exports, failed downloads, and PDF page break issues](https://clonepartner.com/blog/blog/fix-notion-export-errors-and-pdf-page-breaks/).

### Method 2: API-Based Custom ETL (Guru REST API → Notion API)

**How it works:** Build a script that extracts Cards and Folder structures from Guru's REST API, transforms HTML content into Notion block JSON (or Notion-flavored Markdown), and loads it via Notion's Create Page and Append Block Children endpoints.

Guru's REST API is available at `https://api.getguru.com/api/v1/` and uses HTTP Basic Auth with a user email and API token. The primary search endpoint (`GET /v1/search/query`) returns up to 50 cards per call with `Link`-header pagination. For incremental pulls, Guru Query Language supports filtering on `lastModified` with ISO-8601 timestamps. ([developer.getguru.com](https://developer.getguru.com/docs/listing-cards))

Notion's API uses an integration token (Bearer auth).

**When to use it:** Teams with 500+ Cards, complex folder structures, internal cross-references, or structured metadata that needs to land in Notion databases.

**Pros:** Full control over mapping logic. Can preserve internal links by building an ID mapping table (Guru Card ID → Notion Page ID). Can populate Notion database properties with Guru metadata (verification status, owner, tags). Supports repeatable dry runs and delta syncs using Guru's `lastModified` filter.

**Cons:** Significant engineering investment (2–4 weeks for a production-grade implementation). Must handle Notion's aggressive rate limits, block chunking, and nesting constraints. Guru's API rate limits are enforced per account but not publicly documented — you'll discover thresholds empirically.

**Complexity:** High

### Method 3: Middleware / iPaaS (Zapier, Make)

**How it works:** Use Zapier or Make to trigger on Guru events (e.g., new Card created) and create corresponding pages in Notion. Guru supports Zapier integration natively. Make exposes a Notion connector plus HTTP modules for APIs not available as first-class modules. ([zapier.com](https://zapier.com/apps/guru/integrations/notion))

**When to use it:** Ongoing sync of new content during a phased transition — not practical for bulk historical migration.

**Pros:** No code needed. Quick to stand up for event-driven automation.

**Cons:** These tools operate one record at a time and hit Notion's API limits quickly. No bulk backfill capability. Complex HTML content in Guru Cards will be truncated or mangled. Notion's block constraints (100 blocks per request, 2-level nesting per request) are not handled gracefully by most middleware actions. Cost scales with volume.

**Complexity:** Medium

### Method 4: Managed Migration Service

**How it works:** A migration partner handles extraction, transformation, loading, validation, and internal link preservation end-to-end.

**When to use it:** When your engineering team has higher-priority product work, or when the dataset is large enough that handling Notion's API constraints becomes a multi-week project.

**Pros:** Fastest time to completion. Rate limit management, block chunking, and link rewriting handled for you. Validation and rollback planning included.

**Cons:** Cost. Requires sharing API credentials with a third party. Quality varies — many providers just wrap CSV imports, so verify how they handle Card links, attachments, structured data, and rollback.

**Complexity:** Low (for your team)

### Comparison Table

| Approach | Complexity | Best For | Preserves Links | Handles Attachments | Scalability |
|---|---|---|---|---|---|
| Native ZIP/CSV | Low | <200 Cards, simple content | ❌ | ❌ | Small only |
| Custom API ETL | High | 500+ Cards, structured data | ✅ (with ID mapping) | ✅ (download + re-upload) | Enterprise |
| Zapier / Make | Medium | Ongoing sync, not bulk | ❌ | ❌ | Limited |
| Managed Service | Low (your side) | Any scale, complex data | ✅ | ✅ | Enterprise |

### Scenario Recommendations

- **Small team, <200 Cards, no internal links:** Native ZIP export → Markdown conversion → Notion import. Budget a day of manual cleanup.
- **Mid-size, 200–1,000 Cards, some internal links:** Custom API ETL if you have a developer available for 2+ weeks. Otherwise, managed service.
- **Enterprise, 1,000+ Cards, complex hierarchy, compliance needs:** Managed migration. The engineering cost of handling Notion's API limits at scale almost always exceeds the cost of a service.
- **Phased migration (both tools running in parallel):** Use Zapier/Make for ongoing sync of new Cards during the transition, with a one-time bulk migration via API or managed service.

For a broader breakdown of Notion's native import behavior, see [How to Import Data into Notion: Formats, Limits & Data Mapping](https://clonepartner.com/blog/blog/how-to-import-data-into-notion-formats-limits-data-mapping/).

## When to Use a Managed Migration Service

Building a Guru-to-Notion pipeline in-house sounds straightforward until you hit the compounding API constraints. Here's what actually consumes engineering time:

- **HTML-to-block parsing:** Guru Cards are HTML. Converting `<ul><li><ul><li>nested</li></ul></li></ul>` into Notion's block JSON — while respecting the 2-level nesting limit per API request — requires a recursive parser that splits deep structures across multiple sequential API calls.
- **Rate limit management:** Notion enforces an average of 3 requests per second per integration token. A workspace with 1,000 Cards, each requiring 3–5 API calls (create page + append blocks in chunks), means 3,000–5,000 API calls minimum. At 3 req/sec, that's 17–28 minutes of pure API time — assuming zero 429 errors. In practice, you'll hit 429s and need exponential backoff.
- **Internal link rewriting:** Every `href` pointing to another Guru Card must be rewritten to the corresponding Notion page URL. This requires a two-pass approach: first create all pages and build the ID map, then go back and update all internal links.
- **Attachment handling:** Attachments referenced in Cards must be downloaded from Guru's CDN and re-uploaded to Notion. Guru's API doesn't have a dedicated attachment download endpoint — you need to parse HTML `<img>` and `<a>` tags to extract CDN URLs.

The hidden cost isn't the migration script — it's the debugging, edge-case handling, and validation that turn a 2-week estimate into a 6-week project. The first 90% of Cards migrate smoothly. The last 10% — unusual formatting, large attachments, deep nesting — consume 50% of your total effort.

At ClonePartner, we handle Guru-to-Notion migrations with:

- **Automatic block chunking** for Notion's 100-block append ceiling
- **Payload optimization** — base64 images that violate Notion's 500KB payload limit are extracted, hosted, and referenced as valid external block URLs
- **Internal link preservation** via Guru ID → Notion Page ID mapping with a secondary pass to eliminate dead links
- **Structured data support** — CRM-like data (Accounts, Contacts, Opportunities) mapped from Guru tags and cards into Notion relational databases
- **Zero-downtime cutover** so your team keeps using Guru until the Notion workspace is fully validated

For a look at our migration methodology, see [How We Run Migrations at ClonePartner](https://clonepartner.com/blog/blog/how-we-run-migrations-at-clonepartner/).

## Pre-Migration Planning & Data Audit

Before writing any code or exporting any data, audit what you actually have in Guru and what you need in Notion.

### Audit Checklist

- [ ] **Collections inventory** — List every Collection, its Card count, and its owner
- [ ] **Folder structure** — Map the full hierarchy per Collection (Folders can nest up to three levels)
- [ ] **Card metadata** — Identify which metadata fields matter: verification status, tags, boards, last verified date, owner/verifier
- [ ] **Internal links** — Count Cards that reference other Cards or use section-level deep links ([help.getguru.com](https://help.getguru.com/docs/guru-card-editor))
- [ ] **Attachments** — Identify Cards with embedded files, images, or linked attachments
- [ ] **Archived Cards** — Collection exports do not include archived Cards. Restore any that belong in scope before exporting ([help.getguru.com](https://help.getguru.com/docs/exporting-content-from-guru))
- [ ] **Stale content** — Flag Cards that are unverified, expired, or haven't been accessed in 12+ months
- [ ] **Duplicate Cards** — Guru doesn't enforce unique titles; check for duplicates before migrating
- [ ] **Structured templates** — Identify any Card templates acting as databases: account sheets, contact directories, lead records, deal notes

### Define Migration Scope

Not everything in Guru needs to go to Notion. Stale Cards, deprecated processes, and team-specific drafts can often be archived rather than migrated. Reducing scope by 20–30% typically cuts migration time by 40%+ because fewer Cards means fewer API calls, fewer links to rewrite, and less validation work.

### Migration Strategy

| Strategy | When to Use | Risk |
|---|---|---|
| **Big bang** | Small dataset (<500 Cards), team can tolerate a short freeze | All-or-nothing; hard to roll back |
| **Phased by Collection** | Multiple teams, different readiness levels | Requires running both tools in parallel |
| **Incremental** | Ongoing content creation during migration window | Highest complexity; needs sync via `lastModified` watermark or Guru webhooks ([developer.getguru.com](https://developer.getguru.com/docs/syncing-cards)) |

For most teams, **phased by Collection** is the right call. Migrate one Collection at a time, validate it, onboard that team to Notion, then move to the next.

## Data Model & Object Mapping

Guru and Notion have fundamentally different data models. Mapping standard wiki content is relatively straightforward, but things get complex when teams have been using Guru to track structured operational data.

### Standard Wiki Content Mapping

| Guru Object | Notion Equivalent | Notes |
|---|---|---|
| Collection | Top-level Page or Teamspace | One Notion page per Collection |
| Folder | Nested Page | Folders become child pages; preserve the hierarchy |
| Card | Page (under parent Folder page) | Card HTML → Notion blocks |
| Card Tags | Multi-select property | If Cards land in a Notion database |
| Verification Status | Status property | Map TRUSTED → Verified, NEEDS_VERIFICATION → Needs Review |
| Card Owner / Verifier | Person property | Requires mapping Guru user emails to Notion user IDs |
| Board / Board Group | Database View or Linked View | Boards are visual groupings; recreate as filtered database views |

### Structured / CRM-Like Data Mapping

Some teams use Guru to track structured data beyond wiki content — account information, contact directories, opportunity stages.

> [!NOTE]
> **Important:** Accounts, Contacts, Leads, Opportunities, and Activities are not Guru-native objects. Guru's documented core model is Collections, Folders, and Cards. If your team stored CRM-like data in Guru Card templates, the mapping below is a target-state design for Notion databases — not a literal object-for-object export. ([help.getguru.com](https://help.getguru.com/docs/creating-managing-and-deleting-a-collection))

| Guru Template / Concept | Notion Target | Implementation |
|---|---|---|
| Account/Company Cards | Companies Database | Each Card → database row; Card fields → typed properties |
| Contact Cards | Contacts Database | Use Relation property to link to Companies |
| Opportunity/Pipeline Cards | Pipeline Database | Status property with pipeline stages; Board view (Kanban) |
| Activity/Task Cards | Tasks Database | Date property for due dates; Relation to parent records |

### Field-Level Mapping

| Guru Field | Guru Type | Notion Property Type | Transformation |
|---|---|---|---|
| `preferredPhrase` (Title) | String | Title | Direct map |
| `content` | HTML | Page body (blocks) | Parse HTML → Notion block JSON or Markdown |
| `collection.name` | String | Select property | Map to Collection grouping |
| `tags` | Array of strings | Multi-select | Direct map; normalize case |
| `verificationState` | Enum | Status | Map TRUSTED → Verified |
| `owner.email` | String | Person | Resolve to Notion user ID |
| `dateCreated` | ISO 8601 | Date | Direct map |
| `lastModified` | ISO 8601 | Last edited time | Auto-populated by Notion |
| `lastVerifiedDate` | ISO 8601 | Date | Custom "Last Verified" property |
| Card ID | String | Text property (`Guru Card ID`) | Preserve as stable deduplication key |
| Attached files | CDN URLs in HTML | File blocks or uploaded files | Download → re-upload |
| Internal Card Links | Guru URLs | Page mentions or URLs | ID mapping + rewrite in second pass |

> [!NOTE]
> **Notion limitation:** Notion doesn't have true custom objects. Everything is a database with typed properties. You can model any Guru data structure — but you lose Guru's verification workflow, trust scores, and AI-powered answer features. Notion also recommends a maximum of 500 properties or 50 KB schema size per database. Plan for which organizational processes need to be rebuilt in Notion after migration.

For more on mapping complex architectures into Notion, see [Document360 to Notion Migration: Data Mapping, API Limits & Methods](https://clonepartner.com/blog/blog/document360-to-notion-migration-data-mapping-api-limits-methods/).

## Handling Notion API Limits & Constraints

Notion's API has some of the most aggressive constraints of any SaaS platform. A custom migration pipeline must architect around all of them simultaneously. ([developers.notion.com](https://developers.notion.com/reference/request-limits))

### Rate Limits

Notion enforces an average of **3 requests per second** per integration token — roughly 2,700 requests per 15-minute window. Some bursts beyond the average are allowed. Rate-limited requests return HTTP 429 with a `Retry-After` header.

**Practical math for a 1,000-Card workspace (20 blocks per Card average):**

- **Page creation:** ~1,000 API calls
- **Block appends:** ~1,000 calls (most Cards fit in one append)
- **Link rewriting pass:** ~500 calls (for Cards with internal links)
- **Total:** ~2,500 calls minimum
- **Wall clock time at 3 req/sec:** ~14 minutes (best case, zero 429s)

For 5,000 Cards with complex content averaging 50 blocks each, expect 10,000+ API calls and 1–2 hours of migration time — plus retry overhead.

### Block Append Limits

The `PATCH /v1/blocks/{block_id}/children` endpoint has hard limits:

- **100 block children** maximum per single API request
- **2 levels of nesting** maximum per single request
- **1,000 blocks total** per request payload
- **500KB** maximum request payload size
- **2,000 characters** maximum per rich text object

> [!WARNING]
> **Nesting clarification:** The documented 2-level nesting limit applies to a **single append request**, not to the page itself. Deep content trees are still possible — you must create the top two levels first, capture the returned block IDs, then append deeper levels in subsequent requests. This multiplies API calls significantly but achieves the correct structure. ([developers.notion.com](https://developers.notion.com/reference/patch-block-children))

A long Guru Card with 150 paragraphs requires at minimum two append requests. A Card with deeply nested bullet lists (3+ levels) requires creating the top two levels, capturing returned block IDs, then recursively appending deeper children.

### The 500KB Payload Limit and Base64 Images

Guru Cards often contain inline base64-encoded images. A single base64 image can easily exceed 2MB. If you pass this directly to Notion, the request fails. Extract base64 strings during the transformation phase, upload them to external storage (like AWS S3), and pass the public URL to Notion as an `image` block. For files that are publicly reachable, Notion can import by external URL. For private files, use Notion's File Upload API — small direct uploads handle files up to 20 MB, and larger files need multipart upload.

### Guru API Constraints

Guru's REST API uses Basic authentication and supports user and collection tokens. The primary search endpoint (`GET /v1/search/query`) returns a maximum of 50 cards per call with `Link`-header pagination. For incremental pulls, Guru Query Language lets you filter on `lastModified` with ISO-8601 timestamps. ([developer.getguru.com](https://developer.getguru.com/docs/listing-cards))

> [!WARNING]
> **Guru API rate limits are not publicly documented.** Multiple sources confirm Guru enforces rate limits per account but doesn't publish specific numeric thresholds. The API returns HTTP 429 when limits are exceeded. Build in conservative delays (200–300ms between requests) and implement backoff logic.

## Step-by-Step API Migration Process

### Step 1: Extract from Guru

Use the Guru REST API to pull all Cards, Folders, and Collection structures. For delta syncs, filter on `lastModified`:

```bash
curl -u USER:TOKEN \
  'https://api.getguru.com/api/v1/search/query?q=lastModified%20%3E%3D%202026-05-01T00:00:00.000%2B00:00&maxResults=50'
```

Follow the `Link` header until exhausted. Store the final `lastModified` watermark for rerunning deltas safely.

```python
import requests
import time
from base64 import b64encode

GURU_BASE = "https://api.getguru.com/api/v1"
auth_header = b64encode(f"{GURU_EMAIL}:{GURU_TOKEN}".encode()).decode()
headers = {"Authorization": f"Basic {auth_header}"}

# Fetch all collections
collections = requests.get(f"{GURU_BASE}/collections", headers=headers).json()

# Fetch all cards (paginated, 50 per page)
cards = []
page = 1
while True:
    resp = requests.get(
        f"{GURU_BASE}/cards",
        headers=headers,
        params={"page": page, "pageSize": 50}
    ).json()
    if not resp:
        break
    cards.extend(resp)
    page += 1
    time.sleep(0.3)  # Conservative throttle for undocumented rate limits
```

Each Card response includes `content` (HTML), `collection`, `tags`, `verificationState`, and metadata fields.

Alternatively, export Guru Collections as a ZIP file for the initial read — this provides the highest fidelity HTML and avoids Guru API rate limits during extraction. Use the API for subsequent delta syncs.

### Step 2: Transform HTML to Notion Blocks

This is the hardest part of the migration. Parse Guru Card HTML and convert it into Notion block JSON objects.

```python
from bs4 import BeautifulSoup

def html_to_notion_blocks(html_content):
    soup = BeautifulSoup(html_content, "html.parser")
    blocks = []
    for element in soup.children:
        if element.name == "p":
            text = element.get_text()
            # Enforce 2,000 char limit per rich_text object
            chunks = [text[i:i+2000] for i in range(0, len(text), 2000)]
            for chunk in chunks:
                blocks.append({
                    "type": "paragraph",
                    "paragraph": {
                        "rich_text": [{"type": "text", "text": {"content": chunk}}]
                    }
                })
        elif element.name in ["h1", "h2", "h3"]:
            heading_level = element.name
            notion_type = f"heading_{heading_level[1]}"
            blocks.append({
                "type": notion_type,
                notion_type: {
                    "rich_text": [{"type": "text", "text": {"content": element.get_text()[:2000]}}]
                }
            })
        # Handle ul/ol, images, code blocks, tables, etc.
    return blocks
```

**Key transformation rules:**

- Split block arrays into chunks of ≤100 for each API call
- Flatten nested lists deeper than 2 levels per request, or split across multiple sequential API calls to achieve deeper nesting
- Extract inline base64 images, upload to external storage, and reference via URL
- Rewrite internal Guru links as placeholder URLs (resolved in Step 4)
- Truncate any rich text content exceeding 2,000 characters per text object

For document-heavy content that is mostly prose, Notion's Markdown endpoints can be simpler than assembling block JSON by hand. Use the Markdown-based page creation when tight block-level control isn't required. ([developers.notion.com](https://developers.notion.com/guides/data-apis/working-with-markdown-content))

### Step 3: Load into Notion

Create pages and append blocks via the Notion API with rate limiting and retry logic:

```python
import time
import requests

NOTION_API_URL = "https://api.notion.com/v1"
HEADERS = {
    "Authorization": f"Bearer {NOTION_TOKEN}",
    "Content-Type": "application/json",
    "Notion-Version": "2026-03-11"
}

def notion_request_with_retry(method, url, json_body, max_retries=5):
    for attempt in range(max_retries):
        response = requests.request(method, url, headers=HEADERS, json=json_body)
        if response.status_code == 200:
            time.sleep(0.34)  # Stay under 3 req/sec
            return response.json()
        elif response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
            time.sleep(retry_after)
        else:
            raise Exception(f"API error {response.status_code}: {response.text}")
    raise Exception(f"Failed after {max_retries} retries")

def create_page_with_blocks(parent_id, title, blocks):
    # Create page with first 100 blocks
    page_body = {
        "parent": {"page_id": parent_id},
        "properties": {"title": [{"text": {"content": title}}]},
        "children": blocks[:100]
    }
    page = notion_request_with_retry("POST", f"{NOTION_API_URL}/pages", page_body)
    
    # Append remaining blocks in chunks of 100
    remaining = blocks[100:]
    for i in range(0, len(remaining), 100):
        chunk = remaining[i:i+100]
        notion_request_with_retry(
            "PATCH",
            f"{NOTION_API_URL}/blocks/{page['id']}/children",
            {"children": chunk}
        )
    return page
```

### Step 4: Rebuild Internal Links

After all pages are created, build a lookup table mapping Guru Card IDs to Notion Page IDs. Store this mapping in a persistent store (SQLite, CSV, or a tracking database) — not just in memory. If the migration fails mid-run, you need this map to resume without duplicating pages.

Iterate through all migrated pages, find any link blocks pointing to `app.getguru.com/card/{id}` or other Guru URL patterns, look up the corresponding Notion Page ID, and update the block to an internal Notion link.

This two-pass approach is required because you can't know the Notion Page ID for a target Card until that Card has been migrated. If you use Notion relation properties, make sure the related databases are shared with the integration — otherwise relation values appear empty. Rollup values cannot be written directly through the API; load relations first and let Notion recalculate. ([developers.notion.com](https://developers.notion.com/reference/property-value-object))

### Step 5: Validate

Run automated checks: Card count in Guru vs. page count in Notion, spot-check content rendering, verify internal links resolve correctly. See the Validation section below.

## Edge Cases & Challenges

**Duplicate Cards:** Guru doesn't enforce unique Card titles. If you're loading Cards into a Notion database, add a `Guru Card ID` text property as a deduplication key. Rely on IDs for mapping, never string matching on titles.

**Deeply nested content:** Guru Cards can contain nested lists 5+ levels deep. Notion's API allows only 2 levels of nesting per single append request — but deeper trees are achievable by creating the top two levels, capturing returned block IDs, then recursively appending deeper levels in subsequent requests. This multiplies API calls but produces the correct structure.

**Embedded images and files:** Guru stores attachments on its own CDN. These URLs may be authenticated or expire after Guru access is revoked. Download all attachments during extraction and re-upload to Notion. For Notion uploads, small direct uploads handle files up to 20 MB; larger files need multipart handling.

**Rich text formatting loss:** Guru supports certain HTML tables, callouts, and custom formatting that may not have direct Notion block equivalents. Complex HTML tables may need conversion to Notion databases or simplified markdown tables. Notion's Markdown APIs represent unsupported blocks as `<unknown>` tags. Test with a representative sample before running a full migration.

**Verification metadata:** Guru's trust score and verification workflows have no native equivalent in Notion. You can store the last verification state as a database property, but the automated verification reminder workflow must be rebuilt using Notion automations or external tools.

**Section-level deep links:** Guru supports section-level deep links within Cards. Notion's Markdown import does not preserve anchor links reliably. Plan to convert these to page-level links or explicit table-of-contents pages. ([help.getguru.com](https://help.getguru.com/docs/guru-card-editor))

**Large pages:** Very large pages can truncate around the 20,000-block mark when retrieved as Markdown — a signal that the source content should have been split during transformation. ([developers.notion.com](https://developers.notion.com/guides/data-apis/working-with-markdown-content))

**API failures mid-migration:** If a migration fails partway through, you need idempotent logic. Use the Guru Card ID as a deduplication key — before creating a Notion page, check if one already exists with that ID in your tracking store. Notion CSV imports and merges add rows; they do not update existing rows, and a CSV merge cannot be directly undone.

**Relation property gotchas:** Retrieve-page responses can truncate rich relation data, and relation properties expose `has_more` when reference counts grow. Use page-property retrieval for deep validation of relational data. ([developers.notion.com](https://developers.notion.com/reference/property-value-object))

## What Doesn't Survive the Migration

Be explicit with stakeholders about what cannot transfer unchanged:

| Capability | Guru | Notion | Impact |
|---|---|---|---|
| Verification workflow | Built-in (trust scores, expiry, reminders) | None — must rebuild with automations | Manual process change |
| Knowledge alerts | Native | Not available | Lost feature |
| Browser extension (in-context answers) | Native | Limited (Web Clipper is different) | Workflow change |
| AI Answers (context-aware search) | Native | Notion AI (different architecture) | Different UX |
| Permission model | Collection-level + Group-based | Page-level + Teamspace | Requires permission redesign |
| Card comments & activity log | Full history | Page comments (no migration path) | Historical comments lost |
| Section-level deep links | Native card section links | No reliable anchor links | Must convert to page-level links |

## Validation, Testing & Post-Migration

### Record Count Comparison

Query both systems after migration:

- Total Cards in Guru (via API with pagination) vs. total pages in Notion (via `POST /v1/search`)
- Count per Collection → count per parent page in Notion
- If archived Cards were excluded from export, document that as a scope decision, not a surprise

### Content Validation

Sample 5–10% of migrated Cards across different Collections (with a floor of 50 pages for large workspaces). For each:

- Compare rendered text content (strip HTML from Guru, extract text from Notion blocks)
- Verify headings, lists, and code blocks render correctly
- Check that images display (not broken links to expired Guru CDN URLs)
- Verify internal links point to the correct Notion pages

### UAT Process

1. Migrate one Collection as a pilot
2. Have 2–3 team members from that Collection review their most-used Cards
3. Document formatting issues, broken links, missing content
4. Fix the transformation logic
5. Re-run the pilot and validate fixes
6. Proceed to full migration only after pilot sign-off

### Rollback Plan

Do not delete or modify anything in Guru until validation is complete and teams have used Notion for at least 1–2 weeks. Guru remains your rollback. Keep it live during validation, run a final delta using `lastModified`, and only switch entry points after UAT sign-off.

### Post-Migration Tasks

**Rebuild verification workflows:** Create a Notion database with a "Last Reviewed" date property and "Review Status" status property. Set up automations or calendar reminders for periodic content reviews.

**Recreate navigation:** Guru's Board and Board Group views are visual navigation layers. In Notion, recreate these as linked database views with filters, or as curated landing pages linking to key content.

**Rebuild computed properties:** Relations, rollups, formulas, filtered views, and automations that were implicit in Guru habits must be explicitly built in Notion.

**User onboarding:** Notion's editing model is different from Guru's Card editor. Run a brief walkthrough covering: how to find content (search + sidebar), how to edit pages (block-based editing), and how the new review process works.

**Monitor for data inconsistencies:** For the first 2 weeks post-migration, keep a shared Notion page where team members can report missing content, broken links, or formatting issues. Triage and fix in batches.

## Best Practices

- **Back up everything first.** Export all Guru Collections as ZIP files before starting. Store them independently of the migration process.
- **Run a pilot migration.** Pick your smallest, least-critical Collection. Migrate it end-to-end. This surfaces 80% of edge cases with minimal risk.
- **Track migration state.** Maintain a mapping table (CSV or database) of Guru Card ID → Notion Page ID → migration status (pending/migrated/validated/failed). This enables reruns and debugging.
- **Throttle conservatively.** Start at 1 request per second against Notion's API and increase gradually. Hitting 429s repeatedly can trigger longer cooldown periods.
- **Validate incrementally.** Don't wait until the full migration is complete. Check each Collection as it lands in Notion.
- **Plan for the long tail.** The first 90% of Cards migrate smoothly. The last 10% — unusual formatting, large attachments, deep nesting — consume 50% of total effort.
- **Use idempotent writes.** Before creating a Notion page, check your tracking store for the Guru Card ID. This prevents duplicates on reruns.

For more patterns on knowledge base migrations, see [The Ultimate Knowledge Base Migration Checklist: A Zero-Downtime Plan](https://clonepartner.com/blog/blog/the-ultimate-knowledge-base-migration-checklist-a-zero-downtime-plan/).

## What This Comes Down To

Guru-to-Notion migration is technically achievable but architecturally non-trivial. The core challenge isn't getting content out of Guru — the ZIP export and REST API both work. The challenge is getting content *into* Notion at scale while respecting the 3 req/sec rate limit, 100-block append ceiling, 2-level nesting constraint per request, 2,000-character text limit, and 500KB payload cap — all simultaneously.

For small teams with fewer than 200 Cards and mostly prose content, the native ZIP → Markdown → Notion import path works with a day of manual cleanup. For anything larger — especially if you have internal links, attachments, structured operational data, or need repeatable dry runs — you're building a real ETL pipeline with HTML parsing, block chunking, rate limiting, and two-pass link rewriting. That's a 2–4 week engineering project pulling developers off product work.

If your Guru workspace has become part knowledge base and part operational system, treat the move like a schema migration, not a content export.

> Need to move your Guru knowledge base to Notion without burning engineering cycles? ClonePartner handles complex content migrations with full link preservation, zero downtime, and validation built in. Book a 30-minute technical scoping call.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I import Guru data directly into Notion?

There is no native Guru-to-Notion importer. You can export Guru Collections as ZIP files with HTML content, convert to Markdown, then import via Notion's Settings → Import → Text & Markdown. Internal links, attachments, and folder hierarchy will not be preserved automatically. CSV imports do not carry relations, rollups, or formulas.

### What Notion API limits matter most during migration?

Notion enforces an average of 3 requests per second per integration token (approximately 2,700 requests per 15-minute window). Block appends are limited to 100 children per request with a 500KB payload cap. Nesting is limited to 2 levels per single append request. Rich text objects max out at 2,000 characters. Rate-limited requests return HTTP 429 with a Retry-After header.

### How do I handle deeply nested Guru content in Notion's API?

Notion's 2-level nesting limit applies per single append request, not per page. Deep content trees are achievable by creating the top two levels first, capturing returned block IDs, then appending deeper children in subsequent API calls. This multiplies API calls but produces the correct nested structure.

### Does Guru have API rate limits for data extraction?

Yes, Guru enforces rate limits per account, but specific numeric thresholds are not publicly documented. The API returns HTTP 429 when limits are exceeded. Build in conservative delays (200–300ms between requests) and implement backoff logic.

### How long does a Guru to Notion migration take?

For small teams with under 200 Cards using the manual ZIP export path, expect 1–2 days including cleanup. For API-based migrations of 1,000+ Cards, the engineering effort is 2–4 weeks for script development, testing, and validation. A managed migration service typically completes in days.
