Ansible meta Module — Control Play Execution Flow
Introduction
ansible.builtin.meta provides special actions that control Ansible's execution engine — flushing handlers mid-play, ending execution early, clearing cached facts, refreshing inventory, and resetting connections. These aren't normal tasks; they're directives to the Ansible runtime itself.
Available Actions
| Action | Description |
|---|---|
flush_handlers | Run all pending handlers immediately |
end_play | End the current play (skip remaining tasks) |
end_host | Remove current host from play (continue others) |
end_batch | End current serial batch |
clear_facts | Clear cached facts for current host |
clear_host_errors | Clear failure state for a host |
refresh_inventory | Re-read inventory (dynamic inventory) |
noop | Do nothing (placeholder) |
reset_connection | Close and reopen SSH connection |
flush_handlers
Handlers normally run at the end of a play. flush_handlers runs them immediately:
---
- name: Deploy with mid-play handler flush
hosts: webservers
tasks:
- name: Install Nginx
ansible.builtin.package:
name: nginx
state: present
notify: Start Nginx
- name: Deploy SSL certificate
ansible.builtin.copy:
src: cert.pem
dest: /etc/ssl/certs/app.pem
notify: Reload Nginx
# Flush now — Nginx must be running before we test it
- name: Ensure Nginx is running before health check
ansible.builtin.meta: flush_handlers
- name: Verify Nginx is responding
ansible.builtin.uri:
url: "https://{{ inventory_hostname }}/index.html"
validate_certs: false
status_code: 200
handlers:
- name: Start Nginx
ansible.builtin.systemd:
name: nginx
state: started
- name: Reload Nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
Why flush_handlers?
Without it, this fails:
- name: Configure service
ansible.builtin.template:
src: config.j2
dest: /etc/myapp/config.yml
notify: Restart myapp
# ❌ FAILS — myapp hasn't restarted yet (handler pending)
- name: Verify service is healthy
ansible.builtin.uri:
url: "http://localhost:8080/health"
# ✅ FIX — flush handlers before checking
- ansible.builtin.meta: flush_handlers
- name: Verify service is healthy
ansible.builtin.uri:
url: "http://localhost:8080/health"
end_play
Stop the entire play (all hosts):
- name: Check if already deployed
ansible.builtin.stat:
path: /opt/app/DEPLOYED_v2
register: deployed
- name: Skip deployment if already done
ansible.builtin.meta: end_play
when: deployed.stat.exists
# All remaining tasks in this play are skipped for ALL hosts
end_host
Remove the current host from the play (other hosts continue):
- name: Check if host needs updates
ansible.builtin.command:
cmd: needs-update --check
register: update_check
changed_when: false
failed_when: false
- name: Skip host if no updates needed
ansible.builtin.meta: end_host
when: update_check.rc != 0
- name: Apply updates (only on hosts that need them)
ansible.builtin.package:
name: "*"
state: latest
clear_facts
Clear cached facts for the host and re-gather:
- name: Reconfigure network interface
ansible.builtin.template:
src: netplan.yaml.j2
dest: /etc/netplan/01-config.yaml
- name: Apply network changes
ansible.builtin.command:
cmd: netplan apply
- name: Clear cached network facts
ansible.builtin.meta: clear_facts
- name: Re-gather facts with new network config
ansible.builtin.setup:
- name: Show new IP address
ansible.builtin.debug:
msg: "New IP: {{ ansible_default_ipv4.address }}"
reset_connection
Close and reopen the SSH connection:
- name: Change SSH port
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?Port'
line: 'Port 2222'
- name: Restart SSH
ansible.builtin.systemd:
name: sshd
state: restarted
- name: Update connection port
ansible.builtin.set_fact:
ansible_port: 2222
- name: Reset SSH connection
ansible.builtin.meta: reset_connection
- name: Verify new connection works
ansible.builtin.ping:
refresh_inventory
Re-read dynamic inventory mid-play:
- name: Create new EC2 instances
amazon.aws.ec2_instance:
name: "web-{{ item }}"
instance_type: t3.micro
state: running
loop: [1, 2, 3]
delegate_to: localhost
- name: Wait for instances to register
ansible.builtin.pause:
seconds: 30
- name: Refresh inventory to pick up new instances
ansible.builtin.meta: refresh_inventory
- name: Configure new instances
ansible.builtin.debug:
msg: "New host: {{ item }}"
loop: "{{ groups['tag_Name_web'] | default([]) }}"
clear_host_errors
Allow a failed host to continue:
- name: Task that might fail
ansible.builtin.command:
cmd: /opt/risky-operation.sh
ignore_errors: true
- name: Clear the error state
ansible.builtin.meta: clear_host_errors
- name: Continue with clean state
ansible.builtin.debug:
msg: "Host is back in the game"
Troubleshooting
| Issue | Solution |
|---|---|
| Handlers not running when expected | Add meta: flush_handlers before tasks that depend on handler results |
end_play stops all hosts | Use end_host to skip only the current host |
| Facts stale after network change | Use meta: clear_facts then ansible.builtin.setup |
| SSH connection broken after port change | Use meta: reset_connection after updating ansible_port |
| Dynamic inventory outdated | Use meta: refresh_inventory after creating/destroying instances |
Best Practices
flush_handlersbefore verification — ensure services are restarted before health checks- Use
end_hostoverend_play— skip individual hosts, not the entire play clear_factsafter system changes — network, hostname, or package changesreset_connectionafter SSH changes — port, key, or user modifications- Don't overuse
meta— most playbooks don't need it; it's for edge cases
Conclusion
ansible.builtin.meta controls Ansible's execution engine directly. flush_handlers is by far the most common use — ensuring services restart before verification tasks. The other actions handle edge cases: early termination, fact refresh after system changes, and connection resets after SSH modifications. Most playbooks only need flush_handlers; the rest are there when you need them.