# Connect knowledge sources

Import Notion pages, GitHub repositories, docs sites and websites, and keep them current.

Open **Knowledge**, then **Sources** in the sidebar. Connected sources are listed one row each. Under them, a tile for each kind of source: **GitHub**, **Notion**, **Docs site** and **Website**. **Connect** at the top opens the same four.

Only admins and owners can manage sources. Everyone else sees "Only admins and owners can manage sources." See [roles](/articles/roles-and-permissions).

**New** in the library links here too. Its **Connect** items open the source's dialog on this page.

## The list

| Column | Shows |
| --- | --- |
| **Source** | The name, with a detail line under it |
| **Documents** | How many library documents came from it |
| **Last sync** | **Syncing**, **Failed**, **Waiting**, or how long ago it finished |

Each row ends with a sync button, when the source has one, and a menu for everything else. A sync error shows under its row.

## Refresh and limits

| Source | Refreshes on its own | By hand | Limits |
| --- | --- | --- | --- |
| Notion | When the agent starts a reply, at most every 15 minutes. Only pages already imported | **Sync all** | 20 pages per import. Sub-pages 3 levels deep, 25 per page |
| GitHub repository | On every push to the synced branch. When the agent starts a reply, at most once a minute per repository | **Sync now** on the row, **Sync all** | 500 markdown files. 900 KB per file |
| Docs site | Same as a repository | **Sync now** on the row, **Sync all** | 500 pages |
| Website | Never | **Re-scrape** on the row | 500 pages per crawl. 3 crawls at once |

