Mw.col.find_cards (and AnkiConnect findCards) returns empty for is:due from add-on context, but Anki Browser works (Anki 25.02.6 macOS)

Hi everyone,

I’m encountering a strange issue with my Anki setup and would appreciate any insights.

Anki Version: 25.02.6 (6381f184) Python 3.9.18 Qt 6.6.2 PyQt 6.6.1

AnkiConnect Version (when tested):** Latest from AnkiWeb (code 2055492159, updated 2025-02-25). [Edit: I have since removed AnkiConnect to simplify testing with my own add-on, but the core issue persists with direct mw.col.find_cards calls.]

When querying for due cards from a specific deck (e.g., “Cell and Molecular Biology”) using mw.col.find_cards(‘deck:“Cell and Molecular Biology” is:due limit:10’) from within a Python Anki add-on context, the call returns an empty list . This also occurs for is:new queries.

This is despite the following working correctly:

  1. Anki Browser GUI: Searching deck:“Cell and Molecular Biology” is:due in Anki’s own browser shows many due cards (hundreds, in fact, with due dates in the past relative to my system clock of June 2025).

  2. Direct AnkiConnect findCards Test (when AnkiConnect was installed): A fetch call from a simple local HTML page (127.0.0.1) to AnkiConnect’s findCards action with the query deck:“Cell and Molecular Biology” is:due successfully returned 378 card IDs.

  3. Other API Calls from Add-on Context: My custom Python add-on can successfully make other internal Anki calls (e.g., get Anki version) and serve them via its local HTTP server.

Troubleshooting Done:

The issue occurs even with a brand new, clean Anki profile and a very simple test deck with one verifiably due card.

My custom add-on (MyCardServer) correctly handles the RepeatedScalarContainer by converting its contents to a Python list of integers before JSON serialization

The custom add-on’s HTTP server is running and responding to basic requests (like a /version endpoint).

CORS is correctly configured for my Chrome extension ID and 127.0.0.1.

The problem seems specific to mw.col.find_cards when used with is:due or is:new from an add-on, for certain decks/profiles, on this Anki build. The query mw.col.find_cards(‘deck:“Deck Name”’) (without is:due or is:new) does return all card IDs from the deck successfully via my add-on.

Has anyone encountered a similar discrepancy where mw.col.find_cards(‘is:due’) behaves differently when called from an add-on compared to the Anki Browser GUI? Are there any known issues or specific considerations for programmatic is:due / is:new queries in this environment?

My goal is to reliably fetch due/new cards from an add-on for an external Chrome extension

Any advice or things to investigate further would be greatly appreciated!

Thanks

Tried in the debug console, with zero add-ons installed?

Use TRACESQL=1 to see in a terminal exactly what is being searched for.

Hi everyone I found a work around:

I had my custom Anki add-on (MyCardServer) implement the following logic for a /get_daily_cards endpoint:

  1. Fetch all non-suspended/buried card IDs from the target deck using a simple query: cids = mw.col.find_cards(f’deck:“{deck_name}” -is:suspended -is:buried’).

  2. Iterate through these cids in Python.

  3. For each card (card = mw.col.get_card(cid)):

  • Check card.type == 0 to identify “new” cards.

  • Check card.queue (1, 2, or 3) and card.due (against mw.col.sched.today for reviews or time.time() for learning cards) to manually identify “due” cards.

  1. Collect these manually filtered lists of new and due card IDs and return them via JSON.

This manual filtering approach in Python successfully provides my external Chrome extension with the correct sets of due and new card IDs.

I’m more interested in why it was behaving differently. What happens if you try the steps I suggested above?

I ran the test you suggested, and here’s the summary:

Debug Console (No Add-ons): mw.col.find_cards(‘… is:due’) works perfectly.

Browser Search: works perfectly.

Add-on Context: The same mw.col.find_cards(‘… is:due’) call fails (returns an empty list).

The TRACESQL log confirms that the SQL query generated in all three cases is virtually identical and correct.

The find_cards function with is:due generates the right SQL but returns an empty result set only when called from an add-on’s background thread. The issue seems to be with the execution context, not the query itself.

I suspect it’s something else the add-on is doing. If you can boil it down to a minimal reproducible example and it’s still happening, please pass it on and I’ll take a look.

Hi

I’ve created the minimal reproducible example (MRE) you requested to demonstrate the mw.col.find_cards('is:due') issue.

