Every "AI made me 500 flashcards" workflow dies at the same place: the file imports, the toast says N notes added, and then you open the deck and find the answer sitting in the front field, half the cards truncated at the first comma, and accents turned into é.

Anki's text importer is documented, but the docs are spread across a long page and the defaults are guesses. The fix is not a better generator — it is a header block at the top of the file that removes every guess Anki makes.

This post is the short version I wish I had: what the importer actually parses, the five ways a good file goes bad, and a ~30-line Python builder that produces a file you can re-import every week without duplicates.

What Anki actually parses

Three rules explain almost every broken import.

  1. The field count comes from the first non-commented line. If a later row has more fields, the surplus is silently dropped. If it has fewer, the missing ones are blank. One unescaped tab in an answer doesn't error — it shifts everything.
  2. The file must be UTF-8. Not "UTF-8 with a BOM that Excel felt like adding". Excel's default CSV export is the single most common cause of mojibake in imported decks.
  3. Anki guesses the separator. Usually correctly, occasionally not. Guessing is what the header block removes.

Lines beginning with # are directives, not data. Since Anki 2.1.54 they can set the whole import:

Header Value Effect
#separator: tab, comma, semicolon, pipe, … Forces the field separator
#html:true true / false Fields are treated as HTML (needed for <br>)
#columns: names separated by the separator Pre-names the columns in the import screen
#notetype: note type name or id Presets the note type
#deck: deck name or id Presets the deck
#tags column: column number Which column holds per-row tags
#notetype column: column number Which column holds each row's note type
#guid column: column number Which column holds a stable ID for duplicate matching

Two ordering rules that bite people: #separator: must come before #columns: (the column names are split on the separator you just set), and #tags: tags the entire file while #tags column: tags each row.

Five ways a good file goes bad

1. A tab inside a field. The separator is consumed, the row gains a field, and the answer lands in the wrong place. Commas inside prose do the same thing when your separator is a comma. Fix: pick a separator, then strip it from every field before writing.

2. A line break inside a field. Anki has two escape routes — quote the whole field, or use <br> with #html:true. The quote route breaks down for cloze notes that span lines (the manual is explicit about this), so for anything generated I use <br>.

3. HTML on, angle brackets in the content. With #html:true, < starts a tag. Code snippets, math like x < 5, and & all need escaping to &lt;, &gt;, &amp;. If you don't need formatting, set #html:false and skip the whole problem — but then real newlines are not representable, so you're back to rule 2.

4. Encoding. Write bytes as UTF-8 without a BOM. If you built the file in Excel, re-open it in LibreOffice and save as CSV with UTF-8. This is not a nitpick: it silently corrupts every non-ASCII character, which is most of the world.

5. Re-import creates duplicates. By default Anki uses the first field to decide whether a note already exists, so a regenerated deck with slightly different wording duplicates everything. Give each row a stable ID (source URL + paragraph index, or a hash of the front) and declare #guid column:N. Now re-importing updates instead of duplicating.

The builder

This writes a file that survives all five failures. rows is an iterable of (front, back, tags, guid).

import html import io SEP = "\t" def field(text: str) -> str: """Make one cell safe for a tab-separated, HTML-enabled file.""" text = html.escape(text or "", quote=False) # & < > text = text.replace("\r\n", "\n").replace("\r", "\n") text = text.replace("\n", "<br>") # HTML line break text = text.replace(SEP, " ") # never emit the separator return text.strip() def build(rows, path="deck.txt", deck="Inbox", notetype="Basic"): with io.open(path, "w", encoding="utf-8", newline="\n") as f: f.write("#separator:tab\n") f.write("#html:true\n") f.write("#notetype:%s\n" % notetype) f.write("#deck:%s\n" % deck) f.write("#columns:Front%sBack%sTags%sGUID\n" % (SEP, SEP, SEP)) f.write("#tags column:%d\n" % 3) f.write("#guid column:%d\n" % 4) for front, back, tags, guid in rows: f.write(SEP.join([ field(front), field(back), field(tags), field(guid), ]) + "\n") 

Two things worth pointing out. html.escape(..., quote=False) leaves double quotes alone — that's intentional, because with a tab separator and no quoting, quotes are just characters. And field() strips the separator before it can ever split a cell; that one line is the difference between 500 cards and 370 cards plus 130 quiet mistakes.

Call it:

rows = [ ("What does the tunica media contain?", "Smooth muscle\nElastic fibres", "histology::vessels", "src-a1-p3"), ] build(rows) 

Test with five cards, not five hundred

Import into a throwaway profile (File → Switch Profile → Add) before touching your real collection. Five cards is enough to catch everything above:

  • one card with a two-line answer
  • one with a comma, one with a tab character in the source text
  • one with non-Latin characters or accents
  • one with <, > or & in the text
  • one with an empty optional field

Then check the note count after import, spot-check the back of each, and import the same file a second time — if the count doubled, your GUID column isn't doing its job.

When you'd rather not produce a file at all

The file route has one hard limit: it's a snapshot. Re-reading a chapter means regenerating and re-importing, and field mapping lives in your head.

For the PDF → cards case I ended up building a browser extension that skips the file entirely: it reads a text-layer PDF in the browser, generates cards, lets you edit them in a side panel, and writes into the deck through AnkiConnect — or copies a finished TSV to the clipboard if you'd rather import by hand. I'm the developer, so treat the recommendation accordingly; the format notes above are the part that applies no matter which tool you use.

If you want the longer version of the four routes (including what to do with scanned PDFs that have no text layer), it's here: PDF to Anki: four routes, one skips the export.

The one-line summary

Anki's importer is not fragile — it is literal. Declare the separator, the HTML mode, the note type, the deck, the tags column and a GUID column in the first six lines, escape & < > and newlines in every field, and write UTF-8 without a BOM. That's the whole trick, and it turns a one-off experiment into a script you can run again next week.