Skip to main content
RunBook Academy

AnsibleXXVIII · Plugins, Lookups and FiltersPlugins, lookups and filters

lookup, query and loops

Intermediate⏱ ~17 minansible-playbook

What you'll learn

  • Predict the type a lookup returns and prove it with type_debug
  • Choose between lookup, query and wantlist=True for a given use
  • Recognise the loop failure a string-returning lookup causes
  • Use the errors option deliberately rather than inheriting strict behaviour

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

Not yet marked complete on this device.

lookup() and query() call the same plugin with the same arguments and return different types. That is the whole lesson, and it is worth a lesson because the difference is invisible in the common case and catastrophic in the loop case.

The demonstration

Read-only / Safelq.yml
- name: lookup versus query
hosts: localhost
gather_facts: false
vars:
  pkgs: ['nginx', 'curl', 'jq']
tasks:
  - ansible.builtin.debug:
      msg: "{{ lookup('ansible.builtin.items', pkgs) }}"

  - ansible.builtin.debug:
      msg: "{{ lookup('ansible.builtin.items', pkgs) | type_debug }}"

  - ansible.builtin.debug:
      msg: "{{ query('ansible.builtin.items', pkgs) }}"

  - ansible.builtin.debug:
      msg: "{{ query('ansible.builtin.items', pkgs) | type_debug }}"
Read-only / Safethe same call, two return types
$ ansible-playbook -i localhost, lq.yml
TASK [lookup returns a string] *************************************************
ok: [localhost] => {
  "msg": "nginx,curl,jq"
}

TASK [type of lookup result] ***************************************************
ok: [localhost] => {
  "msg": "str"
}

TASK [query returns a list] ****************************************************
ok: [localhost] => {
  "msg": [
      "nginx",
      "curl",
      "jq"
  ]
}

TASK [type of query result] ****************************************************
ok: [localhost] => {
  "msg": "list"
}

"nginx,curl,jq" — one string, with commas in it. Not a list that prints with commas. The type_debug filter is the proof and it is worth reaching for any time an expression behaves oddly; it answers “what is this actually” in one filter.

lookup() joins. It takes whatever the plugin returned and comma-joins it into a string, because the historical use of a lookup was to produce a scalar for a template.

query() does not. It returns the plugin’s list as a list. It is an alias for lookup(..., wantlist=True):

Read-only / Safequery is wantlist
$ ansible-playbook -i localhost, wantlist.yml
TASK [wantlist type] ***********************************************************
ok: [localhost] => {
  "msg": "list vs str"
}

The loop case

This is where it stops being a curiosity:

Read-only / Safethe bug
- ansible.builtin.debug:
  msg: "item is {{ item }}"
loop: "{{ lookup('ansible.builtin.items', pkgs) }}"
Read-only / Safeansible-core 2.21 refuses
$ ansible-playbook -i localhost, lq.yml
TASK [loop over lookup] ********************************************************
[ERROR]: The `loop` value must resolve to a 'list', not 'str'.
Origin: /home/opsuser/project/lq.yml:22:13

20       ansible.builtin.debug:
21         msg: "item is {{ item }}"
22       loop: "{{ lookup('ansible.builtin.items', pkgs) }}"
             ^ column 13

Provide a list of items/templates, or a template resolving to a list.

fatal: [localhost]: FAILED! => {"msg": "The `loop` value must resolve to a 'list', not 'str'."}

A clear error, pointing at the exact line and column, saying precisely what is wrong. That is a good outcome and it is not one you can rely on having in older environments — the historic behaviour of feeding a string to something expecting a sequence is to iterate it, one character at a time.

Which to use

SituationUse
Interpolating a value into a template or a stringlookup()
Feeding loop:query()
Setting a variable that other tasks will iteratequery()
A module parameter that takes a listquery()
A module parameter that takes a scalarlookup()
Any lookup that can return more than one resultquery(), then decide

The default worth adopting: query() unless you specifically want a joined string. A single-element list is easy to handle; a string that should have been a list is a bug that appears months later.

Note that query() always returns a list even for lookups that conceptually return one value:

Read-only / Safequery wraps even a scalar
$ ansible-playbook -i localhost, scalar.yml
TASK [single item still a string] **********************************************
ok: [localhost] => {
  "msg": "list / str"
}

So query('env', 'HOME') gives ['/home/opsuser'], not '/home/opsuser'. For a genuinely scalar read, lookup() is the right call and | first on a query result is a smell.

errors

Every lookup accepts an errors argument controlling what happens when it cannot do its job. The default is strict.

Read-only / Safethree behaviours for a missing file
$ ansible-playbook -i localhost, err.yml
TASK [errors ignore] ***********************************************************
ok: [localhost] => {
  "msg": "got []"
}

TASK [errors warn] *************************************************************
[WARNING]: An error occurred while running the lookup plugin 'ansible.builtin.file': Unable to access the file 'nope.txt': File not found. Use -vvvvv to see paths searched.
ok: [localhost] => {
  "msg": "got []"
}

TASK [errors strict (default)] *************************************************
[ERROR]: Task failed: Finalization of task args for 'ansible.builtin.debug' failed: Error while resolving value for 'msg': The lookup plugin 'ansible.builtin.file' failed: Unable to access the file 'nope.txt': File not found. Use -vvvvv to see paths searched.
fatal: [localhost]: FAILED! => {"msg": "Task failed: Finalization of task args for 'ansible.builtin.debug' failed: Error while resolving value for 'msg': The lookup plugin 'ansible.builtin.file' failed: Unable to access the file 'nope.txt': File not found."}
  • strict (default) — the lookup failure fails the task. Correct for anything the play depends on.
  • warn — logs a warning and yields empty. The failure is visible in the run output and does not stop it.
  • ignore — yields empty silently. got [] with no indication anything went wrong.

Knowledge check

Knowledge check · 4 questions

  1. Q1. What does lookup('ansible.builtin.items', ['nginx', 'curl', 'jq']) return?

  2. Q2. Which use of a string-returning lookup is most dangerous?

  3. Q3. Which statements about the errors argument to a lookup are correct? Select all that apply.

  4. Q4. query() returns a list even for a lookup that conceptually yields a single value, such as an environment variable.

Passing score: 75%. Answers are checked in this browser.