Help with "Invalid add-on manifest" Error and Session-Only Preference Persistence in Anki Desktop

Hi everyone,

I’m trying to create an add-on (`field_persistence`) to persist field visibility preferences across notes in Anki Desktop during a single session, resetting when Anki closes. The add-on uses `pycmd` to handle `load_prefs` and `save_prefs` commands, storing preferences in a global variable. However, I’m stuck with an "Invalid add-on manifest" error when installing the add-on via zip, and it doesn’t load when placed in the `addons21` folder.

**Details**:
- **Anki Version**: [Version <U+2068>25.07.5 (7172b2d2)<U+2069> Python 3.13.5 Qt 6.9.1 Chromium 122]
- **OS**: [Windows 11]
- **Add-on Goal**: Persist field visibility (e.g., hide/show `English_Translation`) across notes in a custom note type (Notetype 4) during a session, similar to how `localStorage` works in AnkiDroid.
- **Error**: When installing `field_persistence.zip` via `Tools > Add-ons > Install`, I get: "Error installing field_persistence.zip: Invalid add-on manifest." The add-on doesn’t appear in `Tools > Add-ons` or in the debug console (`import addonmanager; print(addonmanager.get_addons())`).

**Add-on Structure**:

<Anki2/addons21>/
└── field_persistence/
├── init.py
└── meta.json

