Back to blog
Industries13 min read

Migrating a Notion Prompt Database to a Real Prompt Tool

How to migrate Notion prompts into Prompt Architects: what Notion's CSV and Markdown export actually produces, what breaks, and the exact file format the import expects.

NH
Nafiul Hasan

TL;DR: To migrate Notion prompts into Prompt Architects, export the database as Markdown & CSV, then convert the rows into the app's own JSON shape, since there is no direct Notion import. Your real prompt text may sit in the CSV or in a separate per-row Markdown file, {{variables}} need lowercase names to register, and prompts over your plan's character cap get skipped, not trimmed.

Already deciding whether to leave Notion at all, rather than doing it? That comparison, with a table and a checklist, lives in Prompt Architects vs a Notion Prompt Database. This post assumes you're past that: your prompts are in a Notion database today, one row per prompt, and you want them in a tool that can actually use them, intact, without re-typing two hundred prompts by hand. If that database has quietly turned into a spreadsheet with extra steps, the mechanics below are the same regardless of which broke first.

What Are You Actually Moving: a Property, or a Page?

This is the single question that determines whether the rest of this migration is easy or annoying, and almost nobody checks it before they start exporting.

A Notion database row is really two different containers stacked on top of each other. The properties are the columns running down the side, Category, Status, Target Model, whatever you set up, plus a Text property if you put the prompt itself in a column. The page body is everything underneath the row when you click into it, a separate canvas that can hold paragraphs, headings, callout blocks, even a sub-page of its own.

Short prompts usually live entirely in a property. Longer prompts, especially anything with a system-prompt preamble plus a few examples, tend to end up in the page body instead, because a Notion Text property gets cramped fast. Open three or four of your own rows before you export anything and check which one you actually did. The two cases produce a completely different export, and the fix for each is different too.

It's also common to find a database that mixes both, a short one-liner property for browsing the table, and the full prompt underneath in the body. If that's your setup, the property is almost always the wrong field to migrate on its own: it's a summary, not the thing you'd actually paste into ChatGPT. Check one row end to end, property and body, before you decide which field becomes your content value. Guessing wrong here means importing two hundred summaries instead of two hundred prompts, and nothing in the import step will warn you, because a short summary is still valid content as far as the parser is concerned.

How Do You Export a Notion Database Correctly?

From the database's own page, open its ••• menu and choose Export. Pick Markdown & CSV from the format dropdown (the other two options are PDF and HTML; neither helps here), and switch on the option to include subpages if you want the individual page bodies as well as the summary table. Confirm, and Notion downloads a zip file to your computer.

Two more things worth knowing before you click export, both from that same help page:

  • You can only export the current view or the default view, not every view at once. If your database has several filtered views (Active, Archived, by Category), pick the one that actually contains everything you want, and count the rows in the resulting CSV afterward rather than assuming a filtered view included what you expected.
  • A callout block exports as raw HTML inside the Markdown file, "as there is no Markdown equivalent." If you ever flagged a prompt's system-prompt section with a colored callout box, that box survives as literal <div>/<aside>-style HTML tags sitting in an otherwise plain-text file, not as a heading or a blockquote.

Unzip the download and you'll see one .csv file (your database's properties) and, if you included subpages, a folder of .md files, usually named after each row's title, holding whatever lived in that row's page body.

Open the CSV in a spreadsheet app before you do anything else with it. Notion's help center puts it plainly: "You can open the CSV files corresponding to your databases in Excel, Numbers, etc. to view your data." That ten-minute look is what tells you which columns actually have your prompt text in them versus which ones are empty because that data lived in the page body instead. A multi-select property like Tags or Variables Used usually exports as one cell with values separated by a comma or semicolon, not as separate columns, which matters in the next section: your import file needs an actual array, not that one joined string.

Can You Just Drag That Export Into Prompt Architects?

No, and it's worth saying plainly rather than letting anyone assume otherwise. Verified directly from this app's own code, not guessed: the Library page has an Import button, and the file input behind it only accepts .json. The parser that reads the file (parseLibraryExportFile in lib/library-export.ts) checks one field before it does anything else:

if (obj.format !== LIBRARY_EXPORT_FORMAT) {
  return {
    ok: false,
    error: "Unrecognized file format — not a personal-library export.",
  };
}

