bulkAddNotes |
Add many notes with one undo entry, fast duplicate pre-check, and per-note error reporting. Params: notes (upstream addNotes shape), atomic (default true), allowDuplicates (default false; per-note options.allowDuplicate overrides), suspend (default: config suspendNewCards, which ships false — stock behavior), dryRun (default false — see dry-run note below; returns {wouldAdd: <count>, wouldSuspend: <bool>, skipped, undoEntry: null}). Suspended-draft mode (opt-in): with suspend: true (or config suspendNewCards: true) the cards this batch creates are left suspended — in the same undo entry as the adds, so one Undo removes notes and cards — and listed in suspended. See "Suspension control" below. Returns {added, suspended, skipped: [{index, reason}], undoEntry}. |
bulkUpdateNoteFields |
Update fields and/or tags on many notes (notes: [{id, fields?, tags?}]; tags replaces the whole tag list). One undo entry; same atomic contract. Entries whose requested fields AND tags already byte-match the note are not written and are reported in unchanged (same only-write-what-changed rule as bulkAddTags); updated lists only notes actually written, and undoEntry is null when nothing was. dryRun (default false) returns {wouldUpdate: [noteIds], unchanged, skipped, undoEntry: null}; adding diff: true (dry-run only — on a real run it errors [invalid_param]) also returns preview: [{noteId, field, before, after}] — one entry per changed field plus one per changed tag list under the reserved field name __tags__ (space-joined values, emitted after that note's field rows), unchanged fields omitted, capped at maxPreview (default 20) — plus previewTruncated. Returns {updated, unchanged, skipped, suspensionPreserved, schedulingPreserved, undoEntry} — the two booleans are per-call post-checks (SPEC §31.2): the written notes' cards' (queue, due, ivl) are snapshotted before the writes and re-read after, so "a field edit never moves a card" is a verified fact, not a promise (false = alarm; always present on the real response, absent on dry runs). Tags are compared and previewed CANONIFIED — anki sorts, strips, case-insensitively de-duplicates and registry-matches tags on save, so after in a __tags__ preview row is what will really be stored, and a byte-identical repeat of a non-canonical request ("gamma delta", ["b","a"], ["x","X"]) now lands in unchanged instead of silently re-writing the note. |
bulkAddTags |
Add tags (str split on whitespace, or list) to many notes by id. Only notes actually changed are written and reported in updated. dryRun (default false) returns {wouldUpdate: [noteIds], skipped, undoEntry: null} — already-fully-tagged notes appear in neither list, same as real mode. Returns {updated, skipped, undoEntry}. |
addImageOcclusionNote |
Create a native (built-in) Image Occlusion note from an image {path} or {data, filename} plus occlusions (native string, or array of normalized 0–1 rects with optional ordinal), header, backExtra, tags, deckName, hideAllGuessOne. Returns {noteId, cardIds, undoEntry}. |
getImageOcclusionNote |
Read an IO note: {imageFilename, occlusions[] (one entry per shape with ordinal; rects as floats, other shapes as raw properties), header, backExtra, tags, occludeInactive}. |
updateImageOcclusionNote |
Update any subset of occlusions / header / backExtra / tags on an IO note (omitted params are kept exactly). Returns {undoEntry} (the actual undo entry name; was null before the round-2 undoLabel upgrade; null again when the update is a no-op — every requested value already matched the note, so nothing undoable was written and no entry was created). The image itself cannot be changed here — cropImageOcclusionImage is the supported way to change (crop) it. |
cropImage |
Crop a media image into a new media file (the original is kept). Params: filename (bare media filename), rect {left, top, width, height} as normalized 0–1 floats (clamped to the image, never padded), optional noteIds (every occurrence of the old filename in those notes' fields is rewritten to the new one, one undo entry). Returns {newFilename, width, height, notesUpdated, undoEntry} (undoEntry: null when no notes were rewritten — the media write alone is not undoable). Not for IO base images — see semantics below. |
cropImageOcclusionImage |
Crop a native IO note's base image and remap every occlusion rect into the cropped frame, atomically (one undo restores both the image and the rects). Params: noteId, rect (same normalized shape). Rects falling fully outside the crop are dropped; straddling rects are clipped to the crop edge; dropping all rects is refused. Returns {newFilename, occlusionsKept, occlusionsClipped, occlusionsDropped, cardIds, undoEntry}. |
queryRevlog |
Read-only review-history query filtered by cardIds / noteIds (both deduplicated) / deckName (incl. subdecks) / sinceMs (inclusive) / untilMs (exclusive), limit default 5000, offset default 0. Returns {rows, total, truncated, nextOffset} — total is the count of distinct matching rows, truncated: true means more rows remain beyond this page, and nextOffset (else null) resumes the walk, so hitting exactly limit rows is never a silent cutoff. |
createBackup |
Trigger a .colpkg backup into the profile's backups/ folder. {force} default true. Returns {created: bool} — false means nothing changed since the last backup, not a failure. |
plusInfo |
Version/action/docs metadata for this add-on, including actionDocs — per-action {summary, params, returns}: a one-line description, the wrapper's real parameter signature with JSON-style defaults (e.g. stripHtml=true), and a shape sketch of what the action returns, so callers can discover both ends of every call without reading this table. Also errorCodes — the full error vocabulary as {code: {retryable, reachable, meaning}}, so a client builds its retry table at runtime instead of hardcoding one — errorPrefixNote (which errors carry a code and which do not), and recipes, a list of named {name, description, example} call patterns (raw field projection, verified-sync contract, dry-run-then-write, undo-label convention, suspended-draft workflow and safe deck export — the two recipes that document where this fork deliberately does not behave like Anki — lean deck sweep, reading errors, empty-cards cleanup). Also version and specRevision: specRevision is the SPEC revision this build implements (15 and up), and the version's minor moves whenever default behavior changes, so a client that caches this response can detect that in a field instead of in prose. Works with no profile open, and during a sync. |
renderCard |
Render cards' question/answer exactly as Anki's template pipeline produces them. Params: cardIds, format (default "html" — verbatim rendered HTML, including any <script>/<style> blocks the card template itself contains; "body" removes those blocks; "text" returns visible text only, same conventions as notesSlim — usually what an LLM caller wants), cssMode (default null = format-dependent: "perCard" for html/body, "omit" for text; "byNotetype" hoists the stylesheet into one top-level cssByNotetype: {notetype: css} and drops the per-card css). Returns {cards: [{cardId, question, answer, css?, deckName, modelName, notetype, ord}]} — one entry per input id in input order; bad ids and per-card render failures become per-item {cardId, error} entries, never a hard failure. question/answer exclude the notetype-CSS <style> wrapper; [sound:...] tags render as [anki:play:...] markers. The CSS knob is the payload lever: a notetype stylesheet repeated once per card measured 90–95% of a multi-card response (20 cards: 92.2% smaller with byNotetype, 97.1% with the new text default). Read-only, undo stack untouched. |
notesSlim |
Compact, paginated, HTML-stripped note reader (built for LLM consumption). Params: exactly one of query (Anki search, verbatim; "" = all) / noteIds (caller order kept), fields (name filter, default all), stripHtml (default true; media filenames and [sound:...] kept; cloze markup like {{c1::France}} passes through verbatim, no bracketed-hint conversion; false returns raw field HTML byte-exact), maxFieldLength (default 400, 0 = off, cut with …), offset, limit (default 200, clamped to 2000). Raw-fidelity field projection: fields=[...] + stripHtml: false + maxFieldLength: 0 reads the chosen fields' exact stored HTML — the read-before-edit primitive. Also omitEmptyFields (default false; true drops fields whose emitted value is empty — on a 19-field notetype with 4 populated fields that is ~half the payload, and it runs faster). Returns {total, notes: [{noteId, modelName, tags, fields, truncatedFields}], missing, nextOffset} (nextOffset: null on the last page); truncatedFields explicitly names the fields cut by maxFieldLength ([] when none — the … marker alone is ambiguous). Query path is ascending-noteId order. Breaking change (round 3): under noteIds, total now counts the ids found (it used to count the ids requested, silently including dead ones) and the dead ids are listed in missing, so len(noteIds) == total + len(missing); nextOffset is null when no surviving id remains past the page. Cost note: total/missing are window-independent, so every page re-scans the whole noteIds list — a full paged pass is O(N²/L) (measured: ~103 ms added at N=5,000 / limit: 200, ~1.7 s at N=20,000). Read total/missing off the first page and carry them; they cannot change mid-pass. Read-only. |
mediaThumbnails |
Base64 thumbnails of collection media images — aspect-preserved, never upscaled (small images return at native size). Params: filenames (bare media names), maxDim (default 320, clamped to 1024), format ("jpeg" default or "png"), quality (JPEG 0–100, default 70). Returns {thumbnails: [{filename, data, width, height}]} with per-item {filename, error} entries; input order. JPEG flattens transparency — request png to keep alpha. Pure read. |
mediaExists |
Cheap read-only membership probe: do these bare media filenames exist in the media folder? Params: filenames ([str]; non-string entries are a hard parameter error). Returns {results: [{filename, exists, actualName}]} in input order; a malformed or path-carrying name is simply exists: false, never an error. actualName is the true stored spelling when the filesystem matched case-insensitively (macOS/Windows) and null otherwise — a name that only differs in case passes exists locally but 404s on AnkiWeb/Linux/iOS. Use this instead of diffing getMediaFilesNames (which returns the entire listing) for membership tests. |
storeMediaFilesBulk |
Store many media files in one call. Params: files: [{filename, data | path}] — per item a bare filename plus exactly one of data (base64) or path (absolute; ~ expanded). Returns {stored: [...]} in input order: {requested, actual} on success — actual is the filename Anki actually stored, surfacing its dedup/rename decision (same name + same bytes dedups to the same name; same name + different bytes renames to dup-<sha1>.<ext>; originals are never overwritten) — or {requested, error} per-item failures that never abort the batch. Media writes are not undoable (no undo entry). Stock storeMediaFile is unchanged. |
bulkSuspend |
Suspend or unsuspend many cards with one undo entry (Agent Connect: Bulk Suspend). Params: cardIds (deduplicated; unknown ids silently dropped), suspend (default true; false uses the backend restore op, which also unburies buried cards). Returns {changed, changedIds, undoEntry} (changedIds = the cards actually written, deduplicated, unknown ids dropped); changed: 0 → changedIds: [], undoEntry: null and the undo stack is untouched. |
bulkSetDueDate |
Set the due date on many cards with one undo entry (Agent Connect: Bulk Due Date). Params: cardIds (deduplicated; unknown ids dropped), days string — "0" due today, "5" in 5 days, "1-7" uniform-random per card in the range, "3!" also forces the interval to 3 days — preserveSuspended (default: config preserveSuspendedOnReschedule, which ships true), dryRun (default false; returns {wouldChange, wouldChangeIds, wouldUnsuspend, wouldUnbury, wouldResuspend, undoEntry: null} with zero writes). The grammar ([0-9]+(-[0-9]+)?!?) is validated before the undo entry is created, so a bad string errors with the undo stack genuinely untouched. Applies to every existing card regardless of state (new cards become review cards). ⚠ Anki's own set_due_date RESURRECTS suspended and buried cards — rescheduling a selection that includes suspended leeches silently brings them back; the revived ids are reported in unsuspended / unburied. ⚠ DELIBERATE DEVIATION FROM ANKI: by default this add-on then puts the suspensions back, inside the same undo entry, and lists them in resuspended (buried cards are deliberately not re-buried); pass preserveSuspended: false (or flip the config key) for stock behavior. Cards left in review = unsuspended − resuspended. It also always writes (no no-op suppression: "1-7" is nondeterministic by design), so a byte-identical repeat still creates an undo entry. Returns {changed, changedIds, unsuspended, unburied, resuspended, undoEntry}. |
exportDeckApkg |
Export one deck and its subdecks to an .apkg file. Params: deckName, outPath (default ~/Downloads/<sanitized-deck>-<YYYY-MM-DD>.apkg; ~ expanded; must be a file path — an existing directory or trailing slash is rejected; parent dir must exist), includeScheduling (default true), includeMedia (default true; false still writes an empty media zip member), allowFilteredOmission (default false — round 4's one deliberate behavior change, SPEC §§17/29.3). Fails closed on filtered-deck damage: a deck-scoped export silently OMITS every note whose cards are all sitting in out-of-scope filtered decks, ships partially-filtered notes scheduling-reset (a real class deck nearly went out missing 141 cards / 96 notes — zero warnings from Anki), and ships FOREIGN notes whenever a filtered deck nested inside the export subtree holds cards homed outside it (scheduling-reset, filter recreated as a regular deck). If either set is non-empty, the default REFUSES with [cards_in_filtered_decks] naming counts + filtered-deck names; run emptyFilteredDeck and re-export, or pass allowFilteredOmission: true to export anyway with the damage itemized in warnings — {code: "cards_in_filtered_decks", count, decks, notesOmitted} and/or {code: "foreign_cards_in_scope_filters", count, decks}. warnings is always present ([] when clean — additive key). The check runs before any filesystem work, so a refusal leaves no file and burns no collision suffix. Never overwrites: -2, -3, … appended before the extension (report.apkg → report-2.apkg). Deck presets are never exported (fixed with_deck_configs=False, matching Anki's own dialog default); modern (non-legacy) package format. Returns {path, sizeBytes, notesExported, warnings}. No undo entry; collection unchanged. |
syncNow |
Start a normal AnkiWeb sync as a background job and return immediately ({started: true, mediaSync} or {started: false, reason}); poll syncStatus for the outcome. Normal sync only, zero dialogs: a required full sync is refused (job error full_sync_required). Media syncing follows the profile setting and is watched to completion (media_sync_failed in job.error.code on failure). |
syncStatus |
Read-only AnkiWeb sync probe: {loggedIn, job, mediaSyncing, mediaSecondsSinceLastSync, lastSyncMs, modMs, required, serverChecked}. required comes from a server status round-trip (timeoutSecs default 8) unless localOnly: true (local dirtiness only, and then full_sync_required when the schema changed). serverChecked is true only when this call really completed a network round-trip — Anki's backend answers locally whenever the collection is dirty, so false means "not verified by this call", never "the server says no". Never starts a sync, never clears stored auth, never opens dialogs. Verified-synced iff job.state == "done" && required == "no_changes" && mediaSyncing == false. The syncing state is guaranteed to end: the completion handler cannot leave it stuck, and a job still syncing an hour after it started is reaped into a terminal error — so polling syncStatus until the state leaves syncing, and the retryable [sync_in_progress] that tells you to, always terminate. |
ankihubStatus |
Read-only AnkiHub add-on probe: {installed, enabled, loggedIn, addonVersion, testedAddonVersion, appUrl, decks (with isAnkingDeck), compatible} plus problems when feature detection finds drift. Never network, never raises for a missing/disabled add-on. |
ankihubSuggestNoteUpdate |
Submit ONE change suggestion for an existing AnkiHub note through the installed AnkiHub add-on's own pipeline. Params: note (Anki note id), changeType (wire value, e.g. updated_content), rationale (≤1023 chars), source (only where the dialog shows one — required for content changes on the AnKing deck), autoAccept. Returns {result, comment}. |
ankihubSuggestNewNote |
Suggest a brand-new note to an AnkiHub deck. Params: note, rationale, optional source/deckId (default: resolved from the notetype), autoAccept, resubmitAsChangeOnDuplicate (default true — a duplicate-anki-id conflict is resubmitted as a change suggestion, mirroring the add-on's conflict dialog). Returns {result, resubmittedAsChange}. |
ankihubStageOptionalTagSuggestion |
Stage an AnkiHub optional-tag suggestion — a human submits; this action never touches AnkiHub's code or servers (their ToS prohibits scripted posting, and even opening their dialog from code fires network calls, so the action stops at the Browser selection; see SPEC §33). Params: tag (canonical AnkiHub_Optional::<TagGroup>::<Tag> shape, ≥3 non-empty :: segments — enforced, because the AnkiHub dialog silently ignores anything else), noteIds (≤500 unique ids; all-or-nothing: one missing note refuses the whole call, and every note must sit on exactly ONE AnkiHub deck — a local add-on-db read), dryRun, undoLabel. The real run tags the notes locally as ONE undoable batch (Agent Connect: Stage Optional Tag), opens the Browser on exactly those notes, and stops — the human right-clicks the selection → AnkiHub → "Suggest Optional Tags", reviews, and presses Submit in AnkiHub's own dialog (both AnkiHub-touching clicks are the human's). GUI-coupled: needs the Anki window for the Browser; dryRun runs the full validation chain (add-on compatibility, login, notes, single deck) and predicts the tag write with nothing written and nothing opened. Requires the AnkiHub add-on installed + logged in. Returns {tagged, alreadyTagged, ankihubDeckId, browserOpened, nextStep, undoEntry} — re-staging an already-tagged set is a reported no-op write that still reopens the Browser selection; re-running is always safe (already-tagged notes are skipped). |
checkDeckIntegrity |
Read-only audit of a deck and its subdecks. Params: deckName, includeOrphanMedia (default false), orphanMediaLimit (default 100, 0 = count only). Returns {missingMedia: [{noteId, field, filename}], unbalancedCloze: [{noteId, field}], clozeCardMismatch: [{noteId, expectedOrds, actualOrds}], clozeNotesWithoutCloze: [noteId], orphanMediaCollectionWide, orphanMediaCount, orphanMediaTruncated, notesChecked}. Missing media = per-field references (img/audio/object/[sound:...]) whose file is absent (latex-generated images exempt — regenerated on demand); unbalanced cloze = {{cN:: opens vs }} closes per field (simple balance check — literal }} text with no cloze opens is also flagged); cloze/card mismatch = cloze-type notes whose fields' cloze numbers no longer match their existing card ordinals — except a cloze note with zero effective cloze numbers carrying only Anki's own placeholder card (ord 0), which is Anki's maintained state, not drift: those note ids are reported in clozeNotesWithoutCloze instead (an authoring smell — a "cloze" note that will never cloze anything). Breaking change (round 3): orphanMedia is renamed orphanMediaCollectionWide — every other list here is deck-scoped, this one is collection-wide by nature (a file unreferenced by this deck may be used elsewhere), and on a real collection it returned 37,243 entries / 1.6 MB that read as if they belonged to the deck. It is null unless requested, capped at orphanMediaLimit with the full size always in orphanMediaCount and a orphanMediaTruncated flag: media-dir files referenced by no note field or notetype template anywhere, excluding _-prefixed static files and dotfiles. No undo entry; collection unchanged. |
bulkReplaceInFields |
Find/replace on the raw HTML of one named field across many notes, one undo entry (Agent Connect: Replace in Fields). Params: exactly one of query/noteIds (ids deduplicated), field, find (non-empty), replace, isRegex (default false — python re semantics, no backtracking-bomb protection; a non-compiling pattern is a parameter error; in literal mode replace is inserted verbatim, in regex mode \1-style templates expand), caseSensitive (default true), dryRun (default false), atomic (default true), maxPreview (default 20). Real run returns {changed, matchesTotal, unchanged, skipped: [{noteId, reason}], suspensionPreserved, schedulingPreserved, undoEntry} (the same per-call scheduling/suspension post-check as bulkUpdateNoteFields — SPEC §31.2); notes where nothing matched — or every match replaced itself byte-identically — land in unchanged and are never written. dryRun returns {wouldChange, matchesTotal, unchanged, skipped, preview: [{noteId, before, after}] (capped at maxPreview), previewTruncated, undoEntry: null} with zero writes — preview a batch before committing it. |
undoStatus |
Read-only view of Anki's own undo stack: {undo, redo, lastStep} — what a single Undo / Redo would do right now (null when there is nothing), plus anki's monotonic step counter, read straight off the backend. This is the observed truth behind every action's undoEntry (which is only the API's own report of what it set): snapshot lastStep, run a write, and check that the entry appeared. lastStep is monotonic within a session — clearing the stack (Check Database, a schema mod) keeps the counter rather than rewinding it. No params, no writes. |
renameDeck |
Rename a deck in place — the whole subtree follows in one undoable op, and the options-preset assignments, per-deck descriptions and collapse state all survive (deck ids are stable; the createDeck+changeDeck+deleteDecks workaround silently resets subdeck presets to Default, changing scheduling with no error). Params: oldName (must exist; matched case-insensitively), newName (full :: path; renaming under an existing parent is a move; missing parents auto-created; byte-identical → no-op), dryRun (default false — predicted {wouldRename, configWillBePreserved: true, cardsAffected, undoEntry: null}; configWillBePreserved is a static contract statement about the in-place rename path, not a post-check — the real run's configPreserved is the post-check), undoLabel. A newName (or implied descendant name) already taken by any deck other than the one being renamed onto it — the subtree's own members included — is refused with [duplicate]; anki's own rename would silently land on Occupied+ instead (a case-only respelling stays legal, and newName must be normalized: empty or whitespace-padded :: components are [invalid_param]). Returns {renamed: [{from, to}] (re-read from the collection AFTER the op), configPreserved (an actual post-check of every preset id, not an assumption), cardsAffected (cards homed in or visiting the subtree), undoEntry}. |
bulkSetFlag |
Set or clear (flag: 0) the colored flag on many cards, one undo entry (Agent Connect: Bulk Flag). Flags are the human→agent channel — a human flags cards in review, the agent fixes them and finally clears the flag; stock's only route (setSpecificValueOfCard) clobbers the whole flags byte with no undo entry. Params: cardIds (deduplicated; unknown ids dropped), flag (0–7, pre-validated), dryRun (default false — {wouldUpdate, unchanged, undoEntry: null}), undoLabel. Returns {updated, unchanged, undoEntry} — unchanged = cards that already carried the requested flag (read from their real pre-op values), so a repeat call is a reported no-op with nothing written; on a (never-observed) backend/precheck disagreement updated is re-read from the post-op flags. |
renameTag |
Rename/move a tag and its :: subtree with anki's own segment-aware op: lab1 → lab01 rewrites lab1 and lab1::* but never lab10, matched case-insensitively (stock replaceTagsInAllNotes matches whole tags only — it strands the children, and it writes with no undo entry at all). Renaming onto an existing tag merges the trees — disclosed in merged. Params: oldTag/newTag (single tags, no spaces; a oldTag matching nothing raises [not_found] rather than "succeeding" at renaming nothing), dryRun (default false — {notesUpdated, wouldRewrite: [{from, to}], merged, undoEntry: null}, the preview that proves the prefix safety before anything is written; dry notesUpdated counts the notes carrying the affected tags, a zero-write prediction of the real run's backend count), undoLabel. Returns {notesUpdated (the backend's own count), tagsRewritten: [{from, to}] (re-read from the post-op tag registry — on a merge the existing spelling wins and the pair says so), merged, undoEntry}. Ghost tags (registered but carried by no note) are not renamed by the backend; an all-ghost match reports the no-write shape with the undo stack untouched. |
filteredDeckReport |
Read-only census of filtered decks: one name-sorted row per filtered deck with cardCount and homeDecks ({home deck name: count}, from the cards' odid). Params: deckName (default null = every filtered deck, empty ones included). Naming a regular deck scopes to cards whose home lies in that subtree (rows holding none are dropped) — the pre-export probe: totalCards is then the home-side count exportDeckApkg's fail-closed check trips on (the check also flags nested filters holding foreign-homed cards; the unscoped report shows those), and the rows name the decks to empty. Naming a filtered deck returns just that deck's row. Returns {filteredDecks: [{filteredDeck, filteredDeckId, cardCount, homeDecks}], totalCards}. The census half of the filtered-deck lifecycle — createFilteredDeck/rebuildFilteredDeck are the build half, emptyFilteredDeck the remediation. |
emptyFilteredDeck |
Send every card in ONE filtered deck back to its home deck — the API equivalent of the filtered deck's own Empty button (col.sched.empty_filtered_deck; cards return to did = odid with scheduling intact), and the remediation step exportDeckApkg's refusal points at. Params: exactly one of deckName/deckId, dryRun (default false — {wouldReturn, homeDecks, undoEntry: null}), undoLabel. One undo entry (Agent Connect: Empty Filtered Deck); a single undo puts the cards back in the filter. Emptying an already-empty filter is a reported no-op with nothing written. A regular deck is refused [validation_error]. Returns {returned (pre-op residency cross-checked against the post-op count), homeDecks (where they went, read before the op), undoEntry}. createFilteredDeck/rebuildFilteredDeck are the build half of the same filtered-deck lifecycle. |
createFilteredDeck |
Create and build a filtered (cram) deck from a search in one undoable op (Agent Connect: Create Filtered Deck) — the API version of Tools → Create Filtered Deck: "cram everything tagged PI9 that's due" is {name: "PI9 cram", searchQuery: "tag:PI9 is:due"}. Params: name (must be normalized — padded/empty :: components are [invalid_param] — and untaken: a taken name is refused [duplicate] instead of inheriting Anki's silent name+; missing parents are created as regular decks inside the same entry), searchQuery (validated and normalized by Anki's own parser; empty is refused — say deck:* out loud if you mean the whole collection), limit (default 100; 1–4294967295), order (default "random"; one of oldestReviewedFirst, random, intervalsAscending, intervalsDescending, lapses, added, due, reverseAdded, retrievabilityAscending, retrievabilityDescending — validated here because the backend accepts garbage silently), secondFilter ({searchQuery[, limit, order]}, defaults 20/"due"; two terms max — Anki saves a third but never gathers it), reschedule (default true), dryRun, undoLabel. Defaults mirror the GUI's own new-filtered-deck template. Suspended cards, buried cards, and cards already in another filtered deck are never gathered (Anki's own rule); gathering zero cards is refused [validation_error] with nothing created (the GUI's own refusal); a filtered parent is [validation_error]. dryRun sizes the deck without creating anything: {wouldCreate, wouldGather, exact, wouldGatherMin, wouldGatherMax, name, terms, undoEntry: null} — exact for a single filter (probe-verified min(limit, eligible)); with a second filter overlapping the first under a binding limit the split is genuinely order-dependent (RANDOM is nondeterministic), so the bounds bracket every outcome and wouldGather is the upper bound. Returns {deckId, name (read back post-op), cardsGathered (post-op count), terms ({search, limit, order, eligible} each), undoEntry}; a single undo deletes the deck and returns every card. Anki's build selects the new deck as current (GUI parity, disclosed in preserves). |
rebuildFilteredDeck |
Empty-then-regather ONE filtered deck by its saved search terms — the deck's own Rebuild button (col.sched.rebuild_filtered_deck), one undoable op (Agent Connect: Rebuild Filtered Deck). Params: exactly one of deckName/deckId, dryRun, undoLabel. Returns {cardsGathered (post-op residency), returnedFirst (pre-op residency — the cards the rebuild first sent home), undoEntry} — both halves observed, never echoed. Rebuild-to-zero is legal (deck left empty — Anki's behavior, not an error); rebuilding an empty deck whose saved terms would also gather nothing is a reported no-op ({cardsGathered: 0, returnedFirst: 0, undoEntry: null}, nothing written). A regular deck is refused [validation_error]; missing → [deck_not_found]. dryRun returns {wouldReturn, wouldGather, exact, wouldGatherMin, wouldGatherMax, terms, termsIgnored (saved terms beyond the two Anki actually gathers), undoEntry: null} — the deck's own cards count as re-gatherable; a saved term that no longer parses (external writer) is [validation_error] naming the term, on both paths. Rebuild preserves the current-deck selection (a build does not). |
getEmptyCards |
Read-only — Anki's Tools → Empty Cards report as data: per note, the empty cards' ordinals (ords), exactly which card ids deleteEmptyCards would delete (willDeleteCards), and protectedCard — non-null iff EVERY card of the note is empty, in which case that first card is kept (deletion never removes a note's last card, anki's own dialog rule). Params: deckName (default null = collection-wide; a deck scopes to notes with ≥1 empty card homed in that subtree, odid-aware — a listed note still reports all its empty cards). checkDeckIntegrity's clozeCardMismatch detects the condition; this is the actionable report. Returns {notes: [{noteId, ords, willDeleteCards, protectedCard}], total}. |
deleteEmptyCards |
Delete empty cards as ONE undoable batch (Agent Connect: Delete Empty Cards), honoring the same protection as Anki's own dialog with its shipped "keep notes" default: an all-empty note keeps its first card (listed in protected: [{noteId, cardId}]) and the note itself is NEVER deleted — notesPreserved is an actual post-check of that promise, not an assumption. Params: noteIds (default null = everything the live report finds; explicit ids not in the report land in skipped with "no empty cards" / "note was not found"), dryRun (default false — {wouldDelete: [cardId], notesAffected, protected, skipped, undoEntry: null}), undoLabel. Nothing deletable → data no-op, nothing written. Returns {cardsDeleted, deletedCardIds, notesAffected, protected, notesPreserved, skipped, undoEntry}. |