**`_init_.py`**:
```python
import logging
from anki.hooks import addHook
from aqt import gui_hooks

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(_name_)
logger.debug(“Field Persistence Add-on initialized”)

session_prefs = {}

def on_pycmd(cmd, data, callback):
global session_prefs
logger.debug(f"Received pycmd: {cmd}, data: {data}“)
try:
if cmd == “load_prefs”:
callback(str(session_prefs) if session_prefs else “{}”)
elif cmd == “save_prefs”:
session_prefs = eval(data)
logger.debug(f"Saved preferences: {session_prefs}”)
callback(“Preferences saved”)
else:
logger.debug(f"Unknown pycmd: {cmd}“)
callback(“Unknown command”)
except Exception as e:
logger.error(f"Error in pycmd {cmd}: {str(e)}”)
callback(f"Error: {str(e)}")

addHook(“pycmd”, on_pycmd)

def on_profile_closed():
global session_prefs
session_prefs = {}
logger.debug(“Profile closed, preferences cleared”)

gui_hooks.profile_will_close.append(on_profile_closed)

meta.json:
{
“name”: “Field Persistence”,
“mod”: 0,
“id”: “field_persistence”
}

I hope somebody can clarify this.
BTW it would be great if persistant storage would work by default on ankidesktop.
and as long as that is not possible, perhaps a shared add on on ankiweb would be nice.
I am just starting mostly with help of AI.

Questions:

  1. Why am I getting the “Invalid add-on manifest” error? Is there an issue with my meta.json or zip structure?

  2. How can I debug why the add-on isn’t loading in addons21? Are there specific logs or debug console commands to try?

  3. Is there a way to achieve session-only preference persistence in Anki Desktop without an add-on, e.g., using sessionStorage or cookies in QtWebEngine?

  4. Could my Anki version be causing issues with add-on loading or storage persistence?

Template Context: My back card template toggles field visibility (e.g., English_Translation) and tries to save preferences using pycmd or sessionStorage. Here’s the relevant JavaScript:

Questions:

  1. Why am I getting the “Invalid add-on manifest” error? Is there an issue with my meta.json or zip structure?

  2. How can I debug why the add-on isn’t loading in addons21? Are there specific logs or debug console commands to try?

  3. Is there a way to achieve session-only preference persistence in Anki Desktop without an add-on, e.g., using sessionStorage or cookies in QtWebEngine?

  4. Could my Anki version be causing issues with add-on loading or storage persistence?

Template Context: My back card template toggles field visibility (e.g., English_Translation) and tries to save preferences using pycmd or sessionStorage. Here’s the relevant JavaScript:

const storage = typeof pycmd !== ‘undefined’ ? sessionStorage : localStorage;
function savePreferences(preferences) {
try {
storage.setItem(‘fieldPreferences’, JSON.stringify(preferences));
console.log(`Saved preferences to ${storage === sessionStorage ? ‘sessionStorage’ : ‘localStorage’}:`, preferences);
} catch (e) {
console.error(`Error saving to ${storage === sessionStorage ? ‘sessionStorage’ : ‘localStorage’}:`, e);
}
}
function loadPreferences() {
const stored = storage.getItem(‘fieldPreferences’);
if (stored) {
try {
const preferences = JSON.parse(stored);
console.log(`Loaded preferences from ${storage === sessionStorage ? ‘sessionStorage’ : ‘localStorage’}:`, preferences);
} catch (e) {
console.error(`Error parsing ${storage === sessionStorage ? ‘sessionStorage’ : ‘localStorage’}:`, e);
}
}
}
Thank you

1 Like

The add-on should contain a manifest.json file with the package and name keys. Example:

{
  "package": "my_addon",
  "name": "My Add-on"
}

There are no logs for this specific error, though it’s most commonly caused by a missing manifest.json or invalid JSON or non-UTF8 encoding. For other add-on startup errors (issues in your code), you usually get a traceback.

1 Like

Thank you for your quick reply.
Yes i forgot to say that i double checked with notepad and saved as utf8
and above are the files content and file names, they are correct right ?
still it’s not working, it’s driving me crazy.

Please post the file contents here. Make sure to use the Preformatted text option in the forum’s editor.

I’m not sure if I understand correctly, but you put the manifest contents inside meta.json.
Meta.json can be empty.
Inside manifest.json, you put this.

{
  "package": "my_addon",
  "name": "My Add-on"
}

You can make a type of note or in the form of an addon, below is an example of a type of note.

Front

<div class="tabs">
  <button class="tab-button" id="btn-front" onclick="showTab('front')">Front</button>
  <button class="tab-button" id="btn-back" onclick="showTab('back')">Back</button>
</div>

<div class="content">
  <div id="front" class="tab-content">
    {{Front}}
  </div>
  <div id="back" class="tab-content" style="display:none;">
    {{Back}}
  </div>
</div>

<script>
  function showTab(tabId) {
    document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
    document.querySelectorAll('.tab-button').forEach(el => el.classList.remove('active'));
    const contentToShow = document.getElementById(tabId);
    if (contentToShow) contentToShow.style.display = 'block';
    
    const buttonToActivate = document.getElementById('btn-' + tabId);
    if (buttonToActivate) buttonToActivate.classList.add('active');
    sessionStorage.setItem('activeTabAnki', tabId);
  }
  
  (function() {
    var savedTab = sessionStorage.getItem('activeTabAnki');
    if (savedTab) {
      showTab(savedTab);
    } else {
      showTab('front');
    }
  })();
</script>

Back

<div class="tabs">
  <button class="tab-button" id="btn-front" onclick="showTab('front')">Front</button>
  <button class="tab-button" id="btn-back" onclick="showTab('back')">Back</button>
</div>
<hr>
<div class="content">
  <div id="front" class="tab-content">
    {{Front}}
  </div>
  <div id="back" class="tab-content" style="display:none;">
    {{Back}}
  </div>
</div>

<script>
  function showTab(tabId) {
    document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
    document.querySelectorAll('.tab-button').forEach(el => el.classList.remove('active'));
    const contentToShow = document.getElementById(tabId);
    if (contentToShow) contentToShow.style.display = 'block';
    const buttonToActivate = document.getElementById('btn-' + tabId);
    if (buttonToActivate) buttonToActivate.classList.add('active');
    sessionStorage.setItem('activeTabAnki', tabId);
  }
  
  (function() {
    var savedTab = sessionStorage.getItem('activeTabAnki');
    if (savedTab) {
      showTab(savedTab);
    } else {
      showTab('front'); 
    }
  })();
</script>

CSS

.card {
font-family: sans-serif;
font-size: 20px;
text-align: center;
}

.tabs {
margin-bottom: 15px;
}

.tab-button {
padding: 8px 16px;
border: 1px solid #555;
border-radius: 20px;
background-color: #333;
color: #eee;
cursor: pointer;
font-size: 16px;
margin: 0 5px;
}

.tab-button.active {
background-color: #5e5e5e;
font-weight: bold;
}

.content {
padding: 10px;
}

If you are going to publish the addon, you have to compress the files, change the format from .zip to .ankiaddon
Below is an example of an addon, you have to save it in __init__.py format.

from aqt import gui_hooks
from aqt.webview import WebContent

CSS_CODE = """
<style>
    .tabs {
      margin-bottom: 15px;
    }

    .tab-button {
      padding: 8px 16px;
      border: 1px solid #555;
      border-radius: 20px;
      background-color: #333;
      color: #eee;
      cursor: pointer;
      font-size: 16px;
      margin: 0 5px;
    }

    .tab-button.active {
      background-color: #5e5e5e;
      font-weight: bold;
    }

    .content {
      padding: 10px;
    }
</style>
"""




JS_CODE = """
<script>
  function showTab(tabId) {
    document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
    document.querySelectorAll('.tab-button').forEach(el => el.classList.remove('active'));

    const contentToShow = document.getElementById(tabId);
    if (contentToShow) contentToShow.style.display = 'block';
    
    const buttonToActivate = document.getElementById('btn-' + tabId);
    if (buttonToActivate) buttonToActivate.classList.add('active');

    sessionStorage.setItem('activeTabAnki', tabId);
  }
  

  function restoreTabState() {
    var savedTab = sessionStorage.getItem('activeTabAnki');
    if (savedTab && document.querySelector('.tabs')) {
      showTab(savedTab);
    } else if (document.querySelector('.tabs')) {

      const firstButton = document.querySelector('.tab-button');
      if (firstButton && firstButton.id) {
        const defaultTabId = firstButton.id.replace('btn-', '');
        showTab(defaultTabId);
      }
    }
  }

  if (document.readyState === "loading") {
    document.addEventListener('DOMContentLoaded', restoreTabState);
  } else {
    restoreTabState();
  }


  const observer = new MutationObserver((mutations_list, observer) => {
      for(const mutation of mutations_list) {
          if(mutation.type === 'childList' && mutation.addedNodes.length > 0) {
              restoreTabState();
              return;
          }
      }
  });

  observer.observe(document.body, { subtree: true, childList: true });

</script>
"""

def inject_persistent_tabs_logic(web_content: WebContent, context):
    web_content.head += CSS_CODE
    web_content.body += JS_CODE

gui_hooks.webview_will_set_content.append(inject_persistent_tabs_logic)

Example of note type with the addon

<!-- Front -->
<div class="tabs">
  <button class="tab-button" id="btn-front" onclick="showTab('front')">Front</button>
  <button class="tab-button" id="btn-back" onclick="showTab('back')">Back</button>
</div>


<div class="content">
  <div id="front" class="tab-content">
    {{Front}}
  </div>
  <div id="back" class="tab-content" style="display:none;">
    {{Back}}
  </div>
</div>


####################
<!-- Back -->
<div class="tabs">
  <button class="tab-button" id="btn-front" onclick="showTab('front')">Front</button>
  <button class="tab-button" id="btn-back" onclick="showTab('back')">Back</button>
</div>

<hr>

<div class="content">
  <div id="front" class="tab-content">
    {{Front}}
  </div>
  <div id="back" class="tab-content" style="display:none;">
    {{Back}}
  </div>
</div>
1 Like

manifest.json is required when importing .ankiaddon locally, without it users cannot import add-ons into Anki. (name and package name required.)

But manifest.json isn’t needed when uploading add-ons to AnkiWeb, the name of the AnkiWeb add-on page is auto used (and package name become the number of addon code), so the name in manifest.json isn’t used. (if you want to disable incompatible add-ons, you can set that in manifest.json and it’ll be used.)

meta.json is auto generated, so there is no need to include it in the add-on, the default values are taken from config.json. (it is probably possible to include meta.json directly instead, but in this case the default values will be overwritten by the user, so config.json is needed.)

like this:

  • Default values set by developers → config.json
  • Data saved by users (auto generated) → meta.json

If there are folders named __pycache__ you must delete them all before uploading to AnkiWeb. If they remain you will probably not be able to upload to AnkiWeb. (these folders are generated when the add-on is executed.)

if there are no options config.json is not necessary.
So I think these are the minimum files required for the add-on:

when uploading directly to AnkiWeb (.zip → .ankiaddon)

__init__.py

when distribute .ankiaddon files locally

__init__.py
manifest.json