Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

InfluxDB 3 Plugin SDK

The InfluxDB 3 Plugin SDK is a CLI tool and set of libraries to help author and manage plugins. Plugin repository maintainers use the SDK to publish versioned plugin registries from CI, and plugin authors use the SDK to create versioned plugins.

Why Use The Plugin SDK?

A Registry Solves Versioning

The most common way to install plugins is to fetch them directly from GitHub using the gh: prefix with the influxdb3 CLI. E.g. influxdb3 create trigger --path gh:influxdata/downsampler/downsampler.py. This install path has several problems:

  • No plugin versioning
    • Changes to plugin source are automatically forced onto users because gh: plugins are fetched from the source’s main branch.
    • Users have no control over what code is running; they cannot specify a version, cannot pin, or roll back.
    • Plugin authors are unable to update plugins without potentially breaking users.
  • No dependency management
    • Plugin authors cannot declare which InfluxDB version their plugin supports.
    • Users cannot know whether a plugin is compatible with their InfluxDB version until after they install it and encounter runtime errors.
    • There is no standardized way to communicate plugin dependencies on third-party libraries.
  • Multifile plugins are not supported
    • Plugin authors cannot create plugins that span multiple files

Using the plugin SDK, plugin repository maintainers can solve these problems by publishing a plugin registry. This will result in the following benefits for plugin authors and users:

  • Plugins are versioned
    • Each published plugin version is an immutable artifact with a stable (registry, name, version) identity.
    • Plugin authors can publish updates without breaking or forcing changes on existing users.
    • Users can install, pin, compare, and report the exact plugin version they are running.
  • Plugins declare dependencies and compatibility
    • All dependencies declared for each plugin version.
    • Consumers can reject incompatible plugin versions before they fail at runtime.
  • Multifile plugins are supported
    • Plugin authors can split plugin code across multiple files.
  • No breaking changes
    • Both gh: and registry consumers can coexist in the same repository.
  • Minimal effort to set up and maintain
    • Use a CI workflow to maintain the registry.
    • No change to plugin author workflow.

Next: Getting Started

Getting Started


Back: Introduction | Next: Install the CLI

Installation

The easiest way to install the CLI is to first install Cargo, and then you can install influxdb3-plugin from crates.io:

cargo install influxdb3-plugin-cli --locked

Alternatively, you can install from the repo’s GitHub Releases:

cargo install --git https://github.com/influxdata/influxdb3-plugin-sdk --tag latest influxdb3-plugin-cli

After installing, validate by running:

influxdb3-plugin --version

Back: Getting Started | Next: First Steps with the InfluxDB 3 Plugin SDK

First Steps with the InfluxDB 3 Plugin SDK

This section provides a quick sense for the influxdb3-plugin command line tool. We demonstrate its ability to create a local registry, create a plugin from a template, package the plugin, and view the plugin in the registry’s index.

Start by listing available templates with influxdb3-plugin new list:

$ influxdb3-plugin new list

Template Name           Short Name
----------------------  ----------------------
Process Writes Plugin   process_writes
Scheduled Call Plugin   process_scheduled_call
Process Request Plugin  process_request
Index                   index

A registry is a collection of plugins and an index.json file. Let’s create an index using influxdb3-plugin new index:

$ influxdb3-plugin new index registry

Scaffolded index (index template) at registry
  files written:
    index.json

This creates an index file at registry/index.json with these contents:

{
  "index_schema_version": "2.0",
  "artifacts_url": "file:///path/to/registry",
  "plugins": []
}

As we can see, the index has an artifacts_url and an empty collection of plugins.

By default the artifacts_url points to the absolute path of the local fileystem, but --artifacts-url can be used to specify a remote https:// url.

Next, let’s create a Scheduled Call Plugin for our registry using influxdb3-plugin new process_scheduled_call:

$ influxdb3-plugin new process_scheduled_call src/hello-world

Scaffolded plugin (process_scheduled_call template) at src/hello-world
  name: hello-world
  files written:
    manifest.toml
    __init__.py
    README.md

This is all we need to get started. First, let’s check out manifest.toml:

manifest_schema_version = "1.2"

[plugin]
name = "hello-world"
version = "0.1.0"
description = "A new scheduled-call plugin."
triggers = ["process_scheduled_call"]

[dependencies]
database_version = ">=3.0.0"

The manifest contains all of the metadata needed to package the plugin.

Here’s what’s in __init__.py:

"""Plugin entry point for the `process_scheduled_call` trigger."""


def process_scheduled_call(influxdb3_local, schedule_time, args):
    """Called on each scheduled fire. `schedule_time` is a naive UTC datetime."""
    influxdb3_local.info(f"scheduled call at {schedule_time}")

process_scheduled_call is a special function that gets called by InfluxDB 3 when the plugin is triggered.

Now let’s package the plugin with influxdb3-plugin package:

$ influxdb3-plugin package src/hello-world --index registry/index.json --out build

Packaged hello-world@0.1.0
  artifact: build/hello-world-0.1.0.tar.gz
  index:    build/index.json
  hash:     sha256:5836485b76fad264ac2a13c4d0dc4ba1b067ac800b1c9914b3b2e4644c74c9a5

Now if we inspect the newly generated build/index.json, we can see that it contains our plugin version’s metadata:

{
  "index_schema_version": "2.0",
  "artifacts_url": "file:///Users/rcater/.config/superpowers/worktrees/influxdb3-plugin-sdk/docs/design-spec/docs/superpowers/tmp/registry",
  "plugins": [
    {
      "name": "hello-world",
      "version": "0.1.0",
      "published_at": "2026-05-26T20:03:54Z",
      "description": "A new scheduled-call plugin.",
      "triggers": [
        "process_scheduled_call"
      ],
      "dependencies": {
        "database_version": ">=3.0.0",
        "python": []
      },
      "hash": "sha256:5836485b76fad264ac2a13c4d0dc4ba1b067ac800b1c9914b3b2e4644c74c9a5"
    }
  ]
}

Every published plugin version gets it’s own entry in the index.

The package and publish steps are separate, so we can publish the plugin by moving the artifact and the index from the build directory into the registry directory, overwriting the existing index:

$ mv build/index.json registry 
$ mv build/hello-world-0.1.0.tar.gz registry

