Write metadata.txt for a QGIS Plugin
metadata.txt is the single file QGIS and the official repository read to identify, version, and display your plugin. Get one key wrong and the plugin manager silently hides your extension, or plugins.qgis.org refuses the upload with a terse error. This task page, part of Publishing to the QGIS Plugin Repository, gives you a complete, field-by-field annotated metadata.txt and the exact rules behind every key so your first upload passes validation.
The file is a plain INI document with a single [general] section. There is no schema enforcement inside QGIS beyond the required keys, but the repository's parser is strict — it is the parser, not QGIS Desktop, that decides whether your archive is accepted.
Prerequisites
- A plugin folder created from a generator or by hand — see Create a QGIS Plugin with Plugin Builder.
- QGIS 3.34 LTR for validation (bundles Python 3.12).
- A text editor that preserves UTF-8 and does not convert tabs/spaces unexpectedly.
The Complete Annotated metadata.txt
Below is a full, valid file. Every key the repository understands appears, with required keys listed first. The inline # comments are for this guide only — keep real comments to a minimum, as the repository parser tolerates them but they add noise.
[general]
# ---- REQUIRED KEYS ----
name=Parcel Validator
# Human-readable display name shown in the manager. Free text, can contain spaces.
qgisMinimumVersion=3.34
# Oldest QGIS that will offer this plugin. MAJOR.MINOR (or MAJOR.MINOR.PATCH).
description=Validates parcel polygons for gaps, overlaps, and invalid geometries.
# One short line. Shown in the manager list. No line breaks.
version=1.0.0
# Plugin version. Use semantic versioning MAJOR.MINOR.PATCH.
author=Jane Cartographer
# Author or organisation name. Plain text.
email=jane@example.com
# Contact address. Hidden from the public listing but required.
about=Parcel Validator scans a cadastral layer and reports topology
problems: sliver gaps between parcels, overlapping boundaries, and
self-intersecting rings. Results are written to a styled error layer
you can review and fix before submission.
# Longer paragraph shown on the plugin's page. Continuation lines are indented.
# ---- OPTIONAL BUT STRONGLY RECOMMENDED ----
repository=https://github.com/jane/parcel-validator
# Public source URL. Required for trust; reviewers check it.
tracker=https://github.com/jane/parcel-validator/issues
# Bug tracker URL.
homepage=https://jane.example.com/parcel-validator
# Documentation or landing page.
category=Vector
# One of: Raster, Vector, Database, Web. Controls the menu placement hint.
tags=topology,cadastre,validation,parcels,quality
# Comma-separated, no spaces preferred. Improves search on the repository.
icon=icons/icon.png
# Path relative to the plugin folder, or a Qt resource path (:/...).
experimental=False
# True hides the version unless the user opts into experimental plugins.
deprecated=False
# True marks the whole plugin as unmaintained.
qgisMaximumVersion=3.99
# Newest QGIS that will offer the plugin. Set high to stay visible.
changelog=1.0.0 - Initial release
# Multi-line, newest first. Rendered on the plugin page.
plugin_dependencies=Processing,QuickMapServices
# Other plugins this one needs, by display name, comma-separated.
The experimental flag here is False; flip it to True when you ship a risky pre-release. The publishing workflow that consumes this file is covered in Publishing to the QGIS Plugin Repository.
Required Keys in Detail
These seven keys must be present and non-empty or the upload fails outright.
name— The display name. It does not have to match the folder/package name (the folder name must be a valid Python identifier;namecan be any text).qgisMinimumVersion— A version string like3.34. The manager hides the plugin from any older QGIS. Do not pad it to a patch you have not tested.description— A single line, no newlines. This is the teaser shown in the plugin list, so make it specific.version—MAJOR.MINOR.PATCH. The repository compares this against the live release and rejects anything not strictly greater.author— Plain text name or organisation.email— A valid address. It is hidden publicly but must parse as an email.about— A fuller description. Use indented continuation lines for multiple paragraphs.
Optional Keys in Detail
Optional keys are not enforced by the repository's required-field check, but several materially affect discoverability and trust.
repository— Public VCS URL. Reviewers expect it; omitting it slows first approval.tracker— Where users file bugs.homepage— Documentation site or project page.category—Raster,Vector,Database, orWeb. A hint for where the plugin's tools belong.tags— Comma-separated keywords. The repository search indexes these, so include the terms users actually type.experimental—True/False. Per-version visibility toggle.deprecated—True/False. Marks the plugin unmaintained.changelog— Multi-line release notes, newest first, rendered on the plugin page and shown when an update is available.icon— Relative path to a PNG/SVG, or a Qt resource path. A missing icon shows a generic placeholder.plugin_dependencies— Comma-separated display names of other plugins. The manager prompts the user to install them.
Formatting Rules
The parser is unforgiving about a few things. Internalize these and most validation errors disappear:
- Exactly one
[general]section header, on its own line, before any keys. key=valuewith no spaces around=is the safe convention.name = Xworks in some parsers butkey=valueis what every example and the validator expect.- Continuation lines must be indented (leading spaces or a tab). An un-indented second line is read as a new, invalid key.
- UTF-8 encoding, no byte-order mark. A BOM can make the first key (
[general]) unparseable. - Booleans are
True/False(capitalized).true,yes, and1are not reliably interpreted. versionandqgisMinimumVersionare dotted numerics. Do not prefix withv(v1.0.0is invalid).
# Quick local sanity check before uploading
import configparser
cfg = configparser.ConfigParser()
cfg.read("parcel_validator/metadata.txt", encoding="utf-8")
required = ["name", "qgisMinimumVersion", "description",
"version", "author", "email", "about"]
missing = [k for k in required if not cfg["general"].get(k)]
print("Missing required keys:", missing or "none")
print("Declared version:", cfg["general"]["version"])
Breakdown: Python's configparser reads the same INI format the repository expects, so it surfaces section and continuation errors locally. This catches an unindented about paragraph or a missing required key before you waste an upload round-trip. Wire this into the test suite described in Unit Test a QGIS Plugin with pytest so every commit validates the metadata automatically.
You can take the check one step further and assert that the version is well-formed and the booleans parse, turning the loose INI into something closer to a schema:
import re
general = cfg["general"]
# version must be MAJOR.MINOR.PATCH with no leading 'v'
assert re.fullmatch(r"\d+\.\d+\.\d+", general["version"]), \
f"bad version: {general['version']!r}"
# booleans must be exactly True/False
for flag in ("experimental", "deprecated"):
if flag in general:
assert general[flag] in ("True", "False"), \
f"{flag} must be True or False, got {general[flag]!r}"
# qgisMinimumVersion must be dotted numeric
assert re.fullmatch(r"\d+\.\d+(\.\d+)?", general["qgisMinimumVersion"])
print("metadata.txt is well-formed")
Breakdown: re.fullmatch rejects the two most common formatting mistakes — a v prefix on version and lowercase true/false booleans — before they reach the repository parser. Because this is plain Python, it runs in CI on every push at no cost, so a malformed metadata.txt fails the build instead of a release upload.
How QGIS and the Repository Read Each Field
It helps to know which consumer cares about which key, because the two read metadata.txt for different reasons. QGIS Desktop reads it at startup to decide whether to offer the plugin (qgisMinimumVersion, qgisMaximumVersion), what to call it (name), what icon to show (icon), and which other plugins to require (plugin_dependencies). The repository reads it at upload time to build the public listing — rendering about, changelog, tags, homepage, tracker, and repository, and enforcing that version is strictly greater than the live release.
This split explains some otherwise-confusing behavior. A plugin can be perfectly installable from a manually downloaded ZIP yet rejected by the repository, because the repository enforces fields QGIS Desktop ignores (a duplicate version, a missing required key). Conversely a plugin can upload fine yet stay hidden in a user's manager because qgisMinimumVersion exceeds their build — a constraint the repository accepts but Desktop enforces. Knowing who reads what tells you where to look when something does not behave.
QGIS Version Compatibility
The metadata.txt format itself is stable across the 3.x line, but the values you put in qgisMinimumVersion should track the Python and API generation you actually target.
| QGIS line | Bundled Python | Suggested keys |
|---|---|---|
| 3.28 LTR | 3.9 | qgisMinimumVersion=3.28 |
| 3.34 LTR | 3.12 | qgisMinimumVersion=3.34 (baseline for this guide) |
| 3.40 / 3.44 | 3.12 | qgisMinimumVersion=3.34, qgisMaximumVersion=3.99 |
Setting qgisMaximumVersion=3.99 is a deliberate trick: it keeps your plugin visible to users on QGIS releases that did not exist when you published, instead of vanishing the moment a new version ships.
Troubleshooting
metadata.txt not found on upload. The file is not directly inside a single top-level folder in the ZIP, or that folder name contains a dash. Re-zip so the structure is plugin_folder/metadata.txt.
Version X already exists. Your version is equal to or lower than the live release. Bump the PATCH (or higher) component so it is strictly greater.
Plugin loads in QGIS but the repository shows no icon. The icon= path is wrong or points at a resource you did not compile. Use a path relative to the plugin folder (icons/icon.png) and confirm the file is actually inside the ZIP.
Manager hides the plugin after install. qgisMinimumVersion is higher than the running QGIS, or experimental=True while the user has not enabled experimental plugins. Check both against your test build.
Continuation line parsed as an unknown key. An about or changelog paragraph's second line is not indented. Add leading whitespace to every continuation line.
Upload rejected for invalid version string. You used v1.0.0, 1.0, or a date. Use bare MAJOR.MINOR.PATCH.
Conclusion
metadata.txt is small but load-bearing: it is the contract between your plugin, QGIS Desktop, and the repository. Start from the annotated template above, fill in the seven required keys correctly, set a generous qgisMaximumVersion, and validate locally with configparser before every upload. With those rules internalized, the metadata step of publishing becomes a non-event rather than a source of rejected uploads.
Frequently Asked Questions
Does name have to match my plugin's folder name?
No. The folder (package) name must be a valid Python identifier — lowercase, underscores, no spaces. name is a free-text display label and can differ entirely, for example folder parcel_validator with name=Parcel Validator.
Can I put comments in metadata.txt?
Lines beginning with # are tolerated, but keep them minimal in the shipped file. The annotated comments in this guide are illustrative; a production metadata.txt is cleaner without them.
Is changelog required?
No, but it is strongly recommended. It is rendered on your plugin page and shown to users when an update is available, so omitting it leaves them guessing what changed.
What happens if I omit qgisMaximumVersion?
QGIS assumes a default upper bound tied to your minimum, which can cause the manager to hide your plugin on much newer releases. Set it explicitly to 3.99 to avoid premature invisibility.