This still confirms a problem even when I made a manually triggered MRE instead of the automatic one I’m sharing here, the call returns an empty list when run from a minimal add-on at startup despite due cards being available :frowning: .

  • Anki Version: 25.02.6 (6381f184)
  • Platform: macOS-15.3.2-arm64-arm-64bit
    This is what I did:
  1. Ensure you have a deck with cards that are due in the Anki Browser (I used my deck, Cell and Molecular Biology).
  2. I created a new add-on folder (‘ankitest’) and disabled all of my other add-ons
  3. I placed a file named __init__.py inside that folder.
  4. I restarted Anki.
  5. A pop-up window will appear shortly after the profile loads, showing these test results:

— MRE Test —

Anki: 25.02.6

Deck: ‘Cell and Molecular Biology’

Query: ‘deck:“Cell and Molecular Biology” is:due limit:10’

Found IDs: []

Count: 0

Here is the code I used:

from aqt import mw, gui_hooks
from aqt.utils import showInfo

TEST_DECK_NAME = "Cell and Molecular Biology"

def perform_the_core_test():
    if not (mw and mw.col):
        showInfo("MRE: Anki collection (mw.col) not ready for test.")
        return

    query = f'deck:"{TEST_DECK_NAME}" is:due limit:10'
    
    try:
        cids_container = mw.col.find_cards(query)
        cids = [int(cid) for cid in cids_container] if cids_container else []
        
        result_text = (
            f"--- MRE Test --- \n"
            f"Anki: {mw.appVersion}\n"
            f"Deck: '{TEST_DECK_NAME}'\n"
            f"Query: '{query}'\n"
            f"Found IDs: {cids}\n"
            f"Count: {len(cids)}"
        )
    except Exception as e:
        import traceback
        result_text = (
            f"--- MRE Test ERROR --- \n"
            f"Anki: {mw.appVersion}\n"
            f"Deck: '{TEST_DECK_NAME}'\n"
            f"Query: '{query}'\n"
            f"Error: {type(e).__name__}: {e}\n"
            f"Trace: {traceback.format_exc()}"
        )
    
    print(result_text)
    showInfo(result_text)

gui_hooks.profile_did_open.append(lambda: mw.progress.timer(500, perform_the_core_test, False))
print(" MRE initialized")

And here is me searching the deck for due cards

Screenshot 2025-06-12 at 2.39.46 PM

Your query in the browser and in the code does not match.

1 Like

ok whoops I see that when I search it in browse, anki automatically changes the search query from deck:“Cell and Molecular Biology” is:due to “deck:Cell and Molecular Biology” is:due

That was very subtle, and I didn’t notice that anki was changing my search when debugging until you pointed it out

I modified the code

from aqt import mw, gui_hooks, appVersion
from aqt.utils import showInfo

TEST_DECK_NAME = "Cell and Molecular Biology"

def perform_the_core_test():
    if not (mw and mw.col):
        showInfo("MRE: Anki collection (mw.col) not ready for test.")
        return

    query = f'"deck:{TEST_DECK_NAME}" is:due'
    
    try:
        cids_container = mw.col.find_cards(query)
        cids = [int(cid) for cid in cids_container] if cids_container else []
        
        result_text = (
            f"--- MRE Test --- \n"
            f"Anki: {appVersion}\n"
            f"Deck: '{TEST_DECK_NAME}'\n"
            f"Query: '{query}'\n"
            f"Found IDs: {cids}\n"
            f"Count: {len(cids)}"
        )
    except Exception as e:
        import traceback
        result_text = (
            f"--- MRE Test ERROR --- \n"
            f"Anki: {appVersion}\n"
            f"Deck: '{TEST_DECK_NAME}'\n"
            f"Query: '{query}'\n"
            f"Error: {type(e).__name__}: {e}\n"
            f"Trace: {traceback.format_exc()}"
        )
    
    print(result_text)
    showInfo(result_text)

gui_hooks.profile_did_open.append(lambda: mw.progress.timer(500, perform_the_core_test, False))
print(" MRE initialized")

And was able to see all of the cards that was due. I can’t believe I didn’t notice that especially with all the time I’ve spent on this bro wow. Time to finish the spaced repetition chrome extension :call_me_hand: Right now just trying to figure out how to send the data back to anki and have it update whenever I do a card lol thanks for all your help man !!!

Not so subtle - I believe the issue was you put ‘limit:10’ in your add-on’s query.

2 Likes

:man_facepalming: :man_facepalming: :man_facepalming:

yup

i apologize for wasting your time :smiling_face_with_tear:

2 Likes