The CLI’s search command can be used to query the registry index:

$ influxdb3-plugin search --index registry/index.json

hello-world  0.1.0  process_scheduled_call  A new scheduled-call plugin.

And info can be used to inspect the plugin version’s metadata:

$ influxdb3-plugin info --index registry/index.json hello-world

hello-world
A new scheduled-call plugin.
version: 0.1.0
published_at: 2026-05-26T20:03:54Z
triggers: process_scheduled_call
database: >=3.0.0
python: <none>
artifact_url: file:///path/to/registry/hello-world-0.1.0.tar.gz
hash: sha256:5836485b76fad264ac2a13c4d0dc4ba1b067ac800b1c9914b3b2e4644c74c9a5
visibility: visible

The displayed artifact_url can be used to fetch the plugin artifact for installation in InfluxDB 3.

Finally, let’s update our plugin and publish a new version. First, change the plugin’s source:

"""Plugin entry point for the `process_scheduled_call` trigger."""


def process_scheduled_call(influxdb3_local, schedule_time, args):
    """Called on each scheduled fire. `schedule_time` is a naive UTC datetime."""
    influxdb3_local.info(f"scheduled call at {schedule_time}")
    influxdb3_local.info("hello world!") # <- updated source

Then bump the version in manifest.toml:

manifest_schema_version = "1.2"

[plugin]
name = "hello-world"
version = "1.0.0"
description = "A new scheduled-call plugin."
triggers = ["process_scheduled_call"]

[dependencies]
database_version = ">=3.0.0"

Now we package and publish the new version:

influxdb3-plugin package src/hello-world --index registry/index.json --out build

Packaged hello-world@1.0.0
  artifact: build/hello-world-1.0.0.tar.gz
  index:    build/index.json
  hash:     sha256:21979050833599eb97b78ecccd13ff9385590a868528ebccd89ed0382ae47383
$ mv build/index.json registry 
$ mv build/hello-world-1.0.0.tar.gz registry

Our newly created index now has both plugin versions:

{
  "index_schema_version": "2.0",
  "artifacts_url": "file:///Users/rcater/.config/superpowers/worktrees/influxdb3-plugin-sdk/docs/design-spec/docs/superpowers/tmp/registry",
  "plugins": [
    {
      "name": "hello-world",
      "version": "0.1.0",
      "published_at": "2026-05-26T20:03:54Z",
      "description": "A new scheduled-call plugin.",
      "triggers": [
        "process_scheduled_call"
      ],
      "dependencies": {
        "database_version": ">=3.0.0",
        "python": []
      },
      "hash": "sha256:5836485b76fad264ac2a13c4d0dc4ba1b067ac800b1c9914b3b2e4644c74c9a5"
    },
    {
      "name": "hello-world",
      "version": "1.0.0",
      "published_at": "2026-05-27T17:52:05Z",
      "description": "A new scheduled-call plugin.",
      "triggers": [
        "process_scheduled_call"
      ],
      "dependencies": {
        "database_version": ">=3.0.0",
        "python": []
      },
      "hash": "sha256:21979050833599eb97b78ecccd13ff9385590a868528ebccd89ed0382ae47383"
    }
  ]
}

Inspecting the registry with search and info also shows the latest version:

$ influxdb3-plugin search --index registry/index.json

hello-world  1.0.0  process_scheduled_call  A new scheduled-call plugin.
$ influxdb3-plugin info --index registry/index.json hello-world

hello-world
A new scheduled-call plugin.
version: 1.0.0
published_at: 2026-05-27T17:52:05Z
triggers: process_scheduled_call
database: >=3.0.0
python: <none>
artifact_url: file:///Users/rcater/.config/superpowers/worktrees/influxdb3-plugin-sdk/docs/design-spec/docs/superpowers/tmp/registry/hello-world-1.0.0.tar.gz
hash: sha256:21979050833599eb97b78ecccd13ff9385590a868528ebccd89ed0382ae47383
visibility: visible

To install and run a plugin in InfluxDB 3, see the official InfluxDB 3 plugin documentation.

Normally, a registry is hosted on a remote server so that plugins can be shared with other users. Additionally, the packaging and publishing steps are typically automated in a CI/CD pipeline.


Back: Install the CLI | Next: Guides

Guides


Back: First Steps with the InfluxDB 3 Plugin SDK | Next: Plugin Repository Maintainer

Guide - Plugin Repository Maintainer

As a maintainer, you must make the following decisions:

  • where to host the plugin repository
  • where to host the plugin registry
  • which CI/CD tools to use for packaging and publishing plugins
  • should the repository and registry be public or private

The SDK and registry are flexible and agnostic to hosting solutions, so you could use any combination of the following:

  • Repo Hosts: GitHub, GitLab, Azure DevOps, or any code hosting platform or VCS.
  • CI Runners: GitHub Actions, GitLab CI, Azure Pipelines, CircleCI, Jenkins, Buildkite, or any CI that can run CLI commands.
  • Registry Hosts: S3, GitHub Releases, GitLab Releases, Azure DevOps Artifacts, or any HTTP server (supported URL schemes documented here).
  • Both the repo and registry can be private or public.

If you already have a repo, that’s ok, you can use it as-is without breaking existing plugin consumers.

This guide assumes that you already have a GitHub plugin repo and want to publish a registry to GitHub Releases using GitHub Actions.

initialize

  • access tokens
  • create registry release plugin repo shape
  • directory per plugin (single file vs multi-file?)
  • manifest

Common steps

Step 1: Author the manifest

Author plugins/downsampler/manifest.toml. If you followed the New path, the scaffold wrote a stub for you to fill in; if you followed the Migrate path, create the file now.

Minimal shape:

manifest_schema_version = "1.2"

[plugin]
name = "downsampler"
version = "0.1.0"
description = "Downsample incoming writes."
triggers = ["process_writes"]

[dependencies]
database_version = ">=3.2.0,<4.0.0"

The triggers array must match the functions implemented by the Python file. See The Manifest Format for all fields and validation rules.

Validate locally before wiring CI:

influxdb3-plugin validate plugins/downsampler

Step 2: Create the registry release

Use one GitHub Release as the registry. The release stores index.json and all {name}-{version}.tar.gz artifacts.

