Docs / Your cards
Import & export
Move cards in and out as CSV or TSV, merge decks by name, and keep full backups with progress and review history.
File format
Askesis reads and writes plain-text tables in UTF-8: comma-separated (CSV), tab-separated (TSV), semicolon or pipe. One row is one card. Fields with the separator, quotes or line breaks are wrapped in double quotes, with quotes doubled inside.
#separator:comma
#html:false
#columns:deck,front,back,hint,extra,tags
#deck column:1
#tags column:6
Japanese::Kanji,脳,brain,Kanji meaning,On: ノウ,kanji body
Spanish,aprovechar,to make the most of,,,verbs
Neuroscience,"Who coined the term ""engram""?","Richard Semon, in 1904",,,historyHeader lines
Optional lines at the top of the file describe it. They are the header lines common flashcard apps use for text import and export, so files move between apps.
| Line | Meaning |
|---|---|
#separator:comma | The separator: comma, tab, semicolon or pipe. Detected automatically if missing. |
#html:true | Fields contain HTML. It is converted to plain text on import (see below). |
#columns:deck,front,back | A name for each column. |
#deck column:1 | Which column (counting from 1) holds the deck path. |
#tags column:6 | Which column holds space-separated tags. |
Columns
| Column | Also accepted as | Notes |
|---|---|---|
front | question, term, word | Required. Rows without a front are skipped. |
back | answer, definition, meaning | |
deck | A path like Parent::Child. Missing decks are created. | |
tags | tag | Separated by spaces. Nest them with ::, like lang::spanish. |
hint | reading | A small line under the front. |
extra | notes, note | Shown under the answer. |
Column names can come from #columns or from a header row (detected when the first row is made of column names). With neither, the first column is the front and the second the back. You can change any mapping before importing. Files can also carry each card’s study progress and review history, see Progress columns.
Importing
- Choose a file. Askesis detects the separator, the header lines and the columns.
- Check the table of your first rows, one card per row. Each column’s header says what it holds (front, back, deck, tags, …); change it there if Askesis guessed wrong. The rendered cards below follow.
- Pick a default deck for rows that don’t name one. Type a new name to create it.
- Choose what to do with duplicates, then import.
You get a summary: decks created, decks merged, and cards added, updated and skipped. For a file with progress, it also says “Progress kept for N cards” and “N past reviews restored”.
How merging works
- Decks are matched by full path, ignoring upper/lower case. Cards are added to an existing deck with the same path. Nothing is ever deleted, so you can import into a deck again and again.
- Duplicates are cards in the same deck with the same front (ignoring extra spaces). You choose:
| Option | Result |
|---|---|
| Update | Default. The back, hint and extra are replaced, tags are merged, and the review history is kept. |
| Skip | Existing cards are left alone. |
| Import as new | A second copy is added. |
Exporting
Export everything or only the decks you select (subdecks are included), as CSV (optionally separated with semicolons, for spreadsheets set to a European locale), TSV, or plain text (.txt) for other flashcard apps. Under Include there are two tickboxes:
| Tickbox | Default | Adds |
|---|---|---|
| Study progress | On | Each card’s state, due date, interval, ease, memory, learning step and suspended flag. New cards stay new. |
| Review history | On (needs Study progress) | Every past answer, so your heatmap, streak, XP and stats come back when you import the file. |
Your choice is remembered on this device. With both ticked, the file is a complete backup. To share cards so others start fresh, untick Study progress, which leaves out the history too. Other flashcard apps usually ignore these columns on import; Askesis restores them.
Progress columns
With progress included, these columns follow the tags, in this order:
state, due, interval, ease, stability, difficulty, step, reps, lapses, last_review, suspended, history| Column | Meaning |
|---|---|
state | 0 new, 1 learning, 2 review, 3 relearning. |
due | When the card is next due, in ISO 8601 (2026-10-16T04:00:00.000Z). |
interval | Current interval in days (0 while learning). |
ease | Ease factor of the default scheduler: 2.5 means 250%. |
stability, difficulty | FSRS memory state, 0 when unknown. |
step | Position in the learning or relearning steps, counting from 0. |
reps, lapses | Times reviewed and times forgotten. |
last_review | Time of the last answer. |
suspended | 1 if the card is suspended. |
history | Every past answer (next section). Only with Review history ticked. |
Leave them empty for cards that haven’t been studied. A row only counts as having progress when its state and due date are both valid.
Review history
The history column holds a card’s past answers, oldest first, separated by ;. Each answer is up to eight values separated by spaces:
<time> <rating> <ms> <state before> <state after> <interval before> <interval after> <ease after>| Value | Meaning |
|---|---|
| time | When you answered, in ISO 8601. Required. |
| rating | 1 Again, 2 Hard, 3 Good, 4 Easy. Required. |
| ms | Time spent on the card, in milliseconds. |
| state before, state after | 0 new, 1 learning, 2 review, 3 relearning. |
| interval before, interval after | In days. |
| ease after | Ease factor after the answer, like 2.5. |
Only time and rating are required. This card was learned with two Good answers about ten minutes apart, and graduated to a 1-day interval:
2026-03-01T09:12:00Z 3 5200 0 1 0 0 0;2026-03-01T09:22:40Z 3 3100 1 2 0 1 2.5Restoring a backup
- Cards that are new in the file stay new. Cards you don’t have yet are added with the file’s progress.
- Cards you already have (same deck and front, with duplicates set to Update) take the file’s progress only when its
last_reviewis newer than the card’s own. Restoring an old backup never rolls back study you did since. - History is merged into your review log without duplicates: an answer for the same card at the same moment, to the millisecond, is skipped. Importing the same backup twice is safe.
- Imported history counts everywhere: the heatmap, streaks, XP, trophies and every chart (Reading your stats).
- Imported answers can’t be undone in a study session.
After the import, the summary says how many cards kept their progress and how many past reviews were restored.
Coming from another app
- A plain-text notes export (.txt) from another flashcard app imports directly. Its
#guid columnand#notetype columnare skipped, a third field becomes the extra, and cloze notes ({{c1::answer::hint}}) become one card per deletion. - HTML fields are converted: line breaks, lists, bold and italic are kept, and the
[latex],[$]and[$$]tags some apps use become Askesis math. Other HTML is dropped. - To go the other way, export as Plain text (.txt). Apps that read the header lines also import the Askesis CSV, including the deck and tags columns.
.apkgand.colpkgpackages can’t be imported directly yet; convert them with the prompt below. Media files aren’t imported.
Flashcard packages (.apkg, .colpkg)
Until Askesis reads flashcard packages itself, an AI assistant that can run code (ChatGPT, Claude or Gemini) can convert one for you, keeping your progress:
- Copy the prompt below.
- Open your AI assistant, attach your .apkg or .colpkg file and paste the prompt.
- Download the CSV it creates and import it on the Decks page.
Every card keeps its state, due date, interval, ease, FSRS memory state, review count, lapses and suspended flag. Subdecks come through as Parent::Child paths and cloze notes become one card per deletion.
Your review history comes along too: the package’s review log becomes the history column, so the heatmap, streaks and stats pick up where you left off. Manual reschedules aren’t answers and are skipped. The current learning step isn’t carried over, so cards in the middle of their learning steps start them again.
I'm attaching a flashcard package (.apkg or .colpkg). Convert it to an Askesis CSV
file I can import, keeping each card's study progress and review history. Use
Python. Run the script below as-is (install the "zstandard" package if it's
missing), then give me the resulting askesis-import.csv to download. If something
fails, explain the error and fix the script. Don't invent or rewrite any card
content.
Format rules (the Askesis spec, also at /llms.txt on the Askesis site):
- One row per CARD (not per note), so every card keeps its own progress.
- Header lines first, then rows with columns:
deck,front,back,hint,extra,tags,state,due,interval,ease,stability,difficulty,step,reps,lapses,last_review,suspended,history
- Deck paths use "::" between levels. Tags are space-separated.
- Fields keep the package's HTML; "#html:true" tells Askesis to convert it.
- Progress: state 0 new, 1 learning, 2 review, 3 relearning; due and
last_review as ISO 8601 UTC; interval in days; ease as a factor (2.5 = 250%);
stability/difficulty from FSRS when present; step is left empty (Askesis restarts
the current learning steps); leave progress empty for new cards so they stay new.
- history: every past answer from the review log (revlog table), oldest first,
separated by ";". Each answer is "time rating ms state_before state_after
interval_before interval_after ease" (time ISO 8601 UTC, rating 1-4, intervals
in days). Manual reschedules (revlog type 4 and 5, or ease 0) are not answers
and are skipped.
- Cloze cards are expanded here (front hides the deletion, back shows it in bold).
- Media (images, audio) can't be imported; references are dropped by Askesis.
```python
import csv, io, json, re, sqlite3, sys, tempfile, zipfile
from datetime import datetime, timezone
PATH = sys.argv[1] if len(sys.argv) > 1 else "deck.apkg" # set to the uploaded file
OUT = "askesis-import.csv"
def open_collection(path):
z = zipfile.ZipFile(path)
tmp = tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False)
name = max(n for n in z.namelist() if n.startswith("collection.")) # newest format in the package
raw = z.read(name)
if name.endswith("b"): # modern packages: zstd-compressed SQLite
import zstandard
raw = zstandard.ZstdDecompressor().decompressobj().decompress(raw)
tmp.write(raw); tmp.close()
return sqlite3.connect(tmp.name)
db = open_collection(PATH)
crt = db.execute("select crt from col").fetchone()[0] # collection day zero, seconds
try: # schema 18: decks table, "\x1f" between levels
decks = {i: n.replace("\x1f", "::") for i, n in db.execute("select id, name from decks")}
except sqlite3.OperationalError: # schema 11: JSON in col.decks
decks = {int(k): v["name"] for k, v in json.loads(db.execute("select decks from col").fetchone()[0]).items()}
notes = {nid: (flds.split("\x1f"), tags.strip()) for nid, flds, tags in db.execute("select id, flds, tags from notes")}
last = dict(db.execute("select cid, max(id) from revlog group by cid")) # ms timestamps
# Review history. revlog type: 0 learn, 1 review, 2 relearn, 3 filtered, 4 manual, 5 rescheduled.
# ivl/lastIvl: positive = days, negative = seconds (still in learning steps).
history = {}
for rid, cid, ease, ivl, last_ivl, factor, taken, rtype in db.execute(
"select id, cid, ease, ivl, lastIvl, factor, time, type from revlog order by id"):
if ease not in (1, 2, 3, 4) or rtype not in (0, 1, 2, 3):
continue
past = history.setdefault(cid, [])
if rtype == 0:
before = 1 if past else 0 # the first learning answer is the new card's
elif rtype == 2:
before = 3
elif rtype == 1:
before = 2
else: # filtered deck: judge by the interval before
before = 2 if last_ivl > 0 else (1 if past else 0)
if ivl > 0:
after = 2
elif rtype == 3:
after = before if before != 0 else 1
else:
after = 1 if rtype == 0 else 3
past.append(" ".join(str(v) for v in (
datetime.fromtimestamp(rid / 1000, timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
ease, max(0, min(taken, 3_600_000)), before, after, max(last_ivl, 0), max(ivl, 0), round(factor / 1000, 3))))
CLOZE = re.compile(r"\{\{c(\d+)::(.*?)(?:::(.*?))?\}\}", re.S)
def cloze(text, n):
front = CLOZE.sub(lambda m: "[%s]" % ((m.group(3) or "…").strip()) if int(m.group(1)) == n else m.group(2), text)
back = CLOZE.sub(lambda m: "<b>%s</b>" % m.group(2) if int(m.group(1)) == n else m.group(2), text)
return front, back
def iso(seconds):
return datetime.fromtimestamp(seconds, timezone.utc).isoformat().replace("+00:00", "Z")
rows = []
query = "select id, nid, did, ord, type, queue, due, ivl, factor, reps, lapses, odid, odue, data from cards order by nid, ord"
for cid, nid, did, ord_, ctype, queue, due, ivl, factor, reps, lapses, odid, odue, data in db.execute(query):
fields, tags = notes[nid]
if odid: # card sits in a filtered deck: use its home deck and due
did, due = odid, odue
f = lambda i: fields[i] if i < len(fields) else ""
if CLOZE.search(f(0)): # Cloze: one card per deletion (ord 0 = c1)
front, back = cloze(f(0), ord_ + 1)
extra = f(1)
elif ord_ == 1 and len(fields) >= 2: # "Basic (and reversed card)": the reverse card
front, back, extra = f(1), f(0), "<br>".join(fields[2:])
else:
front, back, extra = f(0), f(1), "<br>".join(fields[2:])
if ctype in (1, 2, 3):
if ctype == 2 or queue == 3 or due < 1_000_000_000: # day-based due (days since crt)
due_at = crt + due * 86400
else: # intraday learning: epoch seconds
due_at = due
try:
fsrs = json.loads(data or "{}")
except ValueError:
fsrs = {}
progress = [
ctype, iso(due_at), max(ivl, 0), round(factor / 1000, 3),
fsrs.get("s", 0) or 0, fsrs.get("d", 0) or 0, "", reps, lapses,
iso(last[cid] / 1000) if cid in last else "",
]
else:
progress = [""] * 10
suspended = 1 if queue == -1 else ""
rows.append([decks.get(did, "Imported"), front, back, "", extra, tags, *progress, suspended, ";".join(history.get(cid, []))])
with open(OUT, "w", newline="", encoding="utf-8") as out:
out.write("#separator:comma\n#html:true\n")
out.write("#columns:deck,front,back,hint,extra,tags,state,due,interval,ease,stability,difficulty,step,reps,lapses,last_review,suspended,history\n")
out.write("#deck column:1\n#tags column:6\n")
csv.writer(out, quoting=csv.QUOTE_ALL).writerows(rows)
reviews = sum(len(h) for h in history.values())
print(f"Wrote {len(rows)} cards ({reviews} past reviews) from {len(notes)} notes in {len(set(r[0] for r in rows))} decks to {OUT}")
```
When it's done, tell me how many cards, past reviews and decks were converted.