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.

ini
[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.

python
"""
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.

metadata.txt fields grouped by requirement A diagram with two columns. The left column lists the six required keys: name, qgisMinimumVersion, description, version, author, and email. The right column lists optional listing keys: tags, homepage, repository, tracker, experimental, deprecated, and changelog. [general] section keys Required name qgisMinimumVersion description version author email plugin fails to load if any missing Optional (listing) tags, about, icon homepage, repository tracker qgisMaximumVersion experimental, deprecated changelog controls repository presentation

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:

yaml
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 version on every upload. The repository keys update detection to this value; a repeated version silently blocks users from receiving your fix.
  • Keep description to one sentence and put detail in about. Overlong descriptions are truncated in the manager list.
  • Supply homepage, repository, and tracker. 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=True for betas, never a version suffix. Version strings must stay cleanly numeric so they sort correctly.
  • Deprecate rather than delete. Setting deprecated=True and uploading a final build retires a plugin without breaking existing installs.
  • Indent all continuation lines in about and changelog, or the INI parser will misread them as new keys.
  • Generate the changelog from git tags in CI so the file and your release history never drift apart.