REGISTRY_REPO="${PLUGIN_REPO}"
REGISTRY_TAG="plugin-registry"
ARTIFACTS_URL="https://github.com/${REGISTRY_REPO}/releases/download/${REGISTRY_TAG}"

gh release create "${REGISTRY_TAG}" \
  --repo "${REGISTRY_REPO}" \
  --title "Plugin Registry" \
  --notes "Plugin registry index and artifacts"

Step 3: Seed the index

Generate and upload the initial empty registry index:

SEED_DIR="$(mktemp -d)"
influxdb3-plugin new index "${SEED_DIR}" --artifacts-url "${ARTIFACTS_URL}"
gh release upload "${REGISTRY_TAG}" "${SEED_DIR}/index.json" --repo "${REGISTRY_REPO}"

See The Registry Index Format for the index schema.

Step 4: Add the GitHub Actions workflow

Create the workflow directory, then copy the Publish workflow shown below into .github/workflows/publish.yml:

mkdir -p .github/workflows

Edit .github/workflows/publish.yml and set the env values for your repository:

env:
  SDK_VERSION: "X.Y.Z"
  PLUGIN_ROOT: "plugins"
  REGISTRY_REPO: "YOUR_ORG/my-private-plugins"
  REGISTRY_TAG: "plugin-registry"

Step 5: Configure authentication

Create a fine-grained GitHub personal access token:

  • Resource owner: YOUR_ORG.
  • Repository access: ${REGISTRY_REPO}.
  • Repository permissions: Contents read and write.

Save it as a repository secret:

gh secret set GH_RELEASE_TOKEN --repo "${PLUGIN_REPO}"

Step 6: Trigger the first publish

Commit and push:

git add .
git commit -m "Add initial plugin registry"
git push origin main

Watch the workflow:

gh run list --repo "${PLUGIN_REPO}" --workflow publish.yml

After it succeeds, the registry release contains:

  • index.json
  • one {name}-{version}.tar.gz artifact for each newly published plugin version

Verify the registry locally:

gh release download "${REGISTRY_TAG}" \
  --repo "${REGISTRY_REPO}" \
  --pattern index.json \
  --dir /tmp/plugin-registry \
  --clobber

influxdb3-plugin search --index /tmp/plugin-registry/index.json downsampler

Step 7: Verify installation

Download and extract the published artifact:

gh release download "${REGISTRY_TAG}" \
  --repo "${REGISTRY_REPO}" \
  --pattern "downsampler-0.1.0.tar.gz" \
  --dir /tmp/downsampler-artifact \
  --clobber

mkdir -p /tmp/downsampler-extract
tar -xzf /tmp/downsampler-artifact/downsampler-0.1.0.tar.gz -C /tmp/downsampler-extract
find /tmp/downsampler-extract -maxdepth 2 -type f | sort

The archive extracts to a top-level downsampler-0.1.0/ directory. Copy that directory, or its contents, into the plugin directory configured for your InfluxDB 3 host.

If you use the HTTP API path instead of a manual file move, extract the archive first and send the extracted file entries to PUT /api/v3/plugins/directory. Do not send the tarball bytes to /api/v3/plugins/files; that endpoint accepts single-file content, not plugin archives.

How Publish Pipelines Vary

Every plugin publish pipeline does the same four things:

  1. Validate the plugin directory.
  2. Package the plugin into a <name>-<version>.tar.gz artifact.
  3. Upload the artifact to the registry backend.
  4. Upload the updated index.json to the registry backend.

The differences between pipelines live in dimensions the recipes encode in their filenames or describe inline. This page names those dimensions so a reader can choose a recipe with the right mental model.

Dimensions

Registry backend (primary)

The registry backend determines authentication, upload primitive, URL shape, and rollback story. This is the dimension that drives recipe choice.

BackendUpload primitiveURL shapeRollback
GitHub Releasesgh release upload --clobberhttps://github.com/{org}/{repo}/releases/download/{tag}/...Re-upload a previous index.json asset
S3aws s3api put-object with --if-none-match '*'https://{bucket}.s3.{region}.amazonaws.com/...Object versioning + copy-object --version-id
GCSgsutil cp with generation matchhttps://storage.googleapis.com/{bucket}/...Object versioning
Generic HTTPSOut-of-band (rsync, scp)Whatever the operator choosesBackend-specific

CI runner (secondary)

The CI runner determines YAML syntax, secret plumbing, and the concurrency primitive that prevents two publish runs from racing on the same registry.

RunnerSecret plumbingConcurrency primitive
GitHub Actionssecrets.X or OIDC id-token: writeconcurrency: { group: ..., cancel-in-progress: false }
GitLab CICI/CD variablesresource_group:
CircleCIProject env vars or contextsWorkflow-level serial
JenkinsCredentials binding pluginlock step from Lockable Resources

Repo host (inline variation)

The repo host changes the git clone URL and the shape of any personal access token used for index push. Recipes call out the differences inline rather than fragmenting along this dimension.

Visibility (inline variation)

Public registries do not require authentication for download. Private registries require a token at fetch time. Recipes call out the token shape inline.

Starting state (recipe section)

A repository either has no existing plugin distribution (new) or already distributes via the legacy gh: prefix mechanism (migrate). The recipe steps for these two states share the manifest authoring, registry setup, workflow installation, authentication, and verification sections. They differ only in repository preparation. Each recipe carries both states as ## New and ## Migrate sections so a reader picks the entry point that matches their state and follows shared steps from there.

What stays the same across every pipeline

  • The registry concept itself — see The Registry.
  • Manifest format (manifest.toml) — see The Manifest Format.
  • Index format (index.json) — see The Registry Index Format.
  • The four-step pipeline shape listed at the top of this page.
  • The immutability rule: once (name, version) is published, only yanked can change.

How to read a recipe

Recipe filenames use the pattern <registry>--<ci>.md. Pick a recipe whose filename matches your registry backend and CI runner. Inside, pick ## New if you are starting a repository from scratch, or ## Migrate if you are adding the SDK alongside an existing gh: distribution. Repo host and visibility differences appear inline within the steps.

Publish workflow

name: Publish InfluxDB 3 plugins

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read

