Skip to content

Filter plugins

A filter plugin adds a command to the filter language. The built-in commands select a task by its name and by its group. A plugin command can select by any property of a task.

A filter ships in a plugin package with an entry point in the winslow.filter_plugins group (see How Winslow finds a plugin).

Write a filter

A filter declares its commands, one match rule, and one explanation:

examples/winslow-sample-filter-plugin/winslow_sample_filter_plugin/filters.py
from winslow.filter import TaskFilter


class DependsOnFilter(TaskFilter):
    short_command = "n"
    long_command = "needs"

    def matches(self, task):
        return any(self.value.lower() in str(dep) for dep in task.dependent_tasks)

    def explain(self):
        return f"depends on {self.value}"

The command !needs, or its short form !n, then works in every filter expression. The operators of the language combine a plugin command like any other selection. The task overview shows the dependencies of a task, so the result of this filter is visible on the screen:

winslow run --filter '!needs unit-test'   # integration-test
winslow run --filter '!n test'            # integration-test, build-wheel

The three parts of the contract:

  • The parser passes the text after the command to the filter as self.value. The value is always a string. A filter that converts it must not raise on a bad value: return false, and let the bad value select nothing.
  • matches runs once per task, and true selects the task.
  • explain returns the rule as a short sentence, for example depends on unit-test. A parsed query reports itself with these sentences.

The command names

Declare a long_command, and add a short_command when a shorthand helps. Each command must be unique across every registered filter. A clash stops the startup with an error that names both filters. The built-in commands are !g and !group; a bare word needs no command, because it is the name selection. The commands also give the filter its name (see The name of a plugin).

A complete example

The winslow-sample-filter-plugin package holds the filter of this page:

winslow-sample-filter-plugin/
├── pyproject.toml
└── winslow_sample_filter_plugin/
    └── filters.py       # adds the !needs command

Download this example

The pyproject.toml declares the entry point:

examples/winslow-sample-filter-plugin/pyproject.toml
[project]
name = "winslow-sample-filter-plugin"
version = "0.1.0"
description = "Sample Winslow plugin: one filter command"
requires-python = ">=3.12"
dependencies = ["winslow"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["winslow_sample_filter_plugin"]

[project.entry-points."winslow.filter_plugins"]
winslow-sample-filter-plugin = "winslow_sample_filter_plugin"

Install the package into the workflow project. An editable install keeps a local plugin live while you work on it:

uv add --editable ../winslow-sample-filter-plugin

The command adds the dependency and a local source to the pyproject.toml of the project:

pyproject.toml of the workflow project
[project]
dependencies = [
    "winslow-sample-filter-plugin",
]

[tool.uv.sources]
winslow-sample-filter-plugin = { path = "../winslow-sample-filter-plugin", editable = true }