AnsibleXXVIII · Plugins, Lookups and FiltersPlugins, lookups and filters
The plugin types and how they load
What you'll learn
- Name the plugin types and state what each one hooks into
- Enumerate what a controller actually has, per type, with ansible-doc
- Identify which plugin type governs a behaviour you want to change
- Say where plugins may live in a role, a collection or a project
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
Almost every “can Ansible do X” question has the same answer: yes, and the thing that does it is a plugin. Which kind of plugin is the part worth knowing, because it tells you where to look and what you are allowed to change.
This lesson is a map, not a development course. You will very probably never write a connection plugin. You will, several times a year, need to know that the behaviour annoying you is a connection plugin’s behaviour and not a module’s.
The types, and what each hooks into
$ for t in connection inventory lookup filter test callback cache strategy become shell vars; do printf '%-12s %s\n' "$t" "$(ansible-doc -t $t -l | wc -l)"; doneconnection 4
inventory 9
lookup 25
filter 122
test 82
callback 5
cache 2
strategy 4
become 3
shell 3
vars 1Those numbers are the whole ansible-core extension surface. Everything
else arrives with collections.
| Type | Hooks into | When you care |
|---|---|---|
| connection | How the controller reaches a target | Containers, network devices, Windows, anything that is not SSH |
| inventory | Where the host list comes from | Cloud APIs, a CMDB, a script — Part XXIX |
| lookup | Fetching data on the controller during templating | Reading files, environment, secret stores — lesson 2 |
| filter | Transforming a value in a Jinja expression | Every day — lesson 5 |
| test | Answering a yes/no question in Jinja | is defined, is match — lesson 6 |
| callback | Reacting to run events; all output | Changing what a run reports — lesson 7 |
| cache | Storing gathered facts between runs | Fleet-scale runs — Part XXXIV |
| strategy | The order tasks execute across hosts | linear versus free |
| become | How privilege escalation is performed | sudo, su, doas, runas |
| shell | Which shell dialect commands are built for | Windows targets, unusual shells |
| vars | Injecting variables from a source | group_vars/host_vars loading itself |
| action | Controller-side work wrapped around a module | Explained in Part VIII |
| terminal / cliconf / httpapi / netconf | Network device interaction | Only with network collections installed |
Two entries in that table are worth pausing on because they are usually assumed to be built-in machinery rather than plugins.
vars has exactly one plugin, and it is how group_vars works.
$ ansible-doc -t vars -lansible.builtin.host_group_vars In charge of loading group_vars and host_va...group_vars/ and host_vars/ are not a special case in the engine.
They are a plugin, which is why a collection can add another source of
variables that behaves the same way — and why Part XII’s precedence
list has an entry for it rather than treating it as the ground truth.
shell is not command versus shell. The shell plugin decides
how Ansible builds the command line it sends: quoting, path separators,
temporary directory syntax. sh for POSIX targets, powershell and
cmd for Windows. It is invisible until you target Windows, and then
it explains a whole category of confusion at once.
Where plugins live
Four places, in the order they are searched:
1. Adjacent to a playbook. A directory named for the plugin type
beside the playbook: filter_plugins/, lookup_plugins/,
callback_plugins/, and library/ for modules. Historical, still
works, appropriate for a one-repository helper.
2. Inside a role. The same directories at the top of a role directory. A plugin here is loaded while that role is in play — useful for a role-private helper, awkward for anything shared, because its availability depends on execution context.
3. Inside a collection, under plugins/<type>/. plugins/filter/,
plugins/lookup/, plugins/modules/. This is the modern answer and
the only one that gives the plugin a fully qualified name, a version
and a distribution mechanism.
4. On a configured plugin path. Each type has its own setting —
DEFAULT_FILTER_PLUGIN_PATH, DEFAULT_CALLBACK_PLUGIN_PATH, and so on
— each defaulting to a per-user directory plus a system-wide one.
$ ansible-config dump | grep -E '_PLUGIN_PATH'DEFAULT_ACTION_PLUGIN_PATH(default) = ['/home/opsuser/.ansible/plugins/action', '/usr/share/ansible/plugins/action']
DEFAULT_CALLBACK_PLUGIN_PATH(default) = ['/home/opsuser/.ansible/plugins/callback', '/usr/share/ansible/plugins/callback']
DEFAULT_FILTER_PLUGIN_PATH(default) = ['/home/opsuser/.ansible/plugins/filter', '/usr/share/ansible/plugins/filter']
DEFAULT_LOOKUP_PLUGIN_PATH(default) = ['/home/opsuser/.ansible/plugins/lookup', '/usr/share/ansible/plugins/lookup']Illustrative output
Note the per-user directory again. Everything Part XXVII said about
per-user collection paths applies identically to plugins, with one
extra wrinkle: plugins outside a collection have no version. A
filter in ~/.ansible/plugins/filter/ cannot be pinned, cannot be
verified and does not appear in any manifest. It is the least
reproducible thing you can put on a controller.
Which plugin governs which behaviour
The practical use of the map. A behaviour you want to change, and the plugin type that owns it:
| “I want to…” | Plugin type |
|---|---|
| …run tasks against a container instead of over SSH | connection |
| …build the host list from the cloud provider’s API | inventory |
| …read a value from a secret store into a variable | lookup |
| …reshape a list of dictionaries into a dictionary | filter |
…check whether a string matches a pattern in when: | test |
| …emit per-task timings, or JUnit XML for CI | callback |
| …keep facts between runs so the next run is faster | cache |
| …let fast hosts move ahead instead of waiting for the batch | strategy |
…escalate with doas rather than sudo | become |
| …keep the output of a task for later use | none — that is register |
The last row is the useful one. Not every behaviour is a plugin.
register, when, loop, delegate_to and serial are keywords
— play, block and task directives handled by the engine, documented
under a different command:
$ ansible-doc -t keyword -l | wc -l109If ansible-doc -t <type> -l cannot find what you are looking for,
try ansible-doc -t keyword -l before concluding it does not exist.
Knowledge check
Knowledge check · 4 questions
Q1. Which statement about where plugin code executes is correct?
Q2. You want a run to emit per-task timings without editing any playbook. Which plugin type governs that?
Q3. Which are true about plugins that live in a collection rather than a filter_plugins/ directory beside a playbook? Select all that apply.
Q4. group_vars/ and host_vars/ loading is implemented as a vars plugin rather than being hard-coded in the engine.
Passing score: 75%. Answers are checked in this browser.