env:
  # Update these values for your repository.
  SDK_VERSION: "X.Y.Z"
  PLUGIN_ROOT: "plugins"
  REGISTRY_REPO: "YOUR_ORG/YOUR_REGISTRY_REPO"
  REGISTRY_TAG: "plugin-registry"

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install Rust
        run: |
          rustup set profile minimal
          rustup toolchain install stable
          rustup default stable

      - name: Install InfluxDB 3 plugin SDK
        run: cargo install influxdb3-plugin-cli --version "${SDK_VERSION}" --locked

      - name: Fetch current registry index
        env:
          GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }}
        run: |
          set -euo pipefail
          mkdir -p .publish/current
          gh release download "${REGISTRY_TAG}" \
            --repo "${REGISTRY_REPO}" \
            --pattern index.json \
            --dir .publish/current \
            --clobber
          test -f .publish/current/index.json

      - name: Validate and package new plugin versions
        shell: bash
        run: |
          set -euo pipefail

          current_index=".publish/current/index.json"
          next_index=".publish/next-index.json"
          artifacts_dir=".publish/artifacts"
          package_out=".publish/package-out"

          cp "${current_index}" "${next_index}"
          mkdir -p "${artifacts_dir}"

          mapfile -t plugin_dirs < <(
            find "${PLUGIN_ROOT}" -mindepth 1 -maxdepth 1 -type d -print | sort
          )

          if [ "${#plugin_dirs[@]}" -eq 0 ]; then
            echo "No plugin directories found under ${PLUGIN_ROOT}" >&2
            exit 1
          fi

          packaged_count=0

          for plugin_dir in "${plugin_dirs[@]}"; do
            if [ ! -f "${plugin_dir}/manifest.toml" ]; then
              echo "Skipping ${plugin_dir}: no manifest.toml"
              continue
            fi

            manifest_id="$(python3 - "${plugin_dir}/manifest.toml" <<'PY'
          import sys
          import tomllib

          with open(sys.argv[1], "rb") as f:
              manifest = tomllib.load(f)

          plugin = manifest["plugin"]
          print(f"{plugin['name']}@{plugin['version']}")
          PY
            )"

            if python3 - "${plugin_dir}/manifest.toml" "${next_index}" <<'PY'
          import json
          import sys
          import tomllib

          with open(sys.argv[1], "rb") as f:
              manifest = tomllib.load(f)
          with open(sys.argv[2], "r", encoding="utf-8") as f:
              index = json.load(f)

          name = manifest["plugin"]["name"]
          version = manifest["plugin"]["version"]
          for entry in index.get("plugins", []):
              if entry.get("name") == name and entry.get("version") == version:
                  sys.exit(0)
          sys.exit(1)
          PY
            then
              echo "Skipping ${manifest_id}: already present in registry index"
              continue
            fi

            echo "Packaging ${manifest_id}"
            rm -rf "${package_out}"
            mkdir -p "${package_out}"

            influxdb3-plugin validate "${plugin_dir}" \
              --index "${next_index}" \
              --output json

            influxdb3-plugin package "${plugin_dir}" \
              --index "${next_index}" \
              --out "${package_out}" \
              --output json

            cp "${package_out}/index.json" "${next_index}"
            find "${package_out}" -maxdepth 1 -name "*.tar.gz" -exec cp {} "${artifacts_dir}/" \;
            packaged_count=$((packaged_count + 1))
          done

          cp "${next_index}" .publish/index.json
          echo "packaged_count=${packaged_count}" >> "${GITHUB_ENV}"

      - name: Upload new artifacts
        if: env.packaged_count != '0'
        env:
          GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }}
        run: |
          set -euo pipefail
          while IFS= read -r -d '' artifact; do
            gh release upload "${REGISTRY_TAG}" "${artifact}" \
              --repo "${REGISTRY_REPO}"
          done < <(find .publish/artifacts -maxdepth 1 -name "*.tar.gz" -print0 | sort -z)

      - name: Upload updated index
        if: env.packaged_count != '0'
        env:
          GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }}
        run: |
          set -euo pipefail
          gh release upload "${REGISTRY_TAG}" .publish/index.json \
            --repo "${REGISTRY_REPO}" \
            --clobber

      - name: Report no-op
        if: env.packaged_count == '0'
        run: echo "No unpublished plugin versions found; registry index unchanged."


Back: Guides | Next: Plugin Author

Guide - Plugin Author


Back: Plugin Repository Maintainer | Next: Plugin Consumer

Guide -Plugin Consumer


Back: Plugin Author | Next: Reference

SDK Reference

The reference covers the details of the public file formats used by the SDK.


Back: Plugin Consumer | Next: The Registry

The Registry

A registry is a single index.json file and its collection of plugin artifacts. The index lists every published plugin version and points at the location where the archives are served; the artifacts contain the executable plugins.

This page defines what a registry is and how its two halves relate. The on-disk format of index.json is specified in The Registry Index Format.

A registry has two parts:

PartFormRole
IndexOne index.json file.Catalog of published plugin versions.
ArtifactsOne {name}-{version}.tar.gz artifact per published plugin version.Plugin files required to execute on db.

Globally Unique Identity

A plugin’s identity is globally defined by the tuple (index_url, name, version):

  • index_url is the location of the registry’s index.json. It is supplied by the consumer’s registry configuration; the index does not declare its own URL.
  • name and version are defined in a plugin’s manifest.toml and recorded in the matching index entry.

Two registries with different index_url values are distinct, even when they list the same (name, version) pair. Within one registry, (name, version) is unique, and names that share a canonical form (lowercase(name).replace('-', '_')) cannot coexist regardless of version.

Index and Artifacts Can Be Hosted Separately

The index file and the artifacts do not need to live at the same location, on the same host, or even use the same URL scheme. The index’s artifacts_url field is an independent base URL that consumers combine with each entry’s name and version to compute the archive URL:

{artifacts_url}/{name}-{version}.tar.gz

Valid topologies include:

  • Index and artifacts hosted together (for example, both under one S3 bucket prefix or one GitHub Release).
  • Index hosted on a CDN or static site; artifacts hosted on a separate object store.
  • Index hosted under file:// for offline or air-gapped use; artifacts hosted under https://, or vice versa.
  • Index served from one origin and mirrored to another, with artifacts_url rewritten per mirror.

