You paste three pages of notes into a chatbot, ask for flashcards, and get 60 of them in two seconds. Then you spend an evening discovering that a third are vague, a few are wrong, and the file you saved won't import. The slow part of spaced repetition with AI isn't generating cards. It's making cards worth reviewing, and getting them into Anki without mangling them.
This post gives you three things: rules for what a good card looks like, a prompt that outputs structured notes, and a small Python script that converts those notes into an Anki-ready file and validates it. I ran the script on 2026-10-09 and the output below is real. I did not import the file into the Anki app itself in this session, so check the first import on a throwaway deck.
What makes a flashcard worth keeping?
Reading a good summary feels like learning. Pulling an answer out of your own head is what builds memory, which is why flashcards work when they ask a question you can actually fail. The retrieval-practice case is made in the study prompts post. Here, the question is what to demand of each card.
These are my working rules, not a published standard:
- One fact per card. If the answer is a list of five things, you'll recognise four and feel fine. Split it.
- A question with one defensible answer. "Tell me about HTTP 301" can be answered ten ways. "What does HTTP 301 mean?" can't.
- Short answers. If the back is a paragraph, it's a note, not a card.
- Sourced from your material. The model should write cards only from text you paste. Anything it adds from general knowledge is unchecked.
- Cloze for facts inside a sentence, Basic for question and answer. Anki's cloze syntax hides part of a sentence.
- No cards for things you haven't understood. A card can't teach you a concept. Understand first, then card.
Reject a card if you can't say where in your notes the answer lives. That check is the human step, and it's the one people skip.
How does Anki's import format work?
I checked Anki's own manual on 2026-10-09 (docs.ankiweb.net, text-file import and editing pages, paraphrased here, so confirm exact wording there). The points that matter:
- Only plain text works, and it must be UTF-8.
- Separators can be comma, semicolon, tab and a few others; Anki tries to detect it, and the
#separatorheader can force it. - From Anki 2.1.54, header lines written as
#key:valueat the top of the file can set#html,#tags column,#notetype column,#deck columnand#guid column. - With HTML on, use
<br>for a line break inside a field, and write<,>and&for literal angle brackets and ampersands. - A field containing the separator, a newline or a quote must be wrapped in double quotes, with any inner double quote doubled.
- The first field decides whether a note is a duplicate. By default a match in the same note type updates the existing note.
- Tags go in a tags column, space separated.
Two lines from the cloze section of the manual are worth keeping handy: the syntax is {{c1::text}}, and a hint goes after a second pair of colons, as in {{c1::Canberra::city}}. Each different number (c1, c2) produces its own card. I did not verify cloze behaviour with the multi-line quoting edge case beyond the manual's note that quoted multi-line content breaks multi-line clozes, so the script below converts newlines to <br> instead.
What prompt gets structured notes out of a model?
Ask for JSON, not for a "file". Models are decent at filling a schema and bad at remembering tab characters. This is an untested template; I did not run it on a live model for this post.
Below are my notes on [TOPIC]. Write flashcards using ONLY these notes.
Return a JSON array, nothing else. Each item:
{"type": "Basic" or "Cloze", "deck": "[DECK NAME]",
"front": "...", "back": "...", "tags": ["..."]}
Rules:
- One fact per card. Answers under 25 words.
- Use Cloze when the fact is a term inside a sentence: put {{c1::answer}}
in "front" and any extra context in "back".
- Use Basic otherwise: a question in "front", the answer in "back".
- If a fact is unclear or missing from the notes, skip it and list it in a
separate "skipped" message after the JSON, not as a card.
- Do not add facts that are not in the notes.
NOTES:
[PASTE]
Then read every card against your notes before importing. The model can also be asked a second time to act as a critic ("find cards that are ambiguous, have two facts, or are not supported by the notes"), which catches some problems but not all. See avoiding hallucinations for why you still check by hand.
The converter script, with real output
Here's the script I ran. It takes the JSON, writes the headered tab-separated file with HTML escaping and <br> for newlines, and then re-reads its own output to validate it. Checks: valid UTF-8, header intact, five columns per row, known note type, no empty first field, no stray <, cloze notes actually contain a cloze, and no duplicate first fields.
#!/usr/bin/env python3
"""Convert a notes list (JSON) into an Anki-importable text file, then validate it.
Usage: python3 notes_to_anki.py notes.json out.txt
Format follows Anki's manual (docs.ankiweb.net/importing/text-files.html):
UTF-8 plain text, header lines '#key:value', tab separator, HTML allowed,
fields with tabs/newlines/quotes wrapped in double quotes (quotes doubled).
Columns: 1 notetype, 2 deck, 3 field1 (Front / Text), 4 field2 (Back / Back Extra), 5 tags.
"""
import csv
import html
import io
import json
import re
import sys
HEADER = [
"#separator:tab",
"#html:true",
"#notetype column:1",
"#deck column:2",
"#tags column:5",
]
def to_field(text, is_cloze_front=False):
"""Escape for html:true. Literal < > & become entities; newlines become <br>."""
out = html.escape(text, quote=False)
return out.replace("\n", "<br>")
def build(notes, path):
buf = io.StringIO()
for h in HEADER:
buf.write(h + "\n")
w = csv.writer(buf, delimiter="\t", quoting=csv.QUOTE_MINIMAL, lineterminator="\n")
for n in notes:
w.writerow([
n["type"],
n["deck"],
to_field(n["front"]),
to_field(n["back"]),
" ".join(n.get("tags", [])), # tags are space separated
])
with open(path, "w", encoding="utf-8", newline="") as f:
f.write(buf.getvalue())
def validate(path):
problems = []
raw = open(path, "rb").read()
try:
text = raw.decode("utf-8")
except UnicodeDecodeError as e:
return [f"not valid UTF-8: {e}"]
lines = text.split("\n")
header = [l for l in lines if l.startswith("#")][: len(HEADER)]
if header != HEADER:
problems.append(f"header mismatch: {header}")
body = "\n".join(l for l in lines if not l.startswith("#"))
rows = list(csv.reader(io.StringIO(body), delimiter="\t"))
rows = [r for r in rows if r]
seen = {}
for i, r in enumerate(rows, 1):
if len(r) != 5:
problems.append(f"row {i}: {len(r)} columns, expected 5")
continue
ntype, deck, f1, f2, tags = r
if ntype not in ("Basic", "Cloze"):
problems.append(f"row {i}: unknown note type {ntype!r}")
if not f1.strip():
problems.append(f"row {i}: empty first field")
if "\n" in f1 or "\n" in f2:
problems.append(f"row {i}: raw newline in field (use <br>)")
if re.search(r"<(?!br>)", f1 + f2):
problems.append(f"row {i}: unescaped '<' (HTML is on)")
if ntype == "Cloze" and not re.search(r"\{\{c\d+::.+?\}\}", f1):
problems.append(f"row {i}: Cloze note without {{{{cN::...}}}}")
if ntype == "Basic" and "{{c" in f1:
problems.append(f"row {i}: cloze syntax in a Basic note")
key = (ntype, f1)
if key in seen:
problems.append(f"row {i}: duplicate first field of row {seen[key]} (Anki would update, not add)")
seen.setdefault(key, i)
print(f"parsed {len(rows)} note rows")
return problems
if __name__ == "__main__":
notes = json.load(open(sys.argv[1], encoding="utf-8"))
build(notes, sys.argv[2])
probs = validate(sys.argv[2])
if probs:
print("PROBLEMS:")
for p in probs:
print(" -", p)
sys.exit(1)
print("OK")
I invented a six-note list about HTTP and DNS (two Basic, one Cloze, plus awkward ones: quotes, a newline, a literal <br> in the text, and a deliberate duplicate). First run:
parsed 6 note rows
PROBLEMS:
- row 6: duplicate first field of row 4 (Anki would update, not add)
The validator caught the planted duplicate and exited with status 1. After removing that sixth note:
parsed 5 note rows
OK
And the file it wrote (tabs shown as whitespace here; the real file uses tab characters):
#separator:tab
#html:true
#notetype column:1
#deck column:2
#tags column:5
Basic Networking::HTTP What does HTTP status 301 mean? Moved Permanently: the resource has a new URL and clients should use it from now on. http status-codes
Basic Networking::HTTP Which HTTP method is meant to be idempotent: POST or PUT? PUT. Sending the same PUT twice leaves the server in the same state; POST may create two things. http methods
Cloze Networking::HTTP A {{c1::404}} status means the server could not find the requested resource, while {{c2::500}} means an internal server error. Extra: 4xx is the client side, 5xx is the server side. http status-codes
Basic Networking::DNS "What does a DNS ""A"" record store?" An IPv4 address for a name, e.g. 203.0.113.7 (documentation range).<br>Second line: an AAAA record holds IPv6. dns
Basic Networking::DNS Why does the HTML tag <br> need care in an Anki import? "If ""Allow HTML in fields"" is on, < and > are parsed as markup; write &lt;br&gt; to show it literally." anki gotcha
Look at the DNS row: the double quotes inside the question forced the whole field into quotes, with inner quotes doubled, exactly as the manual describes. The newline became <br>.
What the script does not do: it can't tell you if a card is good, it doesn't check that your Anki note types have the fields in this order (the first two fields of Basic and Cloze are used, as the manual says regular columns fill fields by position), and it hasn't been tested against the Anki desktop app. If your note types are customised, map columns in the import dialog.
How do I import it and check the result?
- Save the output as a
.txtfile and use File, then Import in Anki desktop. - In the dialog, confirm the detected separator is Tab and that "Allow HTML in fields" is on (turn it off only if your content has raw angle brackets you don't want parsed).
- Look at the preview. If the columns look shifted, your separator or quoting is wrong.
- Import into a test deck first, open the card browser, and spot-check three cards of each type.
- Re-importing the same file is safe in the sense that matching first fields update rather than duplicate, but that also means a typo fix in your JSON overwrites the card's fields, not its scheduling. Per the manual, updated notes keep their deck and scheduling.
What review rhythm should I use?
Let Anki schedule it. The point of the app is that it decides when each card returns. I did not verify the default interval settings of the current Anki version, so check the manual's deck options page rather than trusting a number from me.
What you control is intake and habit:
- Add cards in small batches, right after you study the material, while you can still judge whether a card is faithful to it.
- Review daily, briefly. A short daily session beats a long weekly one because overdue cards pile up. If you skip a few days, the pile feels punishing and people quit.
- Cap new cards by your review load, not your enthusiasm. Every new card creates future reviews. If a week's worth of reviews feels like too much, add fewer cards.
- Suspend or rewrite cards you keep failing. A card you miss repeatedly is usually badly worded or two facts in one. Fix the card, not your memory.
- Mark suspicious cards. If an answer looks wrong when you meet it later, tag it
checkand compare it with the source notes. Cards generated by AI will contain a few errors; the review is when you'll find them.
When is this the wrong tool?
Skip flashcards for anything that requires reasoning over steps, such as proofs or system design: practise problems instead. Skip it if the exam is tomorrow and the material is unfamiliar; cram with active recall in a chat, as in the study prompts post. And if you use a source-grounded notebook tool, check its own flashcard export, as covered in the NotebookLM study guide; that CSV can feed the same pipeline after you adjust the columns.
Copyright note: write cards from your own notes and your own wording. Pasting an entire paid textbook chapter into a chatbot, or sharing a deck that reproduces one, may breach the publisher's terms or copyright. Check your course's rules.
A workflow in five steps
- Study a chunk and write short notes in your own words.
- Paste the notes into the prompt above and get JSON back.
- Read each card against the notes. Delete, split or rewrite.
- Run
python3 notes_to_anki.py notes.json out.txtand fix anything the validator reports. - Import into a test deck, spot-check, then move to your real deck and review daily.
Everything else is detail. If you do only step 3 properly, you'll be ahead of most AI-generated decks.



