Structuring plugin metadata.txt for Distribution
Every field of the QGIS plugin metadata.txt explained — name, version, qgisMinimumVersion, author, repository, experimental, deprecated, tags, and changelog…
TL;DR: metadata.txt is an INI file with a single [general] section. The required keys are name, qgisMinimumVersion, description, version, author, and email; the rest — tags, homepage, repository, tracker, experimental, deprecated, changelog, and about — control how your plugin is listed and updated on the QGIS Plugin Repository.
This page is part of the Packaging and Distributing QGIS Plugins guide, which sits inside the broader Headless Automation, CI/CD & Testing for PyQGIS reference. The metadata.txt file is the single source of truth the QGIS Plugin Manager reads to decide whether your plugin loads, whether it appears in search, and whether an existing installation should be offered an update. A malformed or incomplete file is the most common reason a technically correct plugin fails to install, so it pays to get every field exactly right before you publish to the plugin repository.
Complete Working metadata.txt
Drop this into the root of your plugin directory (the same folder as __init__.py). Every commonly used field is present with an inline comment explaining its contract. Remove the optional fields you do not need, but keep all required ones.
[general]
# --- Required fields -------------------------------------------------
name=Parcel Splitter
qgisMinimumVersion=3.28
description=Split cadastral parcels along a drawn line and recompute areas.
version=1.4.2
author=Nunacode GIS
email=info@nunacode.com
# --- Recommended / optional listing fields ---------------------------
# 'about' is a longer prose description shown on the plugin's web page.
about=Parcel Splitter adds an interactive tool for dividing polygon
parcels along a user-drawn cut line. It recomputes geometry areas,
preserves attributes, and writes results back to the source layer.
Continuation lines must be indented so the INI parser folds them
into a single value.
# Space-separated version support. Optional; see notes below.
qgisMaximumVersion=3.99
# Comma-separated discovery tags. Keep them specific and lowercase.
tags=cadastre,parcels,editing,geometry,vector
# URLs surfaced in the Plugin Manager and on the repository page.
homepage=https://example.org/parcel-splitter
repository=https://github.com/nunacode/parcel-splitter
tracker=https://github.com/nunacode/parcel-splitter/issues
# Optional icon path, relative to the plugin root.
icon=icon.png
# Release-state flags. Both default to False when omitted.
experimental=False
deprecated=False
# 'server' marks a plugin as a QGIS Server plugin; 'hasProcessingProvider'
# tells QGIS to look for a Processing provider on load.
server=False
hasProcessingProvider=True
# Changelog: a plain-text, indented block. Newest version first.
changelog=
1.4.2 Fix crash when the cut line has fewer than two vertices.
1.4.1 Honour the layer's CRS when recomputing areas.
1.4.0 Add undo support for split operations.
1.3.0 Initial public release.
Validating Required Keys in CI
Because the Plugin Manager rejects an incomplete file only after upload, you want to catch a missing key locally or in a pipeline first. Python’s standard-library configparser reads metadata.txt directly — no QGIS runtime required — which makes this check cheap to run on every commit as part of your continuous integration for QGIS projects.
"""
validate_metadata.py
Validate a QGIS plugin metadata.txt for required fields and basic
value sanity. Exits non-zero on failure so it can gate a CI pipeline.
Usage:
python validate_metadata.py path/to/metadata.txt
"""
from __future__ import annotations
import configparser
import sys
from pathlib import Path
REQUIRED_KEYS: tuple[str, ...] = (
"name",
"qgisMinimumVersion",
"description",
"version",
"author",
"email",
)
def validate_metadata(path: Path) -> list[str]:
"""
Return a list of human-readable problems found in metadata.txt.
An empty list means the file passed every check. The parser is
case-sensitive on keys, so 'optionxform' is overridden to preserve
the camelCase QGIS uses (qgisMinimumVersion, not qgisminimumversion).
"""
parser = configparser.ConfigParser()
parser.optionxform = str # preserve key case exactly
parser.read(path, encoding="utf-8")
problems: list[str] = []
if not parser.has_section("general"):
return ["Missing required [general] section."]
general = parser["general"]
for key in REQUIRED_KEYS:
if key not in general or not general[key].strip():
problems.append(f"Missing or empty required key: {key}")
# Booleans must parse as true/false, not arbitrary strings.
for flag in ("experimental", "deprecated", "server"):
if flag in general:
raw = general[flag].strip().lower()
if raw not in ("true", "false", "0", "1", "yes", "no"):
problems.append(f"Key '{flag}' must be a boolean, got: {raw!r}")
return problems
def main(argv: list[str]) -> int:
"""Run validation and print a report."""
if len(argv) != 2:
print("Usage: python validate_metadata.py path/to/metadata.txt")
return 2
path = Path(argv[1])
if not path.is_file():
print(f"File not found: {path}")
return 2
problems = validate_metadata(path)
if problems:
print(f"metadata.txt FAILED validation ({len(problems)} issue(s)):")
for problem in problems:
print(f" - {problem}")
return 1
print("metadata.txt passed validation.")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
The following annotated map groups the fields by whether they are contractually required or optional listing metadata — a useful mental model when you strip the template down for a real plugin.
Architecture Breakdown
Required fields and their contract
Six keys must be present and non-empty or the QGIS Plugin Manager refuses to load the plugin. name is the human-readable title shown in the manager list — it is not the folder name or the Python package name, so it may contain spaces and mixed case. description is a single short sentence (keep it under roughly 200 characters); the longer about field exists precisely so you do not overload description.
author and email identify the maintainer. The email is not displayed publicly on the repository page but is used for administrative contact and must be a real, monitored address. version and qgisMinimumVersion carry the most semantic weight and are covered below.
One subtlety: configparser folds indented continuation lines into the preceding value. That is what lets about and changelog span multiple lines. If you accidentally leave a continuation line un-indented, the parser treats it as a new (invalid) key and your file breaks in confusing ways — always indent wrapped text.
version, qgisMinimumVersion and qgisMaximumVersion semantics
version is your plugin’s own release number. The repository uses it to decide whether an installed copy is out of date, so it must increase monotonically with each upload. Dotted numeric versions (1.4.2) sort naturally; avoid prefixes like v1.4.2 or suffixes like 1.4.2-beta, which sort unpredictably. If you want a pre-release channel, use the experimental flag rather than a version suffix.
qgisMinimumVersion is the oldest QGIS release your plugin supports, written as major.minor (3.28). QGIS refuses to install the plugin on any older host. qgisMaximumVersion is optional: when you omit it, QGIS derives an implicit maximum by taking the major version of your minimum and appending .99 — so qgisMinimumVersion=3.28 yields an effective ceiling of 3.99. That default is almost always what you want, because it keeps the plugin available across the entire 3.x line. Only set qgisMaximumVersion explicitly if you have a concrete reason to exclude newer releases, and revisit it whenever a new major version like 4.x approaches, since the implicit ceiling will not cover it.
Repository-listing fields
These fields never affect whether the plugin loads; they shape how it is presented and discovered on the QGIS Plugin Repository. tags is a comma-separated list that feeds search and category filtering — keep them lowercase, specific, and drawn from vocabulary users actually type. homepage, repository, and tracker become the Homepage, Code Repository, and Bug Tracker links on the plugin’s page; supplying all three markedly increases user trust and is effectively expected of any serious plugin.
experimental=True flags a release as unstable. The Plugin Manager hides experimental plugins unless the user opts in via “Show also experimental plugins”, making it the correct way to distribute a beta without disrupting existing users on a stable channel. deprecated=True marks a plugin as no longer maintained: it stays installable for current users but disappears from search and displays a deprecation notice, which is how you retire a plugin gracefully.
changelog is a free-text, indented block conventionally written newest-version-first. It is shown verbatim in the Plugin Manager’s details pane, so structure it as short per-version bullet lines rather than prose. Because it lives in metadata.txt, it is versioned alongside your code and is a natural thing to generate from your git history in CI.
Validating metadata.txt in CI
Wire the validator above into your pipeline so a missing or malformed field fails the build before you ever attempt an upload. A minimal GitHub Actions step needs nothing but a Python interpreter, because the check deliberately avoids importing anything from qgis:
name: validate-plugin-metadata
on: [push, pull_request]
jobs:
metadata:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Validate metadata.txt
run: python validate_metadata.py parcel_splitter/metadata.txt
For the fuller picture — running the plugin’s actual test suite against a real QGIS runtime — see running QGIS plugin tests in GitHub Actions. Pairing a fast, dependency-free metadata check with a slower integration job gives you quick feedback on the most common packaging mistake while still exercising the code end to end.
Production Best Practices
- Bump
versionon every upload. The repository keys update detection to this value; a repeated version silently blocks users from receiving your fix. - Keep
descriptionto one sentence and put detail inabout. Overlong descriptions are truncated in the manager list. - Supply
homepage,repository, andtracker. All three links are expected of a credible plugin and cost nothing to add. - Prefer the implicit
qgisMaximumVersion. Omit it unless you must, and audit it before every new QGIS major release. - Use
experimental=Truefor betas, never a version suffix. Version strings must stay cleanly numeric so they sort correctly. - Deprecate rather than delete. Setting
deprecated=Trueand uploading a final build retires a plugin without breaking existing installs. - Indent all continuation lines in
aboutandchangelog, or the INI parser will misread them as new keys. - Generate the
changelogfrom git tags in CI so the file and your release history never drift apart.
Related
- Packaging and Distributing QGIS Plugins — parent guide covering the full path from source tree to a published, installable plugin
- Publishing a Plugin to the QGIS Plugin Repository — the upload, approval, and update workflow that consumes this metadata.txt
- Continuous Integration for QGIS Projects — automate metadata validation and test runs on every commit
- Plugin Lifecycle and Resource Management in QGIS Python Plugins — how the values declared here map onto load, unload, and cleanup at runtime