Supported URL Schemes

artifacts_url accepts the following schemes:

SchemeUse
https://Recommended default for public and private registries.
http://Local development or trusted internal networks.
file://Offline, air-gapped, or appliance-style deployments.

Unsupported schemes are rejected at registry-configuration time, including oci://, s3://, git://, git+https://, git+ssh://, ftp://, and sftp://. Use an object store’s HTTPS endpoint rather than a native storage URI such as s3://.

Schemes for index_url are governed by the consumer, not by the index format.

Publication and Immutability

A registry grows by appending entries to index.json and uploading the matching archive:

  1. The plugin author creates or updates a plugin directory that follows the plugin format.
  2. Use influxdb3-plugin package to package an archive and append a new entry to index.json.
  3. Upload the new archive to {artifacts_url}/{name}-{version}.tar.gz and replace index.json at its hosted location with the newly generated version.

Once (name, version) is published, the artifact and the index entry are immutable. To update a plugin, bump plugin.version in the manifest and publish a new entry.

Yanking

Yanking is the only permitted mutation to an existing entry. Use influxdb3-plugin yank on a plugin version to mark it unavailable. Yanking is reversible by clearing the flag.

Entries are never deleted from the registry. Consumers may continue to use a yanked version, but new installs should skip yanked versions.

Artifact Integrity

Every index entry carries a hash field of the archive bytes. Consumers should verify the hash before extracting an archive and reject mismatches.

The hash is used to verify the integrity between the index and the artifact. Do not install a plugin when an archive’s bytes disagree with the index entry’s hash.


Back: Reference | Next: The Manifest Format

The Manifest Format

A plugin manifest describes one plugin version. It lives at the root of the plugin directory as manifest.toml, travels inside the packaged artifact, and is authored by the plugin repository maintainer or plugin author.

Scaffolding a plugin with influxdb3-plugin new <template> writes an initial manifest.toml alongside the template’s source files. Packaging and validation commands read the manifest, validate it, and preserve the author-written source file.

Minimal Example

manifest_schema_version = "1.2"

[plugin]
name = "downsampler"
version = "1.2.0"
description = "Downsample data on every WAL write."
triggers = ["process_writes"]

[dependencies]
database_version = ">=3.2.0,<4.0.0"

Complete Example

manifest_schema_version = "1.2"

[plugin]
name = "downsampler"
version = "1.2.0"
description = "Downsample data on every WAL write."
triggers = ["process_writes", "process_scheduled_call"]
homepage = "https://influxdata.com"
repository = "https://github.com/influxdata/plugin-downsampler"
documentation = "https://github.com/influxdata/plugin-downsampler/readme.md"

[dependencies]
database_version = ">=3.2.0,<4.0.0"
python = ["requests>=2.31,<3", "pydantic~=2.0"]

Manifest Structure

Every manifest file consists of these fields and sections:

  • manifest_schema_version - Root-level manifest schema version.
  • [plugin] - Plugin metadata.
    • name - Plugin name.
    • version - Plugin version.
    • description - One-line description.
    • triggers - Trigger types implemented by the plugin.
    • homepage - Optional project homepage URL.
    • repository - Optional source repository URL.
    • documentation - Optional documentation URL.
    • exclude - Optional gitignore-style file exclusion patterns.
  • [dependencies] - Runtime compatibility and Python package requirements.
    • database_version - Compatible InfluxDB 3 database version range.
    • python - Optional Python package requirements.

Unknown fields are ignored within a supported schema major. Do not use unknown fields for durable custom metadata: a future schema version may define them. The key dependencies.plugins is reserved for a future inter-plugin dependency format.

Top-Level Entries

EntryTOML typeRequiredDescription
manifest_schema_versionstringYesManifest schema version in <major>.<minor> form. Parsed before field-level validation.
[plugin]tableYesPlugin metadata.
[dependencies]tableYesRuntime compatibility and Python package requirements.

manifest_schema_version

manifest_schema_version must be a root-level string before any table header:

manifest_schema_version = "1.2"

The value uses <major>.<minor> form. Consumers accept known major version 1, including newer minor versions such as 1.2, and reject unsupported majors instead of guessing.

If manifest_schema_version is malformed or uses an unsupported major, parsing stops with that schema-version error before field-level validation.

The [plugin] Section

The [plugin] section defines the plugin version.

[plugin]
name = "downsampler"
version = "1.2.0"
description = "Downsample data on every WAL write."
triggers = ["process_writes"]
FieldTypeRequiredDescription
namestringYesPlugin name. Forms the name component of plugin identity.
versionstringYesPlugin version. Must be valid SemVer 2.0.0.
descriptionstringYesOne-line human-readable description.
triggersarray of stringsYesTrigger types implemented by the plugin. Must be non-empty.
homepagestringNoHTTP or HTTPS URL for the plugin or project homepage.
repositorystringNoHTTP or HTTPS URL for the plugin source repository.
documentationstringNoHTTP or HTTPS URL for plugin documentation.
excludearray of stringsNoGitignore-style patterns, relative to the plugin root, naming files to omit from packaging and validation. Missing or [] means no exclusions.

plugin.name

The plugin name is an identifier used to refer to the plugin. It is used in registry entries, search and info output, artifact names, and as the name component of plugin identity.

Names are stored case-preserving, but registry collision checks use a canonical form: lowercase, with - replaced by _.

Validation rules:

  • 1 to 64 ASCII characters.
  • Starts with an ASCII letter.
  • Remaining characters are ASCII letters, ASCII digits, _, or -.
  • Windows reserved device names are rejected case-insensitively: con, prn, aux, nul, com0 through com9, and lpt0 through lpt9.

Examples of valid names:

  • downsampler
  • my-plugin
  • MyPlugin
  • process_writes_v2

Examples of invalid names:

  • 123plugin
  • my plugin
  • plugin.example
  • any name containing non-ASCII characters

Within one registry, two plugin names that share a canonical form are treated as the same plugin. For example, foo-bar, foo_bar, and FOO-BAR cannot be published as separate plugins in one registry.

plugin.version

The version field is formatted according to the SemVer 2.0.0 specification:

version = "1.2.0"

