# How to loop over "include\_role" with a list of variable dictionaries?

**URL:** <https://forum.ansible.com/t/how-to-loop-over-include-role-with-a-list-of-variable-dictionaries/44625>\
**Category:** Get Help\
**Tags:** variables, role, loop\
**Created:** [October 7, 2025, 3:51pm UTC](https://forum.ansible.com/t/how-to-loop-over-include-role-with-a-list-of-variable-dictionaries/44625 "2025-10-07T15:51:59Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![Hoeze](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/hoeze/32/9132_2.png) [@Hoeze](https://forum.ansible.com/u/Hoeze)\
**Post date:** [October 7, 2025, 3:51pm UTC](https://forum.ansible.com/t/how-to-loop-over-include-role-with-a-list-of-variable-dictionaries/44625/1 "2025-10-07T15:51:59Z")

</div>

Hi all, I am trying to loop over my “create\_user” role with a list of “users”:

```yaml
    - name: Setup users
        include_role:
          name: create_user
      vars: "{{ user_vars }}"
      loop: "{{ users_to_create }}"
      loop_control:
        loop_var: "{{ user_vars }}"

```

Unfortunately, this does not work, because one cannot dynamically specify the vars dictionary:  
`ERROR! Vars in a IncludeRole must be specified as a dictionary.`

Another solution I tried, was combining `block` and `set_fact`:

```yaml
    - name: Setup users
      block:
        - name: Build vars for user
          set_fact: "{{ user_vars }}"
        - name: Setup user
          include_role:
            name: create_user
      loop: "{{ users_to_create }}"
      loop_control:
        loop_var: "{{ user_vars }}"

```

Now, this does not work because `block` does not support `loop`.

Third try, let’s explicitly specify the vars:

```auto
    - name: Setup steward users
      include_role:
        name: create_user
      vars:
        create_user __user_name: "{{ item.create_user__ user_name | default(omit)}}"
        create_user __user_group: "{{ item.create_user__ user_group | default(omit)}}"
        create_user __user_uid: "{{ item.create_user__ user_uid | default(omit)}}"
        create_user __user_home: "{{ item.create_user__ user_home | default(omit)}}"
        create_user __ssh_authorized_keys: "{{ item.create_user__ ssh_authorized_keys | default(omit)}}"
        create_user __ssh_key: "{{ item.create_user__ ssh_key | default(omit)}}"
      loop: "{{ users_to_create }}"

```

This failed horribly, because now instead of getting the `user_home` specified in the role’s `defaults`, I get a lot of ` __omit_place_holder__ ` values.

**So, what is the Ansible way of looping over `include_role` with a list of variable dicts?**

---

<div class="post-metadata">

**Author:** ![mikemorency](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/mikemorency/32/2672_2.png) [@mikemorency](https://forum.ansible.com/u/mikemorency)\
**Post date:** [October 8, 2025, 1:18pm UTC](https://forum.ansible.com/t/how-to-loop-over-include-role-with-a-list-of-variable-dictionaries/44625/2 "2025-10-08T13:18:25Z")

</div>

Deceptively tricky! One note: your using loop\_var incorrectly. loop\_var sets the variable that the loop data is stored in, if the default `item` is not desired.

The block solution does not work, but the solution for a loop over a block is to do an extra layer of import/include. For example

```auto
---
#main.yml
- name: Include user tasks
  include_tasks: users.yml
  loop: "{{ users_to_create }}"
  loop_control:
    loop_var: user_to_create

---
#users.yml
- name: Build vars for user
  set_fact: { "{{ __user_var.key }}": "{{__ user_var.value }}" }
  loop: "{{ user_to_create | dict2items }}"
  loop_control:
    loop_var: __user_var

- name: Setup user
  include_role:
    name: create_user

```

Note that ansible does not do variable scopes, so if the first user defines a variable (like `create_user__user_group`) and the second user doesnt, you will probably run into trouble.

I would suggest handling this in the role, since that seems easier. You could allow the role to accept a list of users, or have the role accept empty strings as inputs and then ignore them. That way your third attempt would work:

```auto
    - name: Setup steward users
      include_role:
        name: create_user
      vars:
        create_user __user_name: "{{ item.create_user__ user_name | default('')}}"
        create_user __user_group: "{{ item.create_user__ user_group | default('')}}"
        create_user __user_uid: "{{ item.create_user__ user_uid | default('')}}"
        create_user __user_home: "{{ item.create_user__ user_home | default('')}}"
        create_user __ssh_authorized_keys: "{{ item.create_user__ ssh_authorized_keys | default('')}}"
        create_user __ssh_key: "{{ item.create_user__ ssh_key | default('')}}"
      loop: "{{ users_to_create }}"

```

---

<div class="post-metadata">

**Author:** ![Hoeze](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/hoeze/32/9132_2.png) [@Hoeze](https://forum.ansible.com/u/Hoeze)\
**Post date:** [October 10, 2025, 10:55am UTC](https://forum.ansible.com/t/how-to-loop-over-include-role-with-a-list-of-variable-dictionaries/44625/3 "2025-10-10T10:55:07Z")

</div>

Thanks a lot for your detailed explanation, @mikemorency !

> Note that ansible does not do variable scopes, so if the first user defines a variable (like `create_user__user_group`) and the second user doesnt, you will probably run into trouble.

Oh oops, never thought of that. Scary 😅

> I would suggest handling this in the role, since that seems easier. You could allow the role to accept a list of users, or have the role accept empty strings as inputs and then ignore them.

Makes sense. Is there maybe a good way to replace `"null"` values with the defaults from `<role>/defaults/main.yml`?

---

<div class="post-metadata">

**Author:** ![mikemorency](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/mikemorency/32/2672_2.png) [@mikemorency](https://forum.ansible.com/u/mikemorency)\
**Post date:** [October 10, 2025, 1:19pm UTC](https://forum.ansible.com/t/how-to-loop-over-include-role-with-a-list-of-variable-dictionaries/44625/4 "2025-10-10T13:19:23Z")

</div>

I dont really have an elegant way. I think you would probably want to introduce _another_  
layer of variables that are used in the role only.

One solution:  
in the role vars/main.yml put:

```auto
create_user__defaults_user_group: foo

```

in defaults/main.yml you put (Im not sure this will work. if it doesnt you just need to put `foo` in two places)

```auto
create_user __user_group: "{{ create_user__ defaults_user_group }}"

```

and then in the role tasks:

```auto
- name: reassign variable
  set_fact:
    create_user __final_user_group: "{{ create_user__ user_group if create_user __user_group != '' else create_user__ defaults_user_group }}"

- name: do something
  debug:
    msg: user group is {{ create_user__final_user_group }}

```

---

<div class="post-metadata">

**Author:** ![utoddl](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/utoddl/32/249_2.png) [@utoddl](https://forum.ansible.com/u/utoddl)\
**Post date:** [October 10, 2025, 7:59pm UTC](https://forum.ansible.com/t/how-to-loop-over-include-role-with-a-list-of-variable-dictionaries/44625/5 "2025-10-10T19:59:51Z")

</div>

> [@Hoeze](#):
>
> ```auto
> - name: Setup users
> include_role: # [sic: fixed indentation]
> name: create_user
> vars: "{{ user_vars }}"
> loop: "{{ users_to_create }}"
> loop_control:
> loop_var: "{{ user_vars }}"
> 
> ```
> 
> Unfortunately, this does not work, because one cannot dynamically specify the vars dictionary:  
> `ERROR! Vars in a IncludeRole must be specified as a dictionary.`

You were really close, but a couple of things are tripping you up. (After the indentation, fixed above.)

The first is just syntax. “`loop_var`” expects a string that is the name it should use for your loop variable instead of `item`. You’re giving it a variable, which may be a string, but looking at the rest of your post I think you mean “user\_vars” literally, like so:

```
loop_control:
  loop_var: user_vars

```

The second thing is a higher-level problem. It looks like you want `vars:` to create several discrete variables based on the content of each item in `{{ users_to_create }}`. You can do that - as you found with your “Third try”, but all the ` __omit_place_holder__ ` values scared you off.

And this is why I called this a higher-level problem. If you absolutely want to go the route of setting a bunch of discrete variables (hint: you don’t), then your “Third try” is the way to go, `omit`s and all. Unfortunately your role then has to be (re)written to deal with the omit place holders _and_ undefined variables. Basically, you add a lot of code to your `include_role` task, and then have to add a lot more code to the tasks in your role to deal with the mess your discrete variables made.

It’s far better _in my opinion_ to pass in the `user_vars` (or `item` if you don’t use `loop_control.loop_var`) as a single dict and let the role pick out the pieces it needs. You never showed us what your `users_to_create` list looks like, so I made some assumptions and put together the example below. It doesn’t actually create users - the included role just contains a `debug` task - but it builds the values it can and `omit`s the ones it can’t. If your role has `defaults/main.yml` then you could pull in those when appropriate. The point is, it makes it much easier to look over a list of dicts and keeps you out of the lots-of-discrete-variables pattern.

> [@Hoeze](#):
>
> So, what is the Ansible way of looping over `include_role` with a list of variable dicts?

Opinions vary, but here’s the way _I_ would do it.

```yaml
---
# Playbook Hoeze_01.yml
- name: Loop over include_role
  hosts: localhost
  gather_facts: false
  vars:
    users_to_create:
      - user_name: "user1"
        user_group: "group1"
        user_uid: "100001"
        user_home: "/home/user1"
        ssh_authorized_keys: "_authorized_key_1_"
        ssh_key: "_ssh_key_1_"

      - user_name: "user2"
        user_uid: "100002"
        ssh_authorized_keys: "_authorized_key_2_"

  tasks:
    - name: Setup users
      include_role:
        name: hoeze_01_create_user
      loop: "{{ users_to_create }}"
      loop_control:
        loop_var: user_vars

```

and the included role:

```yaml
---
# roles/hoeze_01_create_user/tasks/main.yml
- name: Show the vars this role was given
  ansible.builtin.debug:
    msg: |
      user_name: "{{ user_vars.user_name }}"
      user_group: "{{ user_vars.user_group | default(user_vars.user_name) }}"
      user_uid: "{{ user_vars.user_uid | default(omit) }}"
      user_home: "{{ user_vars.user_home | default('/home/' ~ user_vars.user_name) }}"
      ssh_authorized_keys: "{{ user_vars.ssh_authorized_keys | default(omit) }}"
      ssh_key: "{{ user_vars.ssh_key | default(omit) }}"

```

And here’s the whole playbook run log:

```txt
utoddl@tango:~/ansible$ ansible-playbook Hoeze_01.yml -i localhost,

PLAY [Loop over include_role] **************************************************

TASK [Setup users] *************************************************************
included: hoeze_01_create_user for localhost => (item={'user_name': 'user1', 'user_group': 'group1', 'user_uid': '100001', 'user_home': '/home/user1', 'ssh_authorized_keys': '_authorized_key_1_', 'ssh_key': '_ssh_key_1_'})
included: hoeze_01_create_user for localhost => (item={'user_name': 'user2', 'user_uid': '100002', 'ssh_authorized_keys': '_authorized_key_2_'})

TASK [hoeze_01_create_user : Show the vars this role was given] ****************
ok: [localhost] => 
  msg: |-
    user_name: "user1"
    user_group: "group1"
    user_uid: "100001"
    user_home: "/home/user1"
    ssh_authorized_keys: "_authorized_key_1_"
    ssh_key: "_ssh_key_1_"

TASK [hoeze_01_create_user : Show the vars this role was given] ****************
ok: [localhost] => 
  msg: |-
    user_name: "user2"
    user_group: "user2"
    user_uid: "100002"
    user_home: "/home/user2"
    ssh_authorized_keys: "_authorized_key_2_"
    ssh_key: " __omit_place_holder__ 5cbb9d5f92391d488267e45624dd72729776c145"

PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0   

```

---

<div class="post-metadata">

**Author:** ![utoddl](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/utoddl/32/249_2.png) [@utoddl](https://forum.ansible.com/u/utoddl)\
**Post date:** [October 10, 2025, 8:43pm UTC](https://forum.ansible.com/t/how-to-loop-over-include-role-with-a-list-of-variable-dictionaries/44625/6 "2025-10-10T20:43:51Z")

</div>

> [@mikemorency](#):
>
> You could allow the role to accept a list of users, or have the role accept empty strings as inputs and then ignore them.

Congratulations! You managed to touch two of my pet peeves in one sentence. 🙂

The first is “singleton” vs. “list”. In principle, you’d like the user to be able to provide either a single thing or a list of those things - whichever is more natural. Think of the way we can specify tags on a task. It can be single:

```
tags: alpha

```

or a list:

```
tags:
  - alpha
  - beta

```

So _in principle_ it would be nice if `{{ users_to_create }}` could be a single dict or a list of dicts. You can either make that work by looping over the `include_role` task, or the included role’s `tasks/main.yml` could loop over its own `included_tasks`. Wherever we make that happen, it’s just a matter of saying

```
loop: "{{ [users_to_create] | flatten }}"

```

This does nothing to `users_to_create` if it’s already a list, but if it’s a single dict, now it’s a list with that dict as its single item. As long as whatever is being called can handle a list, then you don’t need one case for a list and another for a single dict. You get the single case for free if your code can handle lists.

* * *
The other pet peeve you touched on is defining otherwise undefined variables as empty strings. There are cases where either undef or length-zero strings are bad™, but in general I prefer to leave things undefined as long as possible. Those things often end up in parameters to modules where `|default(omit)` really shines. There's no way to undefine a variable in Ansible, so I avoid burning that bridge when I can. If I can't avoid it, I'll set a variable to `omit` rather than `""`. Then if it ends up as a module parameter, it has the intended effect of omitting that parameter anyway, and doesn't intrude on the case where `""` is a legitimate value.
