# Role in collection cannot find module in the same collection

**URL:** <https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676>\
**Category:** Get Help\
**Tags:** module, role, ubuntu, collections, ansible-lint, galaxy-ng\
**Created:** [April 14, 2026, 10:00am UTC](https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676 "2026-04-14T10:00:28Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![ab203972](https://avatars.discourse-cdn.com/v4/letter/a/ea5d25/32.png) [@ab203972](https://forum.ansible.com/u/ab203972)\
**Post date:** [April 14, 2026, 10:00am UTC](https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676/1 "2026-04-14T10:00:28Z")

</div>

We have an internal collection with roles, one of which lists available updates, and another that actually performs the updates, excluding necessary packages. I am trying to tweak the role that lists updates so that it doesn’t list the packages that are excluded in the actual update. I was unable to find a combination of Ansible functions/filters to accomplish this, so I have resorted to trying to write a module within this collection that helps with some string formatting. However, the role cannot find the module, even though they are within the same collection.

Below you can find my directory structure, role contents, and module contents:

```auto
# Working directory
├── CHANGELOG.md
├── README.md
├── galaxy.yml
├── meta
│ └── runtime.yml
├── plugins
│ └── modules
│ ├── __init__.py
│ └── add_string.py
├── renovate.json
└── roles
    ├── list_linux_updates
    │ ├── handlers
    │ │ └── main.yml
    │ ├── meta
    │ │ ├── argument_specs.yml
    │ │ └── main.yml
    │ └── tasks
    │ └── main.yml
    └── update_linux_packages
        ├── defaults
        │ └── main.yml
        ├── meta
        │ ├── argument_specs.yml
        │ └── main.yml
        └── tasks
            └── main.yml

```

```yaml
# list_linux_updates role
---
- name: List updates
  block:
    - name: Updates for RedHat/SUSE and derived distributions
      when: ansible_facts.pkg_mgr == 'dnf'
      block:
        - name: Prepare submitted packages to be excluded
          company.update_roles.add_string:
            in_list: list_linux_updates_packages_exclude
            in_string: "--exclude"
            prepend: true
          register: list_linux_updates_packages_exclude_prepped

        - name: Store submitted packages to be excluded
          ansible.builtin.set_fact:
            list_linux_updates_packages_exclude_joined: "{{ list_linux_updates_packages_exclude_prepped.output | join(' ') }}"

        - name: Gather available package updates for RedHat/SUSE systems
          ansible.builtin.command:
            cmd: "dnf check-update --quiet {{ list_linux_updates_packages_exclude_joined }}"
          register: list_linux_updates_updatable_packages_raw_RH
          changed_when: list_linux_updates_updatable_packages_raw_RH.rc in [0, 100]
          failed_when: list_linux_updates_updatable_packages_raw_RH.rc not in [0, 100]
          notify:
            - Write RedHat packages to variable

    - name: Updates for Debian/Ubuntu and derived distributions
      when: ansible_facts.pkg_mgr == 'apt'
      block:
        - name: Gather available package updates for Debian/Ubuntu systems
          ansible.builtin.command:
            cmd: apt-get upgrade --dry-run
          register: list_linux_updates_updatable_packages_raw_DB
          changed_when: list_linux_updates_updatable_packages_raw_DB.rc == 0
          notify:
            - Write Debian packages to variable

```

```python
# Module
#!/usr/bin/python
# Created using the official module in a collection example:
# https://docs.ansible.com/projects/ansible/latest/dev_guide/developing_modules_general.html#creating-a-module-in-a-collection
DOCUMENTATION = r'''
---
module: add_string
author: department_X
version_added: "1.0.0"
short_description: Prepends or appends a string to each string item in a list.
description:
  - Prepends or appends a string to each string item in a list.
  - It makes no changes on any host, and is purely needed for some formatting of text.
options:
    in_list:
        description: This is a list of strings to which the string must be added.
        required: true
        type: list
    in_string:
        description: This is the string to add to each element in the list.
        required: true
        type: str
    prepend:
        description: Prepends string to each element. If false, it will append instead.
        required: false
        type: bool
        default: true
'''

EXAMPLES = r'''
# Base usage
- name: Add "added" to every object in the list
  company.update_roles.add_string:
    in_list: ['ring', 'around', 'the', 'rosie']
    in_string: 'added'
  register: prepend_text

- name: Print output
  ansible.builtin.debug:
    var: prepend_text.output
# returns: ['added ring','added around','added the','added rosie']

# Append instead of prepend
- name: Add "added" to every object in the list
  company.update_roles.add_string:
    in_list: ['ring', 'around', 'the', 'rosie']
    in_string: 'added'
    prepend: false

- name: Print output
  ansible.builtin.debug:
    var: append_text.output
# returns: ['ring added','around added','the added','rosie added']
'''

RETURN = r'''
# These are examples of possible return values, and in general should use other names for return values.
input:
    description: The original name param that was passed in.
    type: list
    returned: always
    sample: ['ring','around','the','rosie']
output:
    description: The output message that the test module generates.
    type: list
    returned: always
    sample: ['added ring','added around','added the','added rosie']
'''

from ansible.module_utils.basic import AnsibleModule

def run_module():
    # define available arguments/parameters a user can pass to the module
    module_args = dict(
        in_list=dict(type='list', required=True),
        in_string=dict(type='str', required=True),
        prepend=dict(type='bool', required=False, default=True)
    )

    result = dict(
        changed=False,
        input=[],
        output=[]
    )

    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True
    )

    if module.check_mode:
        module.exit_json(**result)

    result['input'] = module.params['in_list']
    result['output'] = add_string(module.params['in_list'], module.params['in_string'], module.params['prepend'])

    if module.params['in_list'] != result['output']:
        result['changed'] = True

    module.exit_json(**result)

def add_string(in_list: list, in_string: str, prepend: bool):
    if in_list == []:
        return []
    else:
        out_list = []
        for item in in_list:
            if prepend:
                out_list.append(str(item).strip() + " " + in_string.strip())
            else:
                out_list.append(in_string.strip() + " " + str(item).strip())
        return out_list

def main():
    run_module()

if __name__ == ' __main__':
    main()

```

I have tried to test it via command line, but it yields this error:

```auto
ansible localhost -m company.update_roles.add_string -a "in_list=['one','two','three'] in_string=word" --playbook-dir=$PWD

# result:
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: Error loading plugin 'company.update_roles.add_string': No module named 'ansible_collections.company'
[ERROR]: Task failed: Cannot resolve 'company.update_roles.add_string' to an action or module. Does anyone here have an inkling of what I might be missing?

Task failed.
Origin: <adhoc 'company.update_roles.add_string' task>

{'action': 'company.update_roles.add_string', 'args': {'in_list': "['one','two','three']", 'in_string': 'word'}, [...]

<<< caused by >>>

Cannot resolve 'company.update_roles.add_string' to an action or module.
Origin: <CLI option '-m'>

company.update_roles.add_string

localhost | FAILED! => {
    "changed": false,
    "msg": "Task failed: Cannot resolve 'company.update_roles.add_string' to an action or module."
}

```

Which is pretty similar to the error I get when running ansible-lint:

```auto
$ ansible-lint
WARNING Unable to load module company.update_roles.add_string at roles/list_linux_updates/tasks/main.yml:7 for options validation
WARNING Invalid value (None)for resolved_fqcn attribute of company.update_roles.add_string module.
WARNING Listing 1 violation(s) that are fatal
# Rule Violation Summary
  1 syntax-check profile:min tags:core,unskippable
Failed: 1 failure(s), 0 warning(s) on 27 files.
syntax-check[unknown-module]: couldn't resolve module/action 'company.update_roles.add_string'. This often indicates a misspelling, missing collection, or incorrect module path.
roles/list_linux_updates/tasks/main.yml:7:11

```

I’m sure that there are plenty of things wrong with the module itself, but so long as I cannot test it, I won’t know and learn. I also have a bit of a feeling that this may not be the intended use of modules, or even the best way to tackle the issue at hand, but it’s the only one that I could think of that has proven somewhat fruitful. Nevertheless, I do want to know how I can use modules within the same collection.

---

<div class="post-metadata">

**Author:** ![kks](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/kks/32/10352_2.png) [@kks](https://forum.ansible.com/u/kks)\
**Post date:** [April 14, 2026, 2:05pm UTC](https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676/2 "2026-04-14T14:05:58Z")

</div>

You do not need custom plugin for that.

```yaml
---
- name: add prefix to a list of strings
  hosts: all
  gather_facts: false
  tasks:
    - name: add prefix to a list of strings
      ansible.builtin.assert:
        that:
          - ( list_of_strings | map('regex_replace', '^(.*)$', prefix ~ '\\1') ) == list_of_strings_replaced
      vars:
        prefix: "hello "
        list_of_strings:
          - one
          - two
          - three
        list_of_strings_replaced:
          - hello one
          - hello two
          - hello three

```

map run regex replace for each element in the list.

```yaml
'regex_replace', '^(.*)$', prefix ~ '\\1') 

```

```yaml
'^(.*)$' - all in the parenthesis means so called group, first parenthesis should be refered as group 1

```

```yaml
prefix ~ '\\1' - contatenate prefix variable with group 1, group 1 is effectively compelete sting - from beginning ^ to and end $ (regex simbols that mean beginning and the end respectively)

```

Let’s check if this works.

```bash
bash-5.2$ uv run --with ansible-core==2.19 ansible-playbook -i localhost, playbook.yml 
[WARNING]: Skipping plugin (/opt/homebrew/lib/python3.14/site-packages/ara/plugins/callback/ara_default.py), cannot load: No module named 'ara'

PLAY [add prefix to a list of strings] **********************************************************************************************************************************************************************************************************************

TASK [add prefix to a list of strings] **********************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "changed": false,
    "msg": "All assertions passed"
}

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

```

---

<div class="post-metadata">

**Author:** ![kks](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/kks/32/10352_2.png) [@kks](https://forum.ansible.com/u/kks)\
**Post date:** [April 14, 2026, 2:43pm UTC](https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676/3 "2026-04-14T14:43:17Z")

</div>

and example for postfix

```yaml
---
- name: add postfix to a list of strings
  hosts: all
  gather_facts: false
  tasks:
    - name: add postfix to a list of strings
      ansible.builtin.assert:
        that:
          - ( list_of_strings | map('regex_replace', '^(.*)$', '\\1' ~ postfix) ) == list_of_strings_replaced
      vars:
        postfix: " good bye"
        list_of_strings:
          - one
          - two
          - three
        list_of_strings_replaced:
          - one good bye
          - two good bye
          - three good bye

```

this is the main change

```yaml
'\\1' ~ postfix

```

```bash
bash-5.2$ uv run --with ansible-core==2.19 ansible-playbook -i localhost, playbook2.yml 
[WARNING]: Skipping plugin (/opt/homebrew/lib/python3.14/site-packages/ara/plugins/callback/ara_default.py), cannot load: No module named 'ara'

PLAY [add postfix to a list of strings] *********************************************************************************************************************************************************************************************************************

TASK [add postfix to a list of strings] *********************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "changed": false,
    "msg": "All assertions passed"
}

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

```

---

<div class="post-metadata">

**Author:** ![bvitnik](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/bvitnik/32/4132_2.png) [@bvitnik](https://forum.ansible.com/u/bvitnik)\
**Post date:** [April 14, 2026, 5:51pm UTC](https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676/4 "2026-04-14T17:51:22Z")

</div>

Two things that come to my mind:

1. ` __init__.py` is messing with your module loading so Ansible does not pick it up properly.
2. Your collection is not installed to `COLLECTIONS_PATH` which is usually `~/.ansible/collections`

You did not show your playbook so we don’t know how you test your role.

---

<div class="post-metadata">

**Author:** ![felixfontein](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/felixfontein/32/7530_2.png) [@felixfontein](https://forum.ansible.com/u/felixfontein)\
**Post date:** [April 14, 2026, 7:00pm UTC](https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676/5 "2026-04-14T19:00:07Z")

</div>

` __init__.py` shouldn’t be a problem (unless it contains something funky). Not being in the collection search path sounds more likely.

Also ansible-lint not finding the module might be caused by yet another thing, and not be related to why it doesn’t work in a regular playbook. Since that isn’t mentioned: _does_ the role work from a regular playbook?

---

<div class="post-metadata">

**Author:** ![ab203972](https://avatars.discourse-cdn.com/v4/letter/a/ea5d25/32.png) [@ab203972](https://forum.ansible.com/u/ab203972)\
**Post date:** [April 15, 2026, 8:31am UTC](https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676/6 "2026-04-15T08:31:04Z")

</div>

The rexeg solution looks pretty sleek too, and I may adopt it if the module doesn’t pan out, so thank you for that [kks](https://forum.ansible.com/u/kks)! Still, I do want to get more knowledge on modules, so I will look/ask further for solutions on that front for now.

` __init.py__ ` is empty. I tried adding it to see if that helped with the role realizing that should be a valid module, but sadly to no avail.

Testing the role in a regular playbook does work - or rather, it doesn’t break because it cannot find the module (the module itself needs work). So if necessary, I can run the module in the regular playbook and parse the prepared list to the role, but it would be cleaner if it were handled within the role.

Speaking of which, it’s true that I forgot to include the actual playbook, so here it is below:

```yaml
---
- name: Update GitLab machines
  hosts: gitlab
  become: true
  vars:
    ci_project_dir: "{{ lookup('ansible.builtin.env', 'CI_PROJECT_DIR') }}"
    exclude_list: ["gitlab-runner", "gitlab-runner-helper-images"] # server packages are not included yet!
  roles:
    - role: company.update_roles.list_linux_updates
      list_linux_updates_packages_exclude: "{{ exclude_list }}"
    - role: company.update_roles.update_linux_packages
      update_linux_packages_exclude: "{{ exclude_list }}"
  post_tasks:
    - name: Save available package updates
      block:
        - name: List available package updates
          ansible.builtin.debug:
            var: list_linux_updates_updatable_packages.stdout_lines

        - name: Write available package updates to file
          ansible.builtin.copy:
            dest: "{{ ci_project_dir }}/{{ inventory_hostname }}.packages.txt"
            content: "{{ list_linux_updates_updatable_packages.stdout }}"
            owner: root
            group: root
            mode: '0644'
          delegate_to: localhost

    - name: Verify no pending updates
      block:
        - name: Check for available package updates post-update
          ansible.builtin.import_role:
            name: company.update_roles.list_linux_updates
          vars:
            list_linux_updates_packages_exclude: "{{ exclude_list }}"
          register: post_update_check
          failed_when: post_update_check.rc not in [0, 100]
          changed_when: false

        - name: Fail if updates other than GitLab are available
          ansible.builtin.fail:
            msg: "ERROR: There are pending updates on this system!:\n{{ post_update_check.stdout_lines }}"
          when: post_update_check.rc != 0 and jinja - filters.length(post_update_check.stdout_lines) > 0

        - name: Signal a reboot may be necessary due to a kernel update
          ansible.builtin.debug:
            msg: "A kernel update is available. A reboot may be necessary to complete installation."
          when: "'Security: kernel-core-' in post_update_check.stdout"

```

If I do want the module to work within the role in the same collection, do I need to somehow import the collection into itself? Or is it meant to stay separate?

---

<div class="post-metadata">

**Author:** ![bvitnik](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/bvitnik/32/4132_2.png) [@bvitnik](https://forum.ansible.com/u/bvitnik)\
**Post date:** [April 15, 2026, 8:46am UTC](https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676/7 "2026-04-15T08:46:34Z")

</div>

The only thing required is for your collection (roles + modules) to be installed to a directory pointed by `COLLECTIONS_PATH` which is usually `~/.ansible/collections`. It can alternatively be installed to a directory called `collections` next to your playbook and it will be picked by the `ansible-playbook` but not by `ansible` and `ansible-lint`. After that, your module can be used in the role of the same collection, some other collection or directly within your playbook. There is nothing additional to do to make the module available to the role of the same collection. It should “just work”.

---

<div class="post-metadata">

**Author:** ![dbrennand](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/dbrennand/32/7830_2.png) [@dbrennand](https://forum.ansible.com/u/dbrennand)\
**Post date:** [April 16, 2026, 1:36pm UTC](https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676/8 "2026-04-16T13:36:33Z")

</div>

Hi @ab203972

Regarding `ansible-lint` - What version of `ansible-lint` are you using?

```auto
ansible-lint --version

```

`ansible-lint` should detect and install your collection into a `.ansible` directory which `ansible-lint` uses at runtime, with some verbosity `-v` you should see something like:

```auto
INFO Running from /home/dabrenna/github.com/community.beszel : ansible-galaxy collection install -vvv --force /home/dabrenna/github.com/community.beszel

```

Do you see this?

---

<div class="post-metadata">

**Author:** ![shertel](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/shertel/32/1241_2.png) [@shertel](https://forum.ansible.com/u/shertel)\
**Post date:** [April 16, 2026, 3:10pm UTC](https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676/9 "2026-04-16T15:10:56Z")

</div>

If you share the collection path, output from `ansible-config dump --only-changed`, and the playbook path, it would clarify what’s happening. You can run `ansible-galaxy collection list` to see collections able to be found by `ansible` adhoc.

---

<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:** [April 16, 2026, 6:16pm UTC](https://forum.ansible.com/t/role-in-collection-cannot-find-module-in-the-same-collection/45676/10 "2026-04-16T18:16:58Z")

</div>

> [@ab203972](#):
>
> Still, I do want to get more knowledge on modules, so I will look/ask further for solutions on that front for now.

Admirable attitude. I fully support your efforts. 👍

To add onto @kks’s point (that this can be done with stuff Ansible already provided), if you do want to use Python code in a plugin, for this particular case it would be more appropriate to implement as a _filter_ rather than a _module_. Since you’re just manipulating text, there’s no need to ship code to each target to be executed there, which is what a module does. A filter runs on the controller, so it’s much lighter. Of course, the invocation syntax for filters is radically different from that for modules, etc.

My first plugin was a module, and I thought, “This is great! I’m going to write dozens of modules.” That first module was also my last module. All my plugins since then have been filters or tests.

So, good to be comfortable writing various types of plugins. In this case though, it’s probably a better filter than a module.