Versions must have three numeric parts: major, minor, and patch. A pre-release part can be added after a dash, for example 1.2.0-rc.1. Build metadata can be added after a plus, for example 1.2.0+build.7.

Invalid examples include 1, 1.2, and latest.

The SDK preserves the full version string. Registry ordering uses SemVer precedence. Plugin versions are immutable once published to a registry; to publish changed plugin contents, bump plugin.version.

plugin.description

The description field is a short, plain-text blurb about the plugin. Registries display it with the plugin in browse and discovery output. Use plain text, not Markdown.

description = "Downsample data on every WAL write."

Descriptions must be non-empty, single-line strings no longer than 200 characters. Newline (\n) and carriage return (\r) characters are rejected.

plugin.triggers

triggers lists the trigger functions the plugin implements:

triggers = ["process_writes", "process_scheduled_call"]

The array must contain at least one value. Supported trigger values are:

  • process_writes
  • process_scheduled_call
  • process_request

Each value must correspond to a supported trigger entry point in the plugin source. Unknown trigger strings are rejected. How a declared trigger binds to a Python function in the entry point is specified in The Plugin Directory Format.

plugin.homepage

The homepage field should be a URL to a site that is the home page for the plugin:

homepage = "https://influxdata.com"

Set homepage only when the plugin has a dedicated website other than the source repository or API documentation. Do not make homepage redundant with documentation or repository.

When present, the URL must parse and use the http or https scheme.

plugin.repository

The repository field should be a URL to the source repository for the plugin:

repository = "https://github.com/influxdata/plugin-downsampler"

When present, the URL must parse and use the http or https scheme.

plugin.documentation

The documentation field specifies a URL to a website hosting the plugin’s documentation:

documentation = "https://github.com/influxdata/plugin-downsampler/readme.md"

When present, the URL must parse and use the http or https scheme.

plugin.exclude

exclude is an optional array of gitignore-style patterns, evaluated relative to the plugin root:

exclude = [".git/", ".venv/", "__pycache__/", "*.pyc", "tests/**"]
  • Patterns use gitignore-style glob semantics. Directory-style patterns such as __pycache__/ exclude every file beneath that directory at any depth.
  • ! negation is honored: a later negation can re-include a file removed by an earlier pattern, but cannot add a file that was never discovered.
  • Excluded files are omitted from the packaged archive and are ignored when detecting the Python entry point.

The [dependencies] Section

The [dependencies] section lists requirements needed to run the plugin and filter compatible database versions.

[dependencies]
database_version = ">=3.2.0,<4.0.0"
python = ["requests>=2.31,<3", "pydantic~=2.0"]
FieldTypeRequiredDescription
database_versionstringYesSemVer version requirement for compatible InfluxDB 3 database versions.
pythonarray of stringsNoPEP 508 Python package requirement strings. Omitted or empty means no Python dependencies.

dependencies.database_version

The database_version field is a Rust semver version requirement for compatible InfluxDB 3 database versions:

database_version = ">=3.2.0,<4.0.0"

Use a range that reflects the database versions the plugin supports. The SDK validates the requirement syntax and registry consumers can use it for compatibility filtering.

dependencies.python

The python field is an optional array of PEP 508 requirement strings for Python packages the plugin needs at runtime:

python = ["requests>=2.31,<3", "pydantic~=2.0"]

Omitting python or setting it to an empty array means the plugin has no declared Python package dependencies. Each entry must parse as a PEP 508 requirement. The SDK preserves the original requirement strings.

Validation

Manifest parsing has two phases:

  1. TOML structure and required fields are parsed.
  2. Field-level validation checks names, versions, descriptions, triggers, URLs, dependency ranges, and Python requirements.

Syntax errors, missing required fields, or wrong TOML container shape are reported as root-level TOML parse errors. If manifest_schema_version is malformed or uses an unsupported major, parsing stops with that schema-version error. Otherwise, the parser reports all field-level validation errors it can find in one pass.

Schema Versioning

manifest_schema_version uses <major>.<minor> form.

Within a supported major version, fields may be added and unknown fields are ignored. Breaking changes require a new major version. Consumers reject unsupported majors instead of guessing.


Back: The Registry | Next: The Plugin Directory Format

The Plugin Directory Format

A plugin is a directory containing a manifest.toml and the Python source that implements the triggers the manifest declares. This page specifies the directory-layout contract the SDK validates: which file is the entry point, and how declared triggers bind to Python functions.

This is a format contract in the same sense as the manifest and registry index formats: every consumer — the CLI that packages a plugin and the runtime that loads it — must agree on it.

Required files

  • manifest.toml at the plugin root is required. See The Manifest Format.
  • A Python entry point at the plugin root (see below) is required.

Entry-point detection

The entry point is determined from the top-level regular files of the plugin directory. Subdirectories are not searched, and symbolic links are excluded.

  • Multi-file plugin — the root contains __init__.py. That file is the entry point; any number of helper modules may sit alongside it. __init__.py takes priority over any other top-level .py file.
  • Single-file plugin — the root contains no __init__.py and exactly one top-level .py file. That file is the entry point.
  • No entry point — the root contains no top-level regular .py file. This is a validation error.
  • Ambiguous — the root contains no __init__.py but two or more top-level .py files. This is a validation error; add __init__.py to declare a multi-file plugin, or keep only one .py file.

Detection rules:

  • Non-.py files (for example requirements.txt, README.md) are ignored for entry-point detection.
  • Symbolic links are excluded (archives store the link target, not the link).
  • Subdirectories are ignored; nested .py files (for example pkg/helper.py) do not count, and a directory named foo.py is not an entry point.
  • Matching is case-sensitive: Foo.PY and __INIT__.py are not treated as foo.py/__init__.py.

Interaction with [plugin].exclude. Source-file selection applies exclude patterns before entry-point detection runs. Only the files that survive selection are considered when classifying the entry point; excluded top-level .py files do not count. This means exclude can remove what would otherwise be the sole entry point, yielding the ordinary “no entry point” validation error.

Trigger binding