- A refresh runs beside the reply. The reply uses what is indexed at that moment, and the next reply gets the changes.
- **Sync all** shows once a repository, a docs site, or an imported Notion page is connected. It reads every file in every repository and docs site again, and pulls every Notion page again while Notion is connected. It reads **Syncing** while anything is.
- **Sync now** also reads every file, not just the ones that changed. Both buttons are disabled while a sync runs. A push, or a sync asked for over the API, waits and runs once that sync finishes.
- Titles come from a file's frontmatter `title`, then its first heading, then its filename.
- A repository or docs site whose last sync failed shows as a **Needs fixing** item on [Home](/articles/find-your-way-around-the-dashboard#home) as `owner/repo is not syncing`.

## Notion

1. **Connect**

   Choose **Notion** from **Connect**, or press the **Notion** tile. Notion's consent screen opens. Choose the pages Median may see there.

2. **Pick pages**

   You come back to **Sources** and **Import from Notion** opens. Tick pages in the list, or type in **Search pages, or paste a link**. The list shows 20 pages at a time, most recently edited first. A pasted link imports that one page.

3. **Import**

   Leave **Include sub-pages** on to bring child pages along. Press **Import**. The library opens and each page arrives with an **Indexing** badge.

| Rule | Value |
| --- | --- |
| Pages per import | 20. More returns "Up to 20 pages at a time." |
| Sub-page depth | 3 levels below each picked page |
| Sub-pages per page | 25 |
| Sub-page ceiling | New sub-pages stop once the organization holds 300 Notion documents |
| Title | The page's name in Notion |
| Editing | Edit in Notion. The document page shows a **Read only** badge with the tooltip "Edit this document in Notion." |

A refresh pulls every imported page again. It does not add new pages or new sub-pages. Import those.

The workspace gets one row, named for the workspace, with who connected it on the detail line. Its menu holds the rest.

| Item | Does |
| --- | --- |
| **Import pages** | Opens **Import from Notion** again |
| **Change access** | Reopens Notion's consent screen, where you can share more pages with Median |
| **Disconnect** | Asks **Disconnect Notion?** and keeps every imported document. Refreshes stop |

Once Notion is connected, **Notion** in **Connect** opens **Import from Notion** straight away.

| Connect error | Cause |
| --- | --- |
| Notion connection was not authorized. Try connecting again. | Consent was declined on Notion |
| The Notion connection did not finish. Try again. | Notion did not complete the handoff |
| Notion is unavailable right now. Contact Median support. | Notion is not set up on this Median deployment |

A connect link works once, for 15 minutes. After that, the return from Notion lands on a page that reads "This connection link has expired or was already used. Start again from Median."

| Error on a page | Cause |
| --- | --- |
| This page has nothing on it yet. | The page is empty |
| This page is more than one document can hold. Split it up. | The page converts to more than 900 KB of markdown |
| Notion is not connected anymore. Connect it, then try again. | The workspace was disconnected |
| The Notion connection expired. Connect it again, then retry. | Notion would not renew access |
| That link does not point at a Notion page. | The pasted link is not a Notion page |
| Notion will not show this page. Share it with Median, then try again. | The page is not shared with Median in Notion |

A failed page shows **Failed** in the library. The reason and **Try again** are on the document page.

## GitHub

Median reads repositories through its GitHub app. You pick which repositories the app can see during the install on GitHub.

1. **Connect an account**

   Choose **GitHub** from **Connect**, or press the **GitHub** tile. With no account connected yet, GitHub opens. Install the app there and pick repositories. You come back to **Sources**.

2. **Add a repository**

   Choose **GitHub** again. **Add repository** opens. Find the repository with **Search repositories**, and press **Import**.

3. **Set the fields**

   Pick a **Branch** and a **Folder**, fill **Published at** if you want links, and press **Add**. The first sync starts at once.

| Field | Default | Notes |
| --- | --- | --- |
| **Branch** | The repository's default branch | The default branch is followed even if it is renamed later |
| **Folder** | **Sync the whole repository** | Each folder shows how many markdown and spec files it holds |
| **Published at** | Empty | Optional. Where the files are published, so the agent can link them. See [Let the agent link your docs](/articles/let-the-agent-link-your-docs) |

- **Connect another** under **GitHub accounts** adds a second GitHub account or organization. The account menu in the repository picker has **Connect another account** too.
- The picker lists recently active repositories. Type a full `owner/repo` to reach one the list does not show.
- **Missing a repository? Add access on GitHub** opens the app's settings on GitHub.
- One repository can be added more than once with different folders. The same repository and folder twice returns "That repository is already connected."

### What a sync takes

| File | Becomes |
| --- | --- |
| `.md`, `.mdx`, `.markdown` under the folder | One document per file. The title is the frontmatter `title`, then the first heading, then the file name |
| An OpenAPI or Swagger spec | One document per endpoint, plus an overview. See [API specs](#api-specs) |

- A sync compares every file with the last sync. Unchanged files are skipped. Changed files are indexed again.
- A file deleted from the repository leaves the library.
- A new file joins the library folder its neighbours in the repository are in.
- Markdown files over 900 KB are skipped.
- Documents whose indexing failed are retried on every sync.
- Synced documents are edited in the repository. The document page shows a **Read only** badge with the tooltip "Edit this document in its source repository."

### Repository rows

Each row is named `owner/repo`. The detail line has the folder and branch when set, and the published address with how many documents it could link, like `docs.example.com, 40 of 42 linked`.

| **Last sync** | Means |
| --- | --- |
| **Syncing** | A sync is running, including the first one after you add it |
| `5m ago` | The last sync finished |
| **Failed** | The error shows under the row |

| Control | Does |
| --- | --- |
| **Sync now** | The button on the row. Starts a sync. Disabled while one is running |
| **Published docs** | In the row's menu. Opens the address dialog. See [Let the agent link your docs](/articles/let-the-agent-link-your-docs) |
| **Remove** | In the row's menu. Asks **Remove owner/repo?**, then removes the repository and its documents |

**GitHub accounts**, under the tiles, lists every account Median reads through, with how many repositories each one serves. **Disconnect** asks **Disconnect this account?**, then removes every repository and docs site read through it, and their documents. Uninstalling the app on GitHub, or taking a repository's access away there, does the same.

### Sync errors

| Error | Fix |
| --- | --- |
| More than 500 markdown files in there. Point the sync at a docs folder. | Remove the repository and add it again with a folder |
| This docs site has more than 500 pages, which is more than a sync can take. | The site grew past 500 pages. Remove it and add the repository as a plain repository with a smaller folder |
| This repository is too large to sync. Select a documentation folder instead. | GitHub returned a partial file tree for the branch. The whole branch is read even when a folder is set, so a folder does not clear this |
| GitHub could not find that. Check the repository and branch, and that the app can see it. | The branch is gone, or the app lost access |
| GitHub turned us away. Check the app's key, and that it is still installed. | The app was uninstalled or lost permission |
| GitHub is not answering right now. Try again in a bit. | GitHub was down. Press **Sync now** later |
| Could not sync this repository. Try again. | Anything else |

A sync that runs longer than 10 minutes is treated as stuck. The next sync takes over.

| Connect error | Cause |
| --- | --- |
| This connection attempt expired. Connect again. | More than an hour passed on GitHub |
| That GitHub installation is connected to another organization. Disconnect it there first. | One GitHub install belongs to one Median organization |

## API specs

Repositories and docs sites turn OpenAPI specs into documents. Each endpoint document has **Parameters**, **Request body**, **Responses** and a `curl` command under **Example request**. The overview document lists the version, **Base URL**, **Authentication** and every endpoint.

Each spec can also go on your site as an [API reference](/articles/set-up-your-public-site#api-reference).

A file is read as a spec when all three tests pass:

| Test | Passes |
| --- | --- |
| Extension | `.json`, `.yaml` or `.yml` |
| Name or folder | The name contains `openapi` or `swagger`. Or the name has `api` as its own word, bounded by the start or end or by `-`, `_` or `.`, so `api.json` and `public-api.yaml` pass and `rapid.json` does not. Or a parent folder is named `openapi`, `swagger`, `spec`, `specs` or `apis` |
| Contents | It parses, has an `openapi` or `swagger` version key, and has a `paths` object |

A file that fails the contents test is ignored.

| Limit | Value |
| --- | --- |
| Spec files read per sync | 25 |
| Spec file size | 5 MB |
| Endpoints per spec | 300. The rest are left out |
| Versions | OpenAPI 3.x and Swagger 2.0 |

Specs must sit under the synced folder. A docs site also takes specs its config names, wherever they sit.

## Docs sites

A docs site is a GitHub repository read through its framework's config. The config sets which files are pages and the address each page publishes at. It uses the same GitHub accounts as the **GitHub** source.

Choose **Docs site** from **Connect**, or press the **Docs site** tile. With no GitHub account connected yet, GitHub opens first. Once you are back on **Sources**, choose **Docs site** again. **Add docs site** opens, with three steps.

| Step | What you do |
| --- | --- |
| **Repository** | Pick the repository. **Continue** |
| **Framework** | Check the **Branch**. Median reads the branch and shows the **Framework** with its config file, and **Pages** with any API specs. **Continue** |
| **Address** | Fill **Published at**. It is required. Three sample files show the address each will publish at, and update as you type. **Add docs site** |

A Blume or Docusaurus config that names its site fills **Published at** for you.

| Framework | Config | Pages and routes |
| --- | --- | --- |
| Mintlify | `docs.json` or `mint.json` | Only pages in the navigation, at the route the navigation gives. Specs from any `openapi` field |
| Docusaurus | `docusaurus.config.js`, `.ts`, `.mjs`, `.cjs` or `.mts` | Files under `docs.path`, default `docs`. Published under `routeBasePath`, default `docs`. Number prefixes like `01-` are dropped. `url` plus `baseUrl` fill the address. Specs from `specPath` |
| Fumadocs | `source.config.ts`, `.js`, `.mjs` or `.mts` | Files under `defineDocs({ dir })`, default `content/docs`. Published under the folder holding the `[[...slug]]` route, default `docs` |
| GitBook | `.gitbook.yaml` or `SUMMARY.md` | Only pages the summary links. `root` and `structure.summary` are read from the yaml |
| Blume | `blume.config.ts`, `.js`, `.mjs`, `.mts` or `.json` | Files under `content.root`, default `docs`. `deployment.site` fills the address. Specs from `openapi` when `enabled: true` |

- `index` and `README` files publish at their folder's route, except on Mintlify.
- A route prefix is not added twice when **Published at** already ends with it.
- When a repository holds several configs, GitBook is tried last.
- Median reads configs as text and does not run them. Check the three sample addresses before you add the site.

| Situation | What you see |
| --- | --- |
| No config found | **Not recognised**, "Median reads Mintlify, Docusaurus, Fumadocs, GitBook and Blume." and a **Sync it as a plain repository** link that opens **Add repository** |
| Over 500 pages | "This site has more than 500 pages, which is more than a sync can take." **Continue** stays disabled. Add it as a plain repository with a smaller folder instead |
| Config names more than 100 API specs | "This site exceeds the page limit for a single sync." |

The site row is named for the address it publishes at. Its detail line has `owner/repo`, the framework, the content folder and the branch. It has the same columns, button and menu as a repository row. **Sync now** on a docs site reads the config again even when the branch has not changed. Unchanged files are still skipped. Every sync that finds the repository changed reads the config again too, so a moved docs folder follows.

## Websites

Websites need a paid plan. Without one the dialog shows "Available on Standard and Pro." See [plans](/articles/manage-your-plan-and-credits#plans).

1. **Paste an address**

   Choose **Website** from **Connect**, or press the **Website** tile. In **Import a website**, type the address, for example `docs.example.com`. `https://` is added when missing.

2. **Import**

   Press **Import**. The site gets a row on **Sources**, and pages land in the library as they are scraped.

- The crawl stays under the address you give. `example.com/docs` does not read `example.com/blog`.
- It reads the site's sitemap and follows links. PDFs are read too.
- Each scraped page is charged in credits. A crawl stops at 500 pages, or sooner when credits run short.
- Each page becomes a document linked to the address it was scraped from.

Each address gets one row, however many times it was crawled. The newest crawl speaks for it. While it runs, the row opens into three stages.

| Stage | Shows |
| --- | --- |
| **Discover** | Pages found |
| **Scrape** | Pages scraped, as `12 of 40` |
| **Index** | Pages indexed. Starts once scraping ends |

| Part | Shows |
| --- | --- |
| Detail line | `Website · Started by` and the name of whoever started it. A skipped count for pages that came back blank or too large, and a kept count for pages you edited, when there are any |
| **Documents** | Pages from this site in the library |
| **Last sync** | **Importing** while it runs, **Failed** with the error under the row, or how long ago it finished |

| Control | Does |
| --- | --- |
| **Re-scrape** | The button on the row. Fetches live copies and updates pages in place. Pages you edited keep your version. Disabled while a crawl runs |
| **Cancel import** | In the row's menu while a crawl runs. Stops it. Pages already scraped stay and are indexed |
| **Remove** | In the row's menu otherwise. Removes the row. The pages stay in the library |

- Nothing refreshes on a schedule. Press **Re-scrape** when the site changes.
- A first crawl may use copies up to 2 days old. A re-scrape, or a new crawl of an address you scraped before, fetches live pages.
- The list holds the 20 most recent crawls.

| Error | Cause |
| --- | --- |
| That does not look like a site address. | Not a web address |
| That site is already being scraped. | A crawl of that address is running |
| Up to 3 sites at a time. Let one finish first. | 3 crawls are running |
| Nothing readable came back from this site. | No page had content |
| The crawl took too long, so it was let go. Try again. | The crawl ran for 60 minutes |
| The page import service is busy. Try again in a minute. | The scraper was busy |
| The scraped pages could not be indexed. Try the site again. | Indexing failed after the scrape |
| Your organization is out of credits. Add more in Settings, Billing. | No credits were left when the crawl started |

Other failures say to try again later.

## From the API

| Request | Does |
| --- | --- |
| `POST /v1/knowledge/sync` | Same as **Sync all** |
| `POST /v1/integrations/repos` | Adds a repository with `repo`, `branch`, `path` and `publishedAt` |
| `PATCH /v1/integrations/repos/{owner}/{repo}` | Sets **Published at** |
| `DELETE /v1/integrations/repos/{owner}/{repo}` | Stops syncing a repository. Its documents stay in the library, unlike **Remove** in the dashboard |
| `GET` and `POST /v1/knowledge/crawls` | Lists crawls or starts one with `url` |
| `DELETE /v1/knowledge/crawls/{id}` | Cancels or dismisses a crawl |

Full schemas are in the [management API reference](https://docs.median.sh/reference/manage).