LIBRARY_EXPORT_FORMAT is the literal string "prompt-architects/personal-library". A Notion CSV, a Notion Markdown file, or a hand-written JSON file missing that field is rejected the same way, immediately, before a single row is looked at. There is no Notion connector, no generic CSV importer, and no partial-credit parsing of a differently-shaped file. The exact file the import expects looks like this:

{
  "format": "prompt-architects/personal-library",
  "version": 1,
  "exported_at": "2026-09-03T10:00:00.000Z",
  "count": 2,
  "items": [
    {
      "title": "Weekly Investor Update",
      "description": "",
      "content": "Summarize this week's progress into a 150-word investor update covering {{metric_1}} and {{metric_2}}, plus one risk and one ask.",
      "negative_prompt": "",
      "category": "founder-operations",
      "tags": ["investors", "weekly"]
    },
    {
      "title": "Client Onboarding Email",
      "description": "",
      "content": "Write a warm onboarding email for {{client_name}} introducing our {{product_name}} plan and the first three steps they should take.",
      "negative_prompt": "",
      "category": "copywriting-social",
      "tags": ["client_name", "product_name"]
    }
  ]
}

Getting your Notion export into that exact shape is the actual work of the migration. Everything else in this post is about doing that mapping correctly, once, instead of rebuilding it row by row from memory.

Turning Your Notion Export Into That JSON File

Line up the fields side by side before you write anything:

Notion sourceImport fieldNote
Row titletitleTrimmed to 100 characters on import
A short property, if you kept onedescriptionOptional; the UI shows it in list view, the AI never sees it
The Text property or the matching .md file bodycontentThis is what gets sent to the model; see the previous section
An avoid property, if you have onenegative_promptOptional, capped at 2,000 characters, truncated rather than rejected
Category / Type selectcategoryMust match a real Prompt Architects category id or it silently becomes All Categories
Multi-select / semicolon listtagsMust be a JSON array of strings, not one joined string

The category row is the one people get wrong most often. Prompt Architects ships around 40 fixed category ids (founder-operations, copywriting-social, developer-workflow, code-generation, and so on), and the import code checks your value against that exact list:

const validCategory = (c: unknown): string => {
  if (typeof c === "string" && c in LIBRARY_CATEGORY_MAP) return c;
  return DEFAULT_LIBRARY_CATEGORY.id || "";
};

DEFAULT_LIBRARY_CATEGORY is the id all, which the app displays to you as All Categories. Nothing errors if your Notion Type column says Marketing or Founders, neither is a recognized id, so both fall straight through that return line and every one of those rows lands in the same uncategorized bucket, which defeats the entire point of migrating a categorized database. Map your own values to real ids before you build the file, not after you've imported four hundred rows into one bucket.

A short conversion script does this cleanly. Assuming your prompt text lives in the CSV column (the simple case from the first section):

import csv, json, re
from datetime import datetime, timezone

CATEGORY_MAP = {
    "founders": "founder-operations",
    "marketing": "copywriting-social",
    "engineering": "developer-workflow",
}

def slugify_var(name: str) -> str:
    # Notion placeholders are free text; Prompt Architects only reads
    # lowercase snake_case as a live variable, e.g. {{ClientName}} -> {{client_name}}
    return re.sub(r"[^a-z0-9_]", "_", name.strip().lower())

items = []
with open("prompt-library.csv", newline="", encoding="utf-8") as f:
    for row in csv.DictReader(f):
        content = row["Prompt Body"]
        for raw_name in row.get("Variables Used", "").split(";"):
            raw_name = raw_name.strip()
            if raw_name and raw_name != slugify_var(raw_name):
                content = content.replace(f"{{{{{raw_name}}}}}", f"{{{{{slugify_var(raw_name)}}}}}")
        items.append({
            "title": row["Prompt Name"][:100],
            "description": "",
            "content": content,
            "negative_prompt": "",
            "category": CATEGORY_MAP.get(row["Category"].strip().lower(), ""),
            "tags": [t.strip() for t in row.get("Variables Used", "").split(";") if t.strip()],
        })

export = {
    "format": "prompt-architects/personal-library",
    "version": 1,
    "exported_at": datetime.now(timezone.utc).isoformat(),
    "count": len(items),
    "items": items,
}
with open("import-ready.json", "w", encoding="utf-8") as f:
    json.dump(export, f, indent=2)

If your prompt text is in the per-row Markdown files instead of the CSV, the same script needs one more step: read the matching .md file for each row (Notion names them close to the row title) and use its body as content, falling back to the CSV cell only if no file matches. Skip the file's own title heading, since that duplicates what's already in title.

What Happens to {{Variables}} and Templates in the Move?

Plain text survives the move without any help. If your Notion property literally contained {{client_name}}, that string is still {{client_name}} after export, after conversion, and after import. Nothing strips or mangles curly braces anywhere in this path.

What doesn't survive automatically is recognition. Prompt Architects reads a prompt template's placeholders with one fixed pattern: two curly braces, then a name that starts with a lowercase letter and continues with lowercase letters, digits, or underscores. {{client_name}} qualifies. {{ClientName}}, {{Client Name}}, and {{client-name}} do not; they sit in the imported prompt as inert text until someone renames them.

Importing a batch of items also does not create any Global Variable records on its own. A {{client_name}} placeholder becomes a live, fillable slot the moment you open that prompt in the enhancer, whether or not you've ever defined a value for it anywhere. Giving it a stored default value, so it's pre-filled next time, is a separate, one-time step per variable, done from the Variables tab, not something the library import touches. Names there cap at 80 characters and descriptions at 1,000, flat across every paid plan; which plans include the Variables feature at all isn't published on the pricing page, so check your own account rather than assuming a limit.

Notion's own database templates, the one-click start every new row as Draft with Category pre-filled feature, don't migrate at all, and there's genuinely nothing to convert: a template is an authoring shortcut inside Notion, not data attached to a row, so it was never part of the export in the first place. Prompt Architects has an unrelated, pre-built Template Library of its own; importing your prompts doesn't touch it, and it isn't meant to replace what Notion's templates were doing for you.

If your team built variable reuse into a shared workflow rather than one person's habit, post 59 covers that pattern directly, separate from the one-time migration this post is about.

What Gets Silently Skipped on Import?

The import is generous about partial success, filling in what it can and reporting the rest, rather than rejecting the whole batch over one bad row. Four outcomes, all counted separately in the summary you see after clicking Import:

  • Imported. Landed in your library as a new private item.
  • Skipped as duplicate. Same title, content, and negative prompt as something already in your library, or as an earlier item in the same file. Safe to re-run the same import twice; you won't end up with two copies.
  • Skipped over your plan's limit. Your library was already at capacity, so the remaining rows in the file are left out but not deleted anywhere; they're still sitting in your JSON file if you upgrade or clear space later.
  • Skipped as invalid. An empty title, an empty body, or content over your plan's character cap. This is the one worth watching for a Notion migration specifically: the cap runs from roughly 8,000 characters on Free and Pro up to 25,000 on the highest tiers (verified from constants/library-limit.ts), and a long system-prompt-plus-examples page from Notion can clear that without you noticing until the summary says so. Oversized content is skipped whole, never silently cut down to fit.

How Do You Verify the Migration Actually Worked?

Don't treat the import said success as the finish line. Four checks, in order:

  1. Read the summary counts. Imported, duplicate, over-limit, invalid, they should add up to the number of rows you started with. If they don't, something in your conversion script produced malformed items that got filtered before the count.
  2. Spot-check a few prompts that used callout blocks in Notion. If any HTML tags rode along inside the content field, they'll now render literally inside the library's Markdown view, or worse, get sent to the AI model as part of the prompt text. Strip them from the source before you build the JSON, not after.
  3. Rebuild any relation or rollup column by hand. Notion's own docs don't document what those columns turn into inside an exported CSV cell, only how they behave on the way back in. Don't assume; open the actual CSV and look.
  4. Re-test every {{variable}} in the enhancer, not just the ones you remember using. A placeholder that silently failed the casing check earlier still looks fine sitting in a prompt; the enhancer's variable panel is where the failure actually becomes visible.

Once your library is in place, the natural next step is protecting it the same way you'd have wanted to protect the Notion database: post 633 covers the built-in export, plus two heavier options, for exactly that.

Free Chrome Extension

Stop rewriting prompts. Start shipping.

Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 4.8★ on the Chrome Web Store.

Create An Account

Frequently asked questions

Free Chrome Extension

Stop rewriting prompts. Start shipping.

Works with ChatGPT, Claude, Gemini, Grok, Midjourney, Ideogram, Veo3 & Kling. 4.8★ on the Chrome Web Store.

Create An Account