Each trigger declared in manifest.toml’s plugin.triggers must be implemented as a top-level synchronous def <trigger>(...) in the entry point.

  • A top-level def <trigger>(...) satisfies the trigger.
  • A top-level decorated function (@deco then def <trigger>) counts — a decorator is not indirection.
  • An async def <trigger>(...) is rejected: the runtime invokes trigger functions synchronously.
  • A definition that is not top-level does not count: class methods, nested defs, defs guarded by if/try, re-exports, and module-level assignments (<trigger> = something) all fail to bind.
  • If a name is defined more than once at the top level, the last definition wins (mirroring Python’s own rebind semantics).
  • The entry-point source must parse as valid Python 3; a parse error is reported and no trigger checks run.

Diagnostics

A missing or malformed manifest.toml is reported on its own; entry-point detection does not run, because the entry point is classified from the files that survive source-file selection, and selection requires a valid manifest (to apply [plugin].exclude patterns). When the manifest parses successfully, validation continues: source-file selection, entry-point classification, and trigger checks all run, and multiple cross-file diagnostics from that stage are collected together so authors can fix everything in one pass.


Back: The Manifest Format | Next: The Registry Index Format

The Registry Index Format

A registry index describes the plugin versions published by one registry. It

  • is a single index.json file
  • is generated and updated by the SDK from validated manifests and packaged artifacts
  • contains every published plugin version
  • is consumed by tools that browse, resolve, and install plugins.
  • should never be edited manually; edits should only be made by the SDK tooling.

This page specifies the on-disk format of index.json: required fields, validation rules, identity, and canonical serialization. It does not specify how the index file is fetched, cached, or authenticated. Transport concerns (URL schemes for the index location, HTTP cache headers, redirect handling, private-registry credentials, missing-file responses) are the responsibility of the registry consumer and are out of scope for this document. Credentials for private registries are supplied via consumer-side registry configuration and applied at fetch time; they are never embedded in the index.

Minimal Example

{
  "index_schema_version": "2.0",
  "artifacts_url": "https://plugins.example.com/artifacts",
  "plugins": []
}

Complete Example

{
  "index_schema_version": "2.0",
  "artifacts_url": "https://plugins.example.com/artifacts",
  "plugins": [
    {
      "name": "downsampler",
      "version": "1.2.0",
      "published_at": "2026-04-29T18:45:12Z",
      "description": "Notify an HTTP endpoint on every WAL commit.",
      "triggers": ["process_writes", "process_scheduled_call"],
      "homepage": "https://influxdata.com",
      "repository": "https://github.com/influxdata/plugin-downsampler",
      "documentation": "https://github.com/influxdata/plugin-downsampler/readme.md",
      "dependencies": {
        "database_version": ">=3.2.0,<4.0.0",
        "python": ["requests>=2.31,<3", "pydantic~=2.0"]
      },
      "hash": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
    }
  ]
}

Index Structure

Every index file consists of these fields and sections:

  • index_schema_version - Root-level index schema version.
  • artifacts_url - Base URL where flat artifact files are hosted.
  • plugins - Array of per-version plugin entries.
    • name - Plugin name.
    • version - Plugin version.
    • published_at - Original publication timestamp.
    • description - One-line description.
    • triggers - Trigger types implemented by the plugin.
    • homepage - Optional project homepage URL.
    • repository - Optional source repository URL.
    • documentation - Optional documentation URL.
    • dependencies - Runtime compatibility and Python package requirements.
    • hash - SHA-256 hash of the published archive.
    • yanked - Optional flag marking a version unavailable for new resolution.

Unknown fields are ignored within a supported schema major. Do not use unknown fields for durable custom metadata: a future schema version may define them. The key dependencies.plugins is reserved for a future inter-plugin dependency format.

Top-Level Entries

EntryTypeRequiredDescription
index_schema_versionstringYesIndex schema version in <major>.<minor> form. Parsed before field-level validation.
artifacts_urlstringYesBase URL where flat artifact files are hosted.
pluginsarrayYesPer-version plugin entries. Empty registries use an empty array.

index_schema_version

index_schema_version must be a root-level string:

"index_schema_version": "2.0"

The value uses <major>.<minor> form. Consumers accept known major version 2, including newer minor versions such as 2.1, and reject unsupported majors instead of guessing.

If index_schema_version is malformed or uses an unsupported major, parsing stops with that schema-version error before field-level validation.

artifacts_url

artifacts_url is the base URL under which artifact files are hosted. Artifacts are addressed with this flat naming convention:

{artifacts_url}/{name}-{version}.tar.gz

The artifact URL shape is fixed. There are no templating markers in artifacts_url and no per-entry artifact URL override; the path is always {name}-{version}.tar.gz directly under the base. Consumers can compute the URL for any entry from (artifacts_url, name, version) alone.

Supported schemes:

SchemeUse
https://Recommended default for public and private registries.
http://Local development or trusted internal networks.
file://Offline, air-gapped, or appliance-style deployments.

Unsupported schemes are rejected, including oci://, s3://, git://, git+https://, git+ssh://, ftp://, and sftp://.

Use an object store’s HTTPS endpoint rather than a native storage URI such as s3://.

Plugin Entries

Each object in plugins[] represents one published plugin version.

FieldTypeRequiredDescription
namestringYesPlugin name copied from the manifest.
versionstringYesPlugin version copied from the manifest. Must be valid SemVer 2.0.0.
published_atstringYesOriginal publication timestamp for this exact version.
descriptionstringYesOne-line description copied from the manifest.
triggersarray of stringsYesNon-empty trigger list copied from the manifest.
homepagestringNoHTTP or HTTPS URL copied from the manifest.
repositorystringNoHTTP or HTTPS URL copied from the manifest.
documentationstringNoHTTP or HTTPS URL copied from the manifest.
dependenciesobjectYesDependency metadata copied from the manifest.
hashstringYesSHA-256 hash of the published archive.
yankedbooleanNoPresent and true when this version is yanked. Absence means false.

Relationship to Manifest

All entry fields except published_at, hash, and yanked are copied verbatim from the plugin’s manifest.toml. The SDK does not transform or normalize manifest values during index generation; canonical lowercase-and-underscore name normalization for collision checks happens during validation, not in the stored value. See The Manifest Format for authoring rules.

plugins.name

name is the plugin name copied from the manifest’s plugin.name:

"name": "downsampler"

