Importers: moving in from another tool
Every importer we have, for every space type, on one page. Each section says what the importer takes, where you run it, what lands where, whether it has a dry run, what a second run does, and which credentials it needs.
Three rules hold almost everywhere, and the exceptions are called out:
Check first. Most importers take
dryRun: true(the console calls it Check first or Check (dry run)). It reads everything and writes nothing, and tells you what would happen.A second run is safe. Importers match what they already brought in, by slug, version, address or source id, and skip or update it. Running one again a month later brings in only what is new.
Your credentials are used once. When an importer pulls live from another product with your API key or token, the key is used for that one request and is never stored, logged or shown again. A sync that keeps running (careers ATS, a Git connection) is the exception: it has to store its key, and it keeps it encrypted and write-only.
You are moving from | Into | Section |
|---|---|---|
GitBook, Notion, Confluence, MkDocs, Mintlify, ReadMe, Docusaurus, a Git repo, Word | Docs | |
Zendesk, Intercom, Help Scout | Docs or FAQ | |
Statuspage, Instatus, Better Stack | Status | |
WordPress, Ghost, Substack, Medium, one article by URL | Blog | |
Keep a Changelog, GitHub Releases, Beamer, Headway, LaunchNotes, Canny, Featurebase | Changelog | |
Canny, Featurebase | Roadmap | |
Discourse, phpBB, vBulletin, Flarum, Discord | Community spaces | |
Luma, Eventbrite, Meetup, any calendar ( | Events | |
Teachable, Thinkific, a CSV outline; quizzes as CSV or Moodle GIFT | Courses | |
Webflow (export ZIP or live site), Framer (live site) | Custom pages | |
Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Personio, Recruitee, Teamtailor | Careers | |
Another Forumulate organization | Anything |
Docs
The docs importers are covered in full in Importing documentation: the ZIP, URL (GitHub), JSON and Word doors, the source, onExisting, prune and dryRun options, and what each tool's syntax turns into.
source names the tool the export came from. It is detected from signature files, and you can set it when the export left its config file out:
| Detected by | What it changes |
|---|---|---|
|
|
|
|
| The navigation (tabs, anchors, dropdowns, versions, languages and groups) sets the page order. |
| an |
|
|
| Each folder's |
| the export's file names | Notion's id-suffixed file names and Confluence's page layout are cleaned up. |
| everything else | Markdown and HTML as they are. |
curl -X POST "https://forumulate.com/api/orgs/$ORG/spaces/$SPACE/docs/import/zip" \
-H "Authorization: Bearer $FORUMULATE_TOKEN" \
-F file=@mintlify-docs.zip -F source=mintlify -F dryRun=trueIn the studio, the docs Import button has a picker for the source. Needs docs.write. No external credentials.
Help desks: Zendesk, Intercom, Help Scout
POST /orgs/{orgId}/spaces/{spaceId}/docs/import/helpdesk brings a help centre into a docs or FAQ space, in either of two ways.
Pasted export. Send provider and data, the export as the help desk's API answers it (an object, or its text):
{ "provider": "zendesk", "data": { "categories": [], "sections": [], "articles": [] }, "dryRun": true }Live pull. Send credentials and we fetch the help centre ourselves:
Provider |
|
|---|---|
Zendesk |
|
Intercom |
|
Help Scout |
|
curl -X POST "https://forumulate.com/api/orgs/$ORG/spaces/$SPACE/docs/import/helpdesk" \
-H "Authorization: Bearer $FORUMULATE_TOKEN" -H "Content-Type: application/json" \
-d '{ "credentials": { "provider": "intercom", "token": "'"$INTERCOM_TOKEN"'" }, "dryRun": true }'What maps to what.
Source | Becomes |
|---|---|
Zendesk categories and sections, Intercom collections (and legacy sections), Help Scout collections and categories | Collections, nested as they were. A collection with the same slug is reused. |
Published articles | Pages in those collections. The HTML body is converted to blocks, with images and links handled as in every docs import. |
Labels, keywords | Tags (up to 20 per page). |
Help Scout's article slug | The page's slug, so old paths match. |
Drafts and unpublished articles | Nothing. An import publishes what it lands, so we leave unfinished work behind. |
Re-runs update pages in place by slug; nothing is duplicated. Limits: a live pull reads at most 2,000 articles (40 pages of the provider's API). A larger help centre is imported in passes, by section. A Zendesk subdomain must be a plain label (the acme of acme.zendesk.com), and pagination follows only links on the provider's own host. The answer gives collections, articles, created, replaced, failed and dryRun.
In the studio: the import panel, From Zendesk, Intercom or Help Scout (provider, connect or paste, dry run). Needs docs.write.
Status pages
POST /orgs/{orgId}/spaces/{spaceId}/status/imports copies a Statuspage, Instatus or Better Stack page into a status space. It runs in the background and answers 202. Poll GET …/status/imports/{importId} until it finishes.
{
"source": "statuspage",
"url": "status.acme.com",
"pageId": "abc123",
"apiKey": "…",
"incidents": true,
"subscribers": false
}Field | Default | What it does |
|---|---|---|
| required |
|
| required | The page as a reader opens it. Without a key, everything is read from what the page publishes. |
| none | The page's id in the source's API, and a key. With both, the full incident history is read (Statuspage's public list stops at fifty), and Instatus and Better Stack incidents can be read at all. |
|
| Bring the incident and maintenance history with its updates. |
|
| Statuspage with a key only: bring its active email subscribers, already confirmed. |
What maps to what. Groups are added after the ones the page already has, in the source's order. Each component becomes a manual component, or is matched to an existing component with the same name (at most 500 components). Incidents keep their severity, state, dates, affected components and every update. Nothing the import writes notifies anyone: no email, no channel post, no webhook for the history.
Re-runs: each incident is brought in once and remembered by its source id, so a second run adds only new incidents. The summary counts groups, components, incidents, subscribers and skipped.
In the console: the status manager's Import a status page card. Needs space.update. The key is the customer's own, used for this run only.
Two related doors take people lists. POST …/status/subscribers/import takes a CSV of email[,components]. It skips duplicates, and a bad line refuses the whole file with its line number. See Status pages.
Blog
POST /orgs/{orgId}/spaces/{spaceId}/blog-import brings posts over from another blog platform.
| Send | Read from it |
|---|---|---|
|
| Published posts only (not pages, not drafts). |
|
| Published posts only. |
|
| Published posts, without threads. |
|
|
|
|
| That one page. |
Each post keeps its title, slug, original date, tags (up to 12), excerpt, cover image, author (as the byline) and, where it was published before, its canonical URL. publish: true publishes what it brings in, at the original dates. Publishing an import mails nobody, because those subscribers read the posts when they first came out. When publish is not set, the API keeps the posts as drafts. The console's panel has the switch on by default.
Dry run: dryRun: true lists each post as would-write or skipped. Re-runs: a post whose slug this blog already has is skipped. Limits: one call writes at most 100 posts. When the answer says truncated: true, send the same call again to continue. data is up to 20 MB, files up to 5,000 files of 5 MB, and the endpoint takes 10 calls a minute. A url import fetches only public addresses.
In the studio: the blog's Housekeeping fold, Import. Needs blog.publish. No external credentials.
Newsletter subscribers come in separately. POST …/newsletter/import takes a CSV with an email column (and optional tags and name). In the default optin mode, each address is asked to confirm. subscribed skips the confirmation, and only with consent: true: your statement that these people already opted in somewhere else. See Blog memberships.
Changelog
POST /orgs/{orgId}/spaces/{spaceId}/changelog/import, one body discriminated on source:
| Send |
|---|---|
|
|
|
|
|
|
Fields are read from whichever column name the vendor used (title/name, markdownDetails, markdownContent, publishedAt/date, types/ categories…). A row with no title or no body is skipped rather than guessed at. Change types (New, Fixed, …) become the release's type, and products or segments become tags.
Field | Default | What it does |
|---|---|---|
|
| Publish at the releases' own dates. |
|
| Return each release with |
Re-runs: a release whose version the changelog already has (or, with no version, the same title on the same day) is skipped. Limits: 5,000,000 characters per export and 150 releases per call. When the answer says truncated, call again. Needs blog.publish. In the studio: Import releases. Details are in Blog and changelog.
Roadmap
POST /orgs/{orgId}/imports/roadmap turns a Canny or Featurebase export into a roadmap board. It is queued: the answer is 202 with a job, polled at GET /orgs/{orgId}/imports/{jobId}.
{ "source": "canny", "data": "<the CSV text, or the API JSON>", "space": { "name": "Roadmap", "slug": "roadmap" } }Source status | Lane |
|---|---|
open, under review |
|
planned |
|
in progress |
|
complete |
|
closed, rejected |
|
anything else |
|
Comments come with their posts, the ETA becomes the item's target and the category becomes its theme. Voters with an address become votes by those people. The rest of the source's count is kept as anonymous votes. A multi-board export makes one board per source board. Authors and voters are matched to members by email. An address with no account becomes a staged account, claimed when that person signs up.
Re-runs are idempotent by slug. There is no dry run. Import into a new board first if you want to look before it is live. Needs org.manage_members. See Roadmap spaces.
Forums
A forum import from Discourse, phpBB (3.0–3.3), vBulletin (4 or 5), Flarum or Discord runs as a job in three steps. A forum database dump is routinely hundreds of megabytes, so it is uploaded in parts.
# 1. Create the job: into an existing forum (spaceId), or a new one (space)
POST /orgs/{orgId}/forum-imports
{ "source": "phpbb", "spaceId": "…", "tablePrefix": "phpbb_", "oldBaseUrl": "https://forum.acme.com" }
# 2. Upload each file in parts of at most 8,000,000 characters (0, 1, 2…)
POST /orgs/{orgId}/forum-imports/{id}/files
{ "name": "dump.sql", "part": 0, "content": "…" }
# 3. Rehearse, read the report, then run it for real
POST /orgs/{orgId}/forum-imports/{id}/start { "dryRun": true }
GET /orgs/{orgId}/forum-imports/{id} # progress, then the report
POST /orgs/{orgId}/forum-imports/{id}/start { "dryRun": false }What each source takes.
Source | Upload |
|---|---|
| The backup |
| A mysqldump |
| A |
| A |
| A forum channel exported with DiscordChatExporter ( |
A .sql dump is read without a database server: MySQL INSERT statements with or without column lists, and pg_dump COPY. When your columns are named differently (a hand-made CSV, a renamed table), map them with columnMap, for example { "posts": { "post_text": "body" } }.
What maps to what.
From the old forum | Becomes |
|---|---|
Categories, sub-forums, channels | Categories, nested. |
Tags | Tags. |
Users | Members matched by email. An address with no account here becomes a staged account, claimed when that person signs up. A user with no email is credited by name. |
Discord members | One staged account each, keyed by their Discord id (Discord exports carry no email). |
Topics and replies | Topics and posts with their original dates. BBCode and HTML become blocks. |
Likes, thanks, reactions | Reactions. |
Accepted answer, pinned, closed, archived | The same states here. |
Images and attachments | Copied into the organization's files. Discourse uploads come from the backup: the dry run lists them in |
Old addresses ( | Permalinks that redirect to the new topics, categories, profiles and tags. On by default; |
Dry run. { "dryRun": true } parses and translates everything, then reports counts, problems grouped by reason, and the first 200 skipped rows, without writing anything. The job ends checked and keeps its files for the real run. A real run ends succeeded (files deleted) or failed (files kept, so you can start it again).
Re-runs. Run the same export again, or a newer one, and it skips what an earlier run imported. It adds only new topics, and new replies to topics it already brought in. That lets you import once, keep the old forum open while you move, and run it again on cut-over day.
Staged authors. GET /orgs/{orgId}/forum-imports/people?spaceId=… lists the staged accounts this forum's imports made, with post counts. POST /orgs/{orgId}/forum-imports/claims with { "fromUserId", "to" } (a member's username or email) hands one to a member. Their topics, posts and likes move over, and reputation is recounted.
Limits. 8,000,000 characters per part, 1,500,000,000 characters per job across every file, and up to 64 tables in a columnMap. GET /orgs/{orgId}/forum-imports lists the 20 most recent jobs.
In the studio: the forum's Housekeeping fold, Import a forum, next to the permalinks panel. Needs org.manage_members, because an import stages an account for every author with an address. No credentials on the old forum are needed: you upload what it exported.
To bring in a forum from a format we do not read, write the organization import document instead. Its shape and author rules are in Importing a forum.
Events
POST /orgs/{orgId}/spaces/{spaceId}/events/import creates events from a calendar file:
{ "format": "ics", "content": "BEGIN:VCALENDAR…", "publish": false }
| Reads |
|---|---|
| Any calendar's |
| The CSV exports of Luma, Eventbrite and Meetup, or your own CSV with |
Each event keeps its title, start and end, zone, location, description and link. publish: true publishes them; by default they are drafts. The answer gives created and skipped, each skipped row with its line number and the reason. Limits: 2 MB per file and 500 events per file. There is no dry run and no duplicate check, so importing the same file twice makes the events twice. Import as drafts and review them. Needs forum.moderate on the events space. No external credentials.
Courses
Course imports are a preview, then save flow. The import call only reads the export and answers the outline. Nothing is written until you save that outline, which makes the import call itself the dry run.
POST /orgs/{orgId}/spaces/{spaceId}/course/authoring/import:
|
|
|---|---|
| Teachable's course JSON ( |
| Thinkific's course JSON ( |
| A CSV with |
The answer is { modules: [{ name, lessons: [{ title, body, video? }] }] }. Edit it if you like, then send it to POST …/course/authoring/outline to create the modules and lessons. Up to 5,000,000 characters. Needs docs.write.
POST …/course/authoring/quiz-import reads quiz questions the same way:
| Reads |
|---|---|
| Columns |
| Moodle's GIFT format: |
The answer lists the questions and every line it could not read (skipped, by line number), so nothing is dropped without a word. You save the questions in the quiz editor. Up to 1,000,000 characters. More on courses is in Courses spaces.
Sites: Webflow and Framer
A custom-pages space can take a site built elsewhere. Every page of the site becomes a page here.
Call | Takes |
|---|---|
| multipart |
|
|
A live site is read from its /sitemap.xml (both Webflow and Framer publish one). Without a sitemap, we follow the home page's links. At most 40 pages are read, only from the site's own origin, through our public-address-only fetcher with capped redirects.
What maps to what. Each page's title and meta description become the page's title and description. Its content becomes Markdown and then blocks, and its images are re-hosted where the export carries them. The home page becomes the collection's landing page, and other paths keep their folders. The site's own chrome stays behind: navigation, header, footer, cookie banners, scripts, forms and iframes. The custom-pages space draws its own. Pages land in the collection you name (default Site).
Both doors hand the result to the docs ZIP importer, so dry runs, links between pages and re-runs (which replace pages by slug) work exactly as in Importing documentation. Needs docs.write. In the studio: the custom space's Housekeeping fold, Import a site (Check first, then Import from the address). See Custom pages.
Careers
The careers ATS connection is a sync rather than a one-off import: it publishes a role page per posting and keeps them up to date every hour. Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Personio and Recruitee are read from their public job boards with no key. Teamtailor needs the company's API key, stored encrypted. Everything about it is in Careers spaces.
Which permission each one needs
Importer | Capability |
|---|---|
Docs, help desk, site |
|
Status page |
|
Blog, newsletter subscribers, changelog |
|
Roadmap, forum, organization import |
|
Events |
|
Course outline and quiz |
|
A personal access token works for every one of them, within its scopes and space limits. See API tokens, scopes and capabilities.
In the API
Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.
Blog Import
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Import posts from WordPress, Ghost, Substack, Medium or a URL |
|
Course Builder
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Create the outline in a space |
| |
| Create the AI outline in a space |
| |
| Import the authoring |
| |
| Create a bulk video in a space |
| |
| Create the copy from in a space |
| |
| Generate the caption |
|
Event Engage
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Create an attendee in a space |
| |
| Import the event |
| |
| Get the ticket in a space |
| |
| Create the mail in a space (ticket) |
| |
| Get the certificate in a space (events) |
| |
| Transfer the event |
| |
| List questions in a space (events) |
| |
| Create a question in a space (events) |
| |
| Create the vote in a space (events questions) |
| |
| List polls in a space |
| |
| Create a poll in a space |
| |
| Create the vote in a space (events polls) |
| |
| Replace the rating in a space (events) |
| |
| Create a photo in a space |
| |
| Upload the photo |
| |
| List booths in a space |
| |
| Create a booth in a space |
| |
| Create the lead in a space |
| |
| Check an attendee in at the door |
| |
| Update details in a space |
| |
| List invites in a space |
| |
| Create an invite in a space |
| |
| Delete the invite (spaces events) |
| |
| List blasts in a space |
| |
| Create a blast in a space |
| |
| Delete the blast |
| |
| List cohosts in a space |
| |
| Replace cohosts in a space |
| |
| Delete the cohost |
| |
| Update the question |
| |
| Update the poll |
| |
| List ratings in a space (events) |
| |
| Get the all in a space (events photos) |
| |
| Update the photo |
| |
| Replace the booth |
| |
| Delete the booth |
| |
| List submissions in a space |
| |
| Update the submission (spaces events) |
| |
| Create the mail in a space (submissions) |
|
Forum Import
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List forum imports |
| |
| Create a forum import |
| |
| List the staged members a forum import made |
| |
| Hand imported posts to a member |
| |
| Get a forum import |
| |
| Upload part of a forum export |
| |
| Remove an uploaded file from a forum import |
| |
| Start a forum import, or rehearse it |
|
Site Import
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Create the zip in a space (site import) |
| |
| Create the URL in a space (site import) |
|
Status Ops
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Import a status page from Statuspage, Instatus or Better Stack |
| |
| Get the import (spaces status) |
| |
| Pull a metric from a provider |
| |
| Add a channel that posts with a credential |
| |
| The organization's status integration accounts |
| |
| Replace the integration (spaces status) |
| |
| Delete the integration (spaces status) |
| |
| Get the oncall in a space |
| |
| Add a schedule of who is on call |
| |
| Replace the oncall |
| |
| Delete the oncall |
| |
| Publish a draft incident |
| |
| Write or publish an incident's post-mortem |
| |
| Draft the next update with AI |
|