Tech · PRACTICAL GUIDE
Share Anki Filtered-Deck Templates Without Study History
Export existing searches and preferences to a separate JSON file, then recreate them in another Anki collection. A complete local helper checks the batch before creating new filtered decks.

If you have a set of Anki filtered decks that you want someone else to reuse, share their definitions separately from your cards. The helper below exports their names, searches, limits, order and filtered-deck preferences to JSON. The recipient can inspect that file and recreate the batch using eligible cards in their own collection, without receiving your card records or review log through the JSON.
This is an advanced Anki Desktop workflow, verified against the real Anki 26.09.3 Python/Rust backend in disposable collections. Importing the settings with apply=True immediately builds new filtered decks and moves matching local cards into them. Read the preflight report first, use the correct profile, and keep a current backup before applying changes. The helper is provided in full below; it does not require a separate Python installation.
1. Choose the transfer you actually need
A collection package, .colpkg, is a suitable existing option when the other person wants your entire collection in a separate profile, including its scheduling data. Importing it replaces the current collection’s cards. It does not meet a request to send only filter definitions or to leave your accumulated study history out of the handoff.
An ordinary deck package, .apkg, can add cards to another collection, and Anki offers an option to exclude scheduling. That package is useful for the cards, but it should not be treated as a reliable transport for your filtered-deck definitions. CrowdAnki also explicitly excludes filtered decks from its JSON export. Tools that create a filter from a tag or rebuild existing filters solve useful parts of the workflow; neither operation transfers a selected batch of your existing complete definitions.
Use the supplied helper when the reusable asset is the configured filtered deck. It preserves the current seven configuration fields, including both search terms, explicit false and zero values, current preview delays and older stored delay fields. Copying those older fields does not make an old scheduler active. Ordinary deck presets, FSRS configuration, add-on settings, media and card contents are outside this JSON format.
The file still contains your chosen deck names and literal query text, including any IDs you put in a search. Read it before sharing and remove private names or terms if necessary. The absence of cards and review history does not make those strings anonymous.
2. Save and load the complete helper in Anki Desktop
Save the complete following exec(...) line as a UTF-8 plain-text file named anki_filtered_templates.py. It contains the whole program; the escaped line breaks and spaces preserve Python indentation when copied from this page. Keep the line intact and do not replace its straight quotes with typographic quotes. The companion source can also be kept in its normal, indented form.
exec("\"\"\"Filtered-deck\x20configuration\x20only;\x20tested\x20with\x20Anki\x2026.9.3's\x20real\x20backend.\n\nLoad\x20this\x20file\x20in\x20Anki\x20Desktop's\x20debug\x20console,\x20then\x20use\x20mw.col:\n\x20\x20\x20\x20export_filtered_templates(mw.col,\x20'/absolute/path/templates.json')\n\x20\x20\x20\x20import_filtered_templates(mw.col,\x20'/absolute/path/templates.json')\n\x20\x20\x20\x20import_filtered_templates(mw.col,\x20'/absolute/path/templates.json',\x20apply=True)\nUse\x20names=['Exact\x20name',\x20...]\x20to\x20export\x20a\x20selection.\x20None\x20exports\x20all\x20filtered\ndecks\x20in\x20name\x20order.\x20prefix\x20changes\x20destination\x20deck\x20names,\x20never\x20searches.\napply=True\x20immediately\x20builds\x20using\x20destination-local\x20cards,\x20in\x20file\x20order.\nEarlier\x20builds\x20take\x20precedence\x20for\x20overlapping\x20cards.\x20This\x20is\x20not\x20one\x20atomic\nbatch\x20or\x20one\x20promised\x20Undo.\x20Existing\x20filtered\x20decks\x20are\x20never\x20emptied\x20here.\nNo\x20source\x20deck-ID\x20metadata,\x20card/note\x20records\x20or\x20review\x20logs\x20are\x20exported.\nSearch\x20text\x20is\x20preserved\x20and\x20may\x20contain\x20source-specific\x20IDs.\n\"\"\"\nimport\x20json\nimport\x20math\nimport\x20unicodedata\nfrom\x20pathlib\x20import\x20Path\nfrom\x20anki.collection\x20import\x20Collection\nfrom\x20anki.decks\x20import\x20DeckId,\x20FilteredDeckConfig\nfrom\x20google.protobuf.json_format\x20import\x20ParseDict\n\nSCHEMA\x20=\x20'anki-filtered-templates'\nVERSION\x20=\x201\nCONFIG_KEYS\x20=\x20{'reschedule',\x20'search_terms',\x20'delays',\x20'preview_delay',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'preview_again_secs',\x20'preview_hard_secs',\x20'preview_good_secs'}\nDELAY_KEYS\x20=\x20('preview_delay',\x20'preview_again_secs',\x20'preview_hard_secs',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'preview_good_secs')\nORDER\x20=\x20FilteredDeckConfig.SearchTerm.Order\n\n\nclass\x20PartialImportError(RuntimeError):\n\x20\x20\x20\x20\"\"\"Inspect\x20.partial_report;\x20successful\x20earlier\x20creations\x20remain\x20present.\"\"\"\n\x20\x20\x20\x20def\x20__init__(self,\x20message,\x20report):\n\x20\x20\x20\x20\x20\x20\x20\x20self.partial_report\x20=\x20report\n\x20\x20\x20\x20\x20\x20\x20\x20ids\x20=\x20[row['id']\x20for\x20row\x20in\x20report['created']]\n\x20\x20\x20\x20\x20\x20\x20\x20super().__init__(f'{message}.\x20New\x20filtered\x20IDs\x20so\x20far:\x20{ids}.\x20'\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Inspect\x20exception.partial_report;\x20no\x20rollback\x20was\x20attempted.')\n\n\ndef\x20_object(value,\x20allowed,\x20required,\x20label):\n\x20\x20\x20\x20if\x20type(value)\x20is\x20not\x20dict:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20expected\x20an\x20object')\n\x20\x20\x20\x20unknown,\x20missing\x20=\x20set(value)\x20-\x20allowed,\x20required\x20-\x20set(value)\n\x20\x20\x20\x20if\x20unknown\x20or\x20missing:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20unknown\x20keys\x20{sorted(unknown)},\x20missing\x20keys\x20{sorted(missing)}')\n\n\ndef\x20_name(value):\n\x20\x20\x20\x20if\x20not\x20isinstance(value,\x20str)\x20or\x20not\x20value:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('Deck\x20name\x20must\x20be\x20a\x20nonempty\x20string')\n\x20\x20\x20\x20if\x20(any(ord(c)\x20\u003c\x2032\x20or\x20ord(c)\x20==\x20127\x20for\x20c\x20in\x20value)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20or\x20any(not\x20p\x20or\x20p\x20!=\x20p.strip()\x20for\x20p\x20in\x20value.split('::'))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20or\x20unicodedata.normalize('NFC',\x20value)\x20!=\x20value):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Name\x20needs\x20nonempty,\x20unpadded,\x20NFC-normalized\x20components:\x20{value!r}')\n\x20\x20\x20\x20return\x20value\n\n\ndef\x20_uint(value,\x20label):\n\x20\x20\x20\x20if\x20type(value)\x20is\x20not\x20int\x20or\x20not\x200\x20\u003c=\x20value\x20\u003c=\x204294967295:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20expected\x20an\x20integer\x20from\x200\x20to\x204294967295')\n\n\ndef\x20_config_json(config):\n\x20\x20\x20\x20#\x20Explicit\x20defaults\x20keep\x20false\x20and\x20zero\x20visible;\x20no\x20IDs\x20or\x20scheduling\x20data.\n\x20\x20\x20\x20result\x20=\x20{'reschedule':\x20config.reschedule,\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'search_terms':\x20[{'search':\x20t.search,\x20'limit':\x20t.limit,\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'order':\x20ORDER.Name(t.order)}\x20for\x20t\x20in\x20config.search_terms],\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'delays':\x20list(config.delays)}\n\x20\x20\x20\x20result.update({key:\x20getattr(config,\x20key)\x20for\x20key\x20in\x20DELAY_KEYS})\n\x20\x20\x20\x20return\x20result\n\n\ndef\x20_config(col,\x20value,\x20label):\n\x20\x20\x20\x20_object(value,\x20CONFIG_KEYS,\x20{'search_terms'},\x20label)\n\x20\x20\x20\x20if\x20'reschedule'\x20in\x20value\x20and\x20type(value['reschedule'])\x20is\x20not\x20bool:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}.reschedule:\x20expected\x20true\x20or\x20false')\n\x20\x20\x20\x20for\x20key\x20in\x20DELAY_KEYS:\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20key\x20in\x20value:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20_uint(value[key],\x20f'{label}.{key}')\n\x20\x20\x20\x20delays\x20=\x20value.get('delays',\x20[])\n\x20\x20\x20\x20if\x20type(delays)\x20is\x20not\x20list\x20or\x20any(type(v)\x20not\x20in\x20(int,\x20float)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20or\x20not\x20math.isfinite(v)\x20or\x20v\x20\u003c\x200\x20for\x20v\x20in\x20delays):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}.delays:\x20expected\x20finite\x20nonnegative\x20numbers')\n\x20\x20\x20\x20terms\x20=\x20value['search_terms']\n\x20\x20\x20\x20if\x20type(terms)\x20is\x20not\x20list\x20or\x20not\x201\x20\u003c=\x20len(terms)\x20\u003c=\x202:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20requires\x20one\x20or\x20two\x20search\x20terms')\n\x20\x20\x20\x20normalized\x20=\x20[]\n\x20\x20\x20\x20for\x20i,\x20term\x20in\x20enumerate(terms):\n\x20\x20\x20\x20\x20\x20\x20\x20part\x20=\x20f'{label}.search_terms[{i}]'\n\x20\x20\x20\x20\x20\x20\x20\x20_object(term,\x20{'search',\x20'limit',\x20'order'},\x20{'search'},\x20part)\n\x20\x20\x20\x20\x20\x20\x20\x20search\x20=\x20term['search']\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20not\x20isinstance(search,\x20str)\x20or\x20not\x20search.strip():\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{part}:\x20blank\x20search\x20rejected;\x20use\x20explicit\x20deck:*\x20for\x20all\x20decks')\n\x20\x20\x20\x20\x20\x20\x20\x20_uint(term.get('limit',\x200),\x20part\x20+\x20'.limit')\n\x20\x20\x20\x20\x20\x20\x20\x20order\x20=\x20term.get('order',\x200)\n\x20\x20\x20\x20\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20type(order)\x20is\x20str:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20ORDER.Value(order)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20elif\x20type(order)\x20is\x20int:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20ORDER.Name(order)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20else:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('not\x20an\x20enum\x20name\x20or\x20integer')\n\x20\x20\x20\x20\x20\x20\x20\x20except\x20(ValueError,\x20KeyError)\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{part}:\x20unknown\x20order\x20{order!r}')\x20from\x20error\n\x20\x20\x20\x20\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20normalized.append(col.build_search_string(search))\n\x20\x20\x20\x20\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{part}:\x20invalid\x20search:\x20{error}')\x20from\x20error\n\x20\x20\x20\x20result\x20=\x20FilteredDeckConfig()\n\x20\x20\x20\x20result.Clear()\n\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20ParseDict(value,\x20result,\x20ignore_unknown_fields=False)\n\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'{label}:\x20invalid\x20configuration:\x20{error}')\x20from\x20error\n\x20\x20\x20\x20return\x20result,\x20normalized\n\n\ndef\x20_manifest(col,\x20value):\n\x20\x20\x20\x20_object(value,\x20{'schema',\x20'version',\x20'templates'},\x20{'schema',\x20'version',\x20'templates'},\x20'manifest')\n\x20\x20\x20\x20if\x20value['schema']\x20!=\x20SCHEMA\x20or\x20type(value['version'])\x20is\x20not\x20int\x20or\x20value['version']\x20!=\x20VERSION:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Expected\x20schema\x20{SCHEMA!r},\x20version\x20{VERSION}')\n\x20\x20\x20\x20if\x20type(value['templates'])\x20is\x20not\x20list\x20or\x20not\x20value['templates']:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('templates\x20must\x20be\x20a\x20nonempty\x20list')\n\x20\x20\x20\x20seen,\x20result\x20=\x20set(),\x20[]\n\x20\x20\x20\x20for\x20i,\x20item\x20in\x20enumerate(value['templates']):\n\x20\x20\x20\x20\x20\x20\x20\x20_object(item,\x20{'name',\x20'config'},\x20{'name',\x20'config'},\x20f'template\x20{i\x20+\x201}')\n\x20\x20\x20\x20\x20\x20\x20\x20name\x20=\x20_name(item['name'])\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20name.casefold()\x20in\x20seen:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Duplicate\x20template\x20name:\x20{name}')\n\x20\x20\x20\x20\x20\x20\x20\x20seen.add(name.casefold())\n\x20\x20\x20\x20\x20\x20\x20\x20config,\x20normalized\x20=\x20_config(col,\x20item['config'],\x20name)\n\x20\x20\x20\x20\x20\x20\x20\x20result.append((name,\x20config,\x20normalized))\n\x20\x20\x20\x20return\x20result\n\n\ndef\x20_pairs(pairs):\n\x20\x20\x20\x20result\x20=\x20{}\n\x20\x20\x20\x20for\x20key,\x20value\x20in\x20pairs:\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20key\x20in\x20result:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Duplicate\x20JSON\x20key:\x20{key}')\n\x20\x20\x20\x20\x20\x20\x20\x20result[key]\x20=\x20value\n\x20\x20\x20\x20return\x20result\n\n\ndef\x20_bad_constant(value):\n\x20\x20\x20\x20raise\x20ValueError(f'Nonfinite\x20JSON\x20constant:\x20{value}')\n\n\ndef\x20_counts(col):\n\x20\x20\x20\x20return\x20{'cards':\x20int(col.card_count()),\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'revlog':\x20int(col.db.scalar('select\x20count(*)\x20from\x20revlog'))}\n\n\ndef\x20export_filtered_templates(col,\x20path,\x20*,\x20names=None):\n\x20\x20\x20\x20\"\"\"Read\x20existing\x20filtered\x20configs,\x20validate\x20all,\x20then\x20exclusively\x20write\x20JSON.\n\n\x20\x20\x20\x20names=None\x20means\x20all\x20filtered\x20decks;\x20a\x20nonempty\x20list\x20gives\x20exact\x20selection\n\x20\x20\x20\x20and\x20order.\x20The\x20collection\x20is\x20not\x20written.\x20Existing\x20output\x20files\x20are\x20refused.\n\x20\x20\x20\x20Returns\x20the\x20manifest\x20that\x20was\x20written.\n\x20\x20\x20\x20\"\"\"\n\x20\x20\x20\x20available\x20=\x20{n.name:\x20n.id\x20for\x20n\x20in\x20col.decks.all_names_and_ids()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20col.decks.get(n.id,\x20default=False)['dyn']}\n\x20\x20\x20\x20if\x20names\x20is\x20None:\n\x20\x20\x20\x20\x20\x20\x20\x20selected\x20=\x20list(available)\n\x20\x20\x20\x20elif\x20type(names)\x20is\x20list\x20and\x20names\x20and\x20all(isinstance(n,\x20str)\x20for\x20n\x20in\x20names):\n\x20\x20\x20\x20\x20\x20\x20\x20selected\x20=\x20names\n\x20\x20\x20\x20else:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('names\x20must\x20be\x20None\x20or\x20a\x20nonempty\x20list\x20of\x20exact\x20deck\x20names')\n\x20\x20\x20\x20if\x20len(selected)\x20!=\x20len(set(selected)):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('Duplicate\x20names\x20in\x20selection')\n\x20\x20\x20\x20missing\x20=\x20[name\x20for\x20name\x20in\x20selected\x20if\x20name\x20not\x20in\x20available]\n\x20\x20\x20\x20if\x20missing:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Not\x20existing\x20filtered\x20deck\x20names:\x20{missing}')\n\x20\x20\x20\x20manifest\x20=\x20{'schema':\x20SCHEMA,\x20'version':\x20VERSION,\x20'templates':\x20[]}\n\x20\x20\x20\x20for\x20name\x20in\x20selected:\n\x20\x20\x20\x20\x20\x20\x20\x20saved\x20=\x20col.sched.get_or_create_filtered_deck(DeckId(available[name]))\n\x20\x20\x20\x20\x20\x20\x20\x20manifest['templates'].append({'name':\x20saved.name,\x20'config':\x20_config_json(saved.config)})\n\x20\x20\x20\x20_manifest(col,\x20manifest)\x20\x20#\x20No\x20incomplete\x20output\x20when\x20one\x20selection\x20is\x20invalid.\n\x20\x20\x20\x20text\x20=\x20json.dumps(manifest,\x20ensure_ascii=False,\x20allow_nan=False,\x20indent=2)\x20+\x20'\\n'\n\x20\x20\x20\x20with\x20Path(path).open('x',\x20encoding='utf-8')\x20as\x20stream:\n\x20\x20\x20\x20\x20\x20\x20\x20stream.write(text)\n\x20\x20\x20\x20return\x20manifest\n\n\ndef\x20import_filtered_templates(col,\x20path,\x20*,\x20prefix='',\x20apply=False):\n\x20\x20\x20\x20\"\"\"Validate\x20the\x20whole\x20file\x20first;\x20apply=False\x20never\x20creates/builds\x20decks.\n\n\x20\x20\x20\x20apply=True\x20creates\x20NEW\x20IDs\x20in\x20manifest\x20order\x20and\x20immediately\x20builds\x20from\n\x20\x20\x20\x20local\x20cards.\x20Prefix\x20alters\x20deck\x20names\x20only.\x20Backend-normalized\x20queries\x20are\n\x20\x20\x20\x20shown\x20in\x20the\x20plan\x20and\x20checked\x20on\x20readback;\x20original\x20query\x20text\x20is\x20submitted.\n\x20\x20\x20\x20On\x20execution\x20failure,\x20PartialImportError.partial_report\x20lists\x20created\x20IDs.\n\x20\x20\x20\x20Earlier\x20successful\x20operations\x20remain;\x20no\x20automatic\x20Empty/delete\x20is\x20done.\n\x20\x20\x20\x20\"\"\"\n\x20\x20\x20\x20if\x20type(apply)\x20is\x20not\x20bool\x20or\x20not\x20isinstance(prefix,\x20str):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('apply\x20must\x20be\x20a\x20bool\x20and\x20prefix\x20must\x20be\x20a\x20string')\n\x20\x20\x20\x20with\x20Path(path).open(encoding='utf-8-sig')\x20as\x20stream:\n\x20\x20\x20\x20\x20\x20\x20\x20data\x20=\x20json.load(stream,\x20object_pairs_hook=_pairs,\x20parse_constant=_bad_constant)\n\x20\x20\x20\x20parsed\x20=\x20_manifest(col,\x20data)\n\x20\x20\x20\x20existing\x20=\x20{n.name.casefold():\x20n.id\x20for\x20n\x20in\x20col.decks.all_names_and_ids()}\n\x20\x20\x20\x20original_ids\x20=\x20set(existing.values())\n\x20\x20\x20\x20planned\x20=\x20[_name(prefix\x20+\x20name)\x20for\x20name,\x20_,\x20_\x20in\x20parsed]\n\x20\x20\x20\x20folded\x20=\x20{name.casefold()\x20for\x20name\x20in\x20planned}\n\x20\x20\x20\x20if\x20len(folded)\x20!=\x20len(planned):\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError('Destination\x20names\x20collide\x20after\x20adding\x20prefix')\n\x20\x20\x20\x20for\x20name\x20in\x20planned:\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20col.decks.id_for_name(name)\x20is\x20not\x20None:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Destination\x20name\x20already\x20exists:\x20{name}')\n\x20\x20\x20\x20\x20\x20\x20\x20components\x20=\x20name.split('::')\n\x20\x20\x20\x20\x20\x20\x20\x20for\x20length\x20in\x20range(1,\x20len(components)):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20parent\x20=\x20'::'.join(components[:length])\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20pid\x20=\x20col.decks.id_for_name(parent)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20parent.casefold()\x20in\x20folded\x20or\x20(pid\x20is\x20not\x20None\x20and\x20col.decks.get(pid)['dyn']):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Filtered\x20decks\x20cannot\x20be\x20parents:\x20{parent}')\n\x20\x20\x20\x20report\x20=\x20{'applied':\x20False,\x20'templates':\x20[],\x20'created':\x20[],\x20'new_parent_decks':\x20[],\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'before':\x20_counts(col),\x20'warnings':\x20[\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Apply\x20immediately\x20builds\x20from\x20destination-local\x20cards\x20in\x20manifest\x20order.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Earlier\x20filtered\x20builds\x20take\x20precedence\x20for\x20overlapping\x20cards.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'The\x20batch\x20is\x20not\x20atomic;\x20no\x20automatic\x20rollback\x20or\x20promised\x20single\x20Undo.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Prefix\x20changes\x20filtered\x20names\x20only;\x20search\x20strings\x20keep\x20their\x20original\x20targets.'],\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'recovery':\x20[\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Inspect\x20created\x20IDs\x20and\x20names\x20before\x20any\x20recovery\x20action.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'To\x20return\x20cards,\x20use\x20Empty\x20on\x20ONLY\x20those\x20newly\x20created\x20filtered\x20decks.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Verify\x20each\x20is\x20empty,\x20then\x20delete\x20only\x20those\x20new\x20filtered\x20decks\x20if\x20unwanted.',\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'Do\x20not\x20delete\x20normal\x20parent\x20decks\x20that\x20contain\x20other\x20decks\x20or\x20cards.']}\n\x20\x20\x20\x20for\x20name,\x20(_,\x20config,\x20normalized)\x20in\x20zip(planned,\x20parsed):\n\x20\x20\x20\x20\x20\x20\x20\x20report['templates'].append({'name':\x20name,\x20'config':\x20_config_json(config),\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20'normalized_searches':\x20normalized})\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20normalized\x20!=\x20[term.search\x20for\x20term\x20in\x20config.search_terms]:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['warnings'].append(f'{name}:\x20backend\x20will\x20normalize\x20search\x20spelling;\x20see\x20normalized_searches.')\n\x20\x20\x20\x20if\x20not\x20apply:\n\x20\x20\x20\x20\x20\x20\x20\x20report['after']\x20=\x20_counts(col)\n\x20\x20\x20\x20\x20\x20\x20\x20return\x20report\n\x20\x20\x20\x20report['applied']\x20=\x20True\n\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20for\x20item,\x20(_,\x20config,\x20normalized)\x20in\x20zip(report['templates'],\x20parsed):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20col.decks.id_for_name(item['name'])\x20is\x20not\x20None:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20ValueError(f'Name\x20appeared\x20after\x20preflight:\x20{item[\"name\"]}')\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new\x20=\x20col.sched.get_or_create_filtered_deck(DeckId(0))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new.name,\x20new.allow_empty\x20=\x20item['name'],\x20True\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new.config.Clear()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20new.config.CopyFrom(config)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20did\x20=\x20int(col.sched.add_or_update_filtered_deck(new).id)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20did\x20in\x20original_ids:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20RuntimeError(f'Backend\x20unexpectedly\x20reused\x20existing\x20ID\x20{did}')\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20row\x20=\x20{'id':\x20did,\x20'name':\x20item['name']}\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['created'].append(row)\x20\x20#\x20Retain\x20the\x20ID\x20even\x20if\x20verification\x20fails.\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20actual\x20=\x20col.sched.get_or_create_filtered_deck(DeckId(did))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20row.update(name=actual.name,\x20config=_config_json(actual.config),\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20card_count=int(col.db.scalar('select\x20count(*)\x20from\x20cards\x20where\x20did=?',\x20did)))\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20expected\x20=\x20FilteredDeckConfig()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20expected.CopyFrom(config)\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20for\x20term,\x20search\x20in\x20zip(expected.search_terms,\x20normalized):\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20term.search\x20=\x20search\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20actual.name\x20!=\x20item['name']\x20or\x20actual.config\x20!=\x20expected:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20RuntimeError(f'Readback\x20mismatch\x20for\x20new\x20filtered\x20deck\x20{did};\x20inspect\x20partial_report')\n\x20\x20\x20\x20\x20\x20\x20\x20report['after']\x20=\x20_counts(col)\n\x20\x20\x20\x20\x20\x20\x20\x20if\x20report['before']\x20!=\x20report['after']:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20raise\x20RuntimeError('Card/revlog\x20totals\x20changed\x20during\x20import;\x20inspect\x20partial_report')\n\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20raise\x20PartialImportError(str(error),\x20report)\x20from\x20error\n\x20\x20\x20\x20finally:\n\x20\x20\x20\x20\x20\x20\x20\x20try:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20created_ids\x20=\x20{row['id']\x20for\x20row\x20in\x20report['created']}\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['new_parent_decks']\x20=\x20[{'id':\x20n.id,\x20'name':\x20n.name}\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20for\x20n\x20in\x20col.decks.all_names_and_ids()\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20if\x20n.id\x20not\x20in\x20original_ids\x20and\x20n.id\x20not\x20in\x20created_ids]\n\x20\x20\x20\x20\x20\x20\x20\x20except\x20Exception\x20as\x20error:\n\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20report['inspection_error']\x20=\x20str(error)\n\x20\x20\x20\x20return\x20report\n")
Open the source profile in Anki Desktop and return to its main window. Open the Debug Console with Ctrl+Shift+; on Windows/Linux or Command+Shift+; on macOS. Some keyboard layouts use : or + instead of ;. Paste a command in the top pane and evaluate it with Ctrl+Return or Command+Return, without Shift. These are the official console instructions; the verification below did not exercise the graphical console itself.
Load the file with the following command, replacing the example with its full path. Forward slashes also work in a quoted Windows path such as C:/Users/YOU/Downloads/anki_filtered_templates.py. The mw.col object is the collection open in this Anki profile; do not point a separately installed Python process at a live collection file.
from pathlib import Path; from aqt import mw; exec(Path("/FULL/PATH/anki_filtered_templates.py").read_text(encoding="utf-8"))
3. Export the definitions before preparing any cards
Choose a new JSON filename in an existing folder. The following command exports every current filtered deck in name order and prints the number exported. Export reads the collection and refuses to overwrite an existing output file. Change the path for your own computer.
manifest = export_filtered_templates(mw.col, "/FULL/PATH/templates.json"); print(len(manifest["templates"]))
For a selected set, supply exact names in the order you want them built. For example: manifest = export_filtered_templates(mw.col, "/FULL/PATH/selected-templates.json", names=["Morning", "Exam"]). Ordinary deck names, missing names, duplicates and an empty selection are rejected. The names in this example are placeholders for your actual filtered decks.
Open the JSON in a text editor. Its top-level fields are schema, version and templates; each template has name and config. Under search_terms, each search has its query, limit and order. Check the second term where present, reschedule and the preview values. A limit of 0 remains 0, and reschedule=false remains false. Retain the generated fields rather than deleting a value because it looks like a default.
Keep a copy of the original file. Reordering templates changes build order; changing a query changes which target cards can be gathered. If a source template has a blank query, export stops and asks for an explicit query. Use deck:* only when you actually intend all decks; do not insert it simply to dismiss an error.
4. Add an optional card package without your scheduling
Skip this step when the recipient already has suitable cards. Otherwise, after exporting the JSON, use Empty on each source filtered deck holding cards that belong in the intended ordinary home deck. Empty returns those cards home without deleting them. This deliberately changes their current deck placement in the source collection; the helper does not perform this step for you.
Export the ordinary home deck, including the intended subdecks, as an Anki Deck Package (.apkg). Turn Include Scheduling off. Choose media according to what the cards need. Do not substitute a whole-collection .colpkg. Anki’s ordinary deck-presets option is separate from the filtered definitions supplied in JSON.
This ordering matters. In our actual backend test, exporting the home deck while cards were still borrowed by filters outside that home hierarchy omitted one of the three intended cards. A populated nested filter was carried into the first package as an ordinary deck. After emptying the source filters, the new package carried all three cards in their normal home deck and omitted the now-empty filter definitions. Both packages contained zero review-log rows when their internal collection databases were inspected.
The recipient imports the APKG before the JSON, then checks the expected cards, tags and home-deck names. A merge is not a promise that existing notes remain unchanged: in the tested matching-GUID case, newer source note fields and tags updated according to the import settings, while existing target card records and review-log rows remained unchanged. Inspect those settings and results if the recipient already has related cards. The settings-only JSON has a narrower effect than the separate card-package import.
5. Preflight the complete file in the recipient profile
Switch to the intended destination profile, reopen the console and load the helper there with the command from step 2. Choose a prefix if it helps keep the new views distinct. Shared:: creates names such as Shared::Morning; Shared must be an ordinary parent deck, and a new ordinary parent may be created during the actual import. The preflight itself creates nothing.
plan = import_filtered_templates(mw.col, "/FULL/PATH/templates.json", prefix="Shared::"); pp(plan)
Read every planned name and config, plus normalized_searches and warnings. The helper validates all entries before creating any deck: it rejects invalid JSON, duplicate keys, unknown fields or order values, malformed searches, blank searches, invalid names and destination name collisions. Each template requires one or two nonblank searches. Existing names are rejected rather than silently overwritten or accepted with an automatic + suffix.
The prefix does not rewrite deck:Course, tag:chapter1 or any other part of a query. A cid: card-ID or nid: note-ID search keeps the source ID, which may not identify the intended record in another collection. Check that these targets refer to the intended destination records, or deliberately edit a working copy of the JSON and run preflight again. Anki may normalize the spelling of a valid search. The plan shows that result, and the importer checks the stored query against it after creation.
Preflight checks configuration and search syntax, not the eventual card population or every scheduler-specific build condition. A valid query can match no eligible cards. The recipient’s scheduler setup can also differ from the sender’s; the tested boundary for retrievability ordering is described below. Resolve name or syntax errors before using apply=True.
6. Apply once, then verify the created decks
When the report is correct, run the following command with the same JSON path and prefix. The wrapper prints either the completed result or the partial failure report, and requests an Anki interface refresh. Apply is an intentional change to the destination collection. Do not run it on the source profile by mistake.
exec("try:\n\x20\x20\x20\x20result\x20=\x20import_filtered_templates(mw.col,\x20'/FULL/PATH/templates.json',\x20prefix='Shared::',\x20apply=True)\nexcept\x20PartialImportError\x20as\x20error:\n\x20\x20\x20\x20pp(error.partial_report)\n\x20\x20\x20\x20mw.reset()\n\x20\x20\x20\x20raise\nelse:\n\x20\x20\x20\x20pp(result)\n\x20\x20\x20\x20mw.reset()\n")
For each entry in created, inspect its id, name, card_count and config. The helper always requests a new filtered-deck ID and reads the result back. It checks the stored name and configuration, while allowing the query normalization shown in the plan. The before and after card/revlog totals should match: this operation rearranges eligible local cards; it does not import source cards or review history.
Open the new filtered decks and compare their searches, limits, selection order, rescheduling choice and preview delays with the JSON. Check a known matching card and its original home deck. Equal global counts alone do not prove that your query selected the cards you intended. A filtered view can legitimately be empty, because the helper permits an empty result.
Keep the JSON and the printed result together. If you want another set later, choose new names or a new prefix and preflight again. Repeating the same import into unchanged destination names is rejected; the helper is not an in-place updater.
7. Understand overlap, Empty and partial failure
The first build has priority for overlapping cards. If Morning gathers cards 1, 2 and 3, a later Exam filter cannot also gather 2 and 3 while they remain in Morning. In the illustration, Exam receives card 4. These are synthetic card labels, not runnable queries or real Anki card IDs. Existing filtered decks can similarly hold cards that a new filter would otherwise match.
Common exclusions include suspended cards, buried cards and cards already in another filtered deck. Other eligibility rules can apply. The helper neither unsuspends cards nor empties existing filters. Check the actual search, limits, eligibility and build order before treating a low or zero result as missing data.
The reschedule setting controls the consequences of studying the gathered cards. With rescheduling enabled, answers can change their normal scheduling; preview mode uses its preview behavior instead. Merely transferring a template is separate from subsequently studying it. Verify this option before starting reviews.
If apply fails after earlier templates succeeded, those new decks remain. Read created and new_parent_decks in the printed partial report and inspect the destination. To abandon the new views, use Empty only on the newly created filtered decks identified in that report. Confirm that their cards returned home and that the views are empty; then delete those new filtered decks if they are unwanted. Do not delete an ordinary parent deck that holds other cards or decks.
There is no automatic rollback of the batch and no promised single Undo. Emptying a new filtered deck returns its cards; it does not remove the newly created deck records, erase subsequent study, reverse a prior APKG note update or restore every collection setting. Correct the underlying problem and preflight the remaining work, rather than rerunning the same names blindly.
8. What was verified, and what remains version-dependent
The normal helper passed 21 tests using the real Anki 26.9.3 Python/Rust backend, whose reported application version is 26.09.3, in temporary collections. The suite included a 20-template export/import/re-export, two searches, explicit false/zero values, exact new-ID requests, name and syntax failures, nonempty source histories, and full SQLite logical snapshots for read-only export and preflight. It generated actual review-log data by answering cards through the backend.
The overlap and Empty cases used real backend operations. The partial-failure test deliberately injected an error on the second creation after a real first creation; it verifies the report and retained earlier work, not the frequency of natural failures. A separate reviewer ran four further backend cases: a natural second-item failure with retrievability order 8, a later filtered-parent conflict, a later unknown enum and a query containing an actual card ID from the synthetic source collection. The natural failure retained the first created deck and reported it correctly; the two preflight failures left all SQLite tables unchanged. The card-ID case preserved its literal cid: query and successfully imported into an empty target with zero gathered cards, showing that valid syntax does not guarantee the intended target cards. Separate actual APKG tests checked ordinary card sharing, a matching-GUID merge and the corrected Empty-before-export sequence, then paired that package with this exact helper.
A concrete compatibility limit emerged in the initial test setup: RETRIEVABILITY_ASCENDING, order 8, failed inside Anki while building a source fixture in a new collection without FSRS, before the helper ran. The final 20-template round trip used working orders 0 through 7. All 11 defined order names and numbers passed read-only preflight; that is not a claim that all 11 successfully build in every scheduler setup. If your order fails, inspect the error and any earlier creations, then choose a compatible order or deliberately align the destination setup before retrying.
The program uses Anki’s scheduler and configuration APIs, which can change between versions. It copies the seven fields defined by the tested version; it does not claim future unknown options are supported. No Anki GUI, private user profile, mobile app, cross-platform matrix or large production collection was tested. Keep the source, JSON and result, and repeat the meaningful checks when changing Anki versions.