The name follows the manifest name rule: 1 to 64 ASCII characters, starts with an ASCII letter, remaining characters are ASCII letters, digits, _, or -. Windows reserved device names (con, prn, aux, nul, com0-com9, lpt0-lpt9) are rejected case-insensitively. See Manifest: plugin.name for the canonical definition and examples.

Names are stored case-preserving. Registry collision checks use a canonical form: lowercase, with - replaced by _. Two different spellings that share a canonical name cannot coexist in one registry, even across versions. For example, foo-bar and foo_bar cannot both appear.

plugins.version

version is the plugin version copied from the manifest’s plugin.version:

"version": "1.2.0"

The value must be valid SemVer 2.0.0. The SDK preserves the full version string, including any pre-release or build metadata.

Version identity uses SemVer precedence, which ignores build metadata. 1.0.0 and 1.0.0+build.7 are the same version for uniqueness and ordering, and only one of them can appear in a registry. To publish changed plugin contents, bump the pre-release or release version, not the build metadata.

plugins.published_at

published_at records the original publication time for this exact version. It uses the Cargo registry pubtime shape:

"published_at": "2026-04-29T18:45:12Z"

The timestamp must be UTC, use uppercase T and Z, include seconds precision, and represent a real calendar time. Offsets, fractional seconds, lowercase z, leap seconds, and non-UTC forms are rejected.

published_at is set on first publish and preserved verbatim when an entry is yanked or unyanked.

plugins.description

description is the one-line description copied from the manifest’s plugin.description:

"description": "Downsample data on every WAL write."

The description must be non-empty, single-line, and no longer than 200 characters. Newline (\n) and carriage return (\r) characters are rejected. Descriptions are stored in Unicode NFC form by canonical serialization.

plugins.triggers

triggers lists the trigger functions the plugin implements, copied from the manifest’s plugin.triggers:

"triggers": ["process_writes", "process_scheduled_call"]

The array must contain at least one value. Supported trigger values are:

  • process_writes
  • process_scheduled_call
  • process_request

Unknown trigger strings are rejected.

plugins.homepage

homepage is an optional URL to the plugin’s home page, copied from the manifest’s plugin.homepage:

"homepage": "https://influxdata.com"

When present, the URL must parse and use the http or https scheme.

plugins.repository

repository is an optional URL to the plugin’s source repository, copied from the manifest’s plugin.repository:

"repository": "https://github.com/influxdata/plugin-downsampler"

When present, the URL must parse and use the http or https scheme.

plugins.documentation

documentation is an optional URL to the plugin’s documentation, copied from the manifest’s plugin.documentation:

"documentation": "https://github.com/influxdata/plugin-downsampler/readme.md"

When present, the URL must parse and use the http or https scheme.

plugins.dependencies

dependencies is the dependency metadata copied from the manifest’s [dependencies] table. It has the same shape:

"dependencies": {
  "database_version": ">=3.2.0,<4.0.0",
  "python": ["requests>=2.31,<3", "pydantic~=2.0"]
}
FieldTypeRequiredDescription
database_versionstringYesSemVer version requirement for compatible InfluxDB 3 database versions.
pythonarray of stringsNoPEP 508 Python package requirement strings. Omitted or empty means no Python dependencies.

database_version parses as a Rust semver version requirement. Each python entry parses as a PEP 508 requirement. The SDK preserves the original requirement strings.

There is no field for plugin-to-plugin dependencies. The key dependencies.plugins is reserved in the manifest for a future inter-plugin dependency format and is correspondingly absent from the index.

plugins.hash

hash is the SHA-256 hash of the published archive. It uses this canonical form:

"hash": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"

The literal prefix sha256: is followed by exactly 64 lowercase hexadecimal characters. The hash is calculated over the archive bytes and is verified by consumers before extraction.

plugins.yanked

yanked marks a version as unavailable for new resolution without deleting the entry or the artifact:

"yanked": true

Existing lockfiles can still resolve the exact artifact. To yank a version, the SDK writes yanked: true; to unyank, it removes the field. Absence of the field means the version is not yanked. yanked is the only entry field that can change after publication.

Identity And Uniqueness

Within one index, (name, version) must be unique.

Version identity uses SemVer precedence, which ignores build metadata. Canonical name form (lowercase, with - replaced by _) is checked across the registry, so foo-bar, foo_bar, and FOO-BAR cannot be published as separate plugins in one registry.

Global registry identity is outside the index. Consumers identify a registry entry by (index_url, name, version), where index_url is the URL configured by the registry consumer.

Immutability

Once an entry is added to a registry, its fields are immutable. The SDK rejects any attempt to insert a second entry with the same (name, version), so a published version’s description, triggers, dependencies, hash, URL fields, and published_at cannot be changed in place. To publish changed plugin contents, bump plugin.version in the manifest and publish a new entry.

The only field that can change after publication is yanked. Yanking and unyanking flip that field on the existing entry; all other fields, including published_at, are preserved verbatim.

Validation

Index parsing has two phases:

  1. JSON structure and required fields are parsed.
  2. Field-level validation checks names, versions, descriptions, triggers, URLs, dependency ranges, Python requirements, timestamps, and hashes against the rules defined in each field’s section above.

Syntax errors, missing required fields, or wrong JSON container shape are reported as root-level JSON parse errors. If index_schema_version is malformed or uses an unsupported major, parsing stops with that schema-version error. Otherwise, the parser reports all field-level validation errors it can find in one pass, including duplicate entries and canonical-name collisions.

Schema Versioning

index_schema_version uses <major>.<minor> form.

Within a supported major version, fields may be added and unknown fields are ignored. Breaking changes require a new major version. Consumers reject unsupported majors instead of guessing.

Indexes using schema 1.x must be backfilled with a required published_at field on every plugins[] entry before they can be parsed by schema 2.0 consumers.

Canonical Serialization

The SDK writes index JSON in canonical form:

  • Field ordering matches the schema order shown above.
  • plugins[] is sorted by name ascending, then version ascending by SemVer precedence.
  • Pretty-printed JSON uses two-space indentation.
  • The file ends with a trailing newline.
  • Description strings are normalized to Unicode NFC.
  • Optional fields are omitted when absent.
  • yanked is omitted when false.

Back: The Plugin Directory Format | Next: None