Home Assistant skill for Codex
Automations that fire once, at the right time. Codex writes Home Assistant YAML in the current triggers, conditions and actions syntax, gives every automation a stable id and a deliberate mode, and guards against the unavailable-state transitions that make automations fire after restarts. It validates config before reloading, debugs through traces, and scaffolds async custom integrations with config flows and a coordinator.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- home-assistant Installs to ~/.agents/skills/home-assistant/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install home-assistant The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project home-assistant Installs to .agents/skills/home-assistant/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/home-assistant.zipunzip -o home-assistant.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) home-assistant PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $home-assistant, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes Home Assistant automations, scripts, template sensors, blueprints and packages in current YAML syntax (triggers/conditions/actions, action instead of service), picks automation modes, validates config before restart, and scaffolds custom integrations with config flows and DataUpdateCoordinator. Use when an automation does not fire or fires twice, converting old YAML that uses platform and service keys, building template sensors, organising configuration.yaml into packages, or writing a custom_components integration for a local device or API.
Use it when
- An automation does not fire, fires twice or fires after restarts
- Converting older YAML to the triggers/actions and action syntax
- Building template sensors, blueprints or a packages layout
- Writing a custom_components integration for a local device
Use something else when
- The problem is in the MQTT broker, topics or ACLs underneath Home Assistant: use mqtt-iot
- You are writing the firmware of an ESP32 device itself: use esp32-firmware
How to check the result
Run the config check, reload only the changed domains, and read the automation trace of a real or manually triggered run to confirm the expected path.
The mistake it prevents
A state trigger with to: "on" and no from: also fires on unavailable to on after every restart or Wi-Fi drop.
Done well means: automations use current syntax with a stable id, the right mode, and
conditions that make them safe to fire twice; configuration passes the checker before any
restart; and custom integrations are async, configured through the UI and never block the
event loop.
Inspect first
- Read
AGENTS.md, then the config tree:configuration.yaml,automations.yaml,scripts.yaml,packages/,custom_components/,secrets.yaml(do not print its values). - Find the Home Assistant version (
.HA_VERSIONin the config folder). Syntax below is for 2024.10 and later; older installs need the legacy keys. - Ask for the real entity IDs (Developer tools > States, or
GET /api/states). Never inventlight.living_room; a wrong entity ID fails silently at runtime.
Current syntax
| Legacy | Current (2024.8 / 2024.10+) |
|---|---|
trigger: list with platform: state | triggers: list with trigger: state |
condition: | conditions: |
action: list with service: light.turn_on | actions: list with action: light.turn_on |
data_template:, service_template: | Templates work directly in data: and action: |
Old keys still load, but write new code in current syntax and convert a file at a time.
Automation pattern
- id: hallway_motion_light # stable id: enables UI editing and traces
alias: Hallway light on motion after dark
mode: restart # new motion restarts the off-timer
triggers:
- trigger: state
entity_id: binary_sensor.hallway_motion
to: "on"
conditions:
- condition: state
entity_id: sun.sun
state: below_horizon
- condition: state
entity_id: input_boolean.guest_mode
state: "off"
actions:
- action: light.turn_on
target:
entity_id: light.hallway
data:
brightness_pct: "{{ 30 if now().hour >= 23 or now().hour < 6 else 80 }}"
- wait_for_trigger:
- trigger: state
entity_id: binary_sensor.hallway_motion
to: "off"
for: "00:02:00"
timeout: "00:30:00"
- action: light.turn_off
target:
entity_id: light.hallway
Choosing the mode
| Mode | Use when |
|---|---|
single (default) | One run at a time; extra triggers are dropped with a warning |
restart | Motion lights, debounced reactions: latest trigger wins |
queued | Every trigger must run in order (counting, notifications per event) |
parallel | Independent runs per trigger, e.g. per-entity reactions with trigger.entity_id |
Triggers that behave
to: "on"withoutfrom:also fires onunavailable -> onafter a restart or Wi-Fi blip. Addfrom: "off"when that matters, or anot_from: ["unavailable", "unknown"].- State values are strings: quote
"on","off", numbers instate:conditions. numeric_statewithabove/belowfires only when crossing the threshold, not while staying above it.- Use
for:on triggers to debounce, notdelayin actions. - Add
triggerIDs (id: arrived) and branch withchooseinstead of writing near-duplicate automations. - Time-based logic:
trigger: timewith aninput_datetimehelper so users can change it in the UI.
Template sensors
template:
- sensor:
- name: Average upstairs temperature
unique_id: avg_upstairs_temp
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
availability: >
{{ has_value('sensor.bedroom_temp') and has_value('sensor.office_temp') }}
state: >
{{ ((states('sensor.bedroom_temp') | float + states('sensor.office_temp') | float) / 2)
| round(1) }}
- Always provide
availability(orfloat(default)) so a missing sensor shows unavailable instead of crashing the template or recording a false zero. unique_idmakes the entity editable in the UI;state_classenables long-term stats.- Test templates in Developer tools > Template before saving.
Organising config
# configuration.yaml
homeassistant:
packages: !include_dir_named packages # packages/heating.yaml, packages/security.yaml
automation: !include automations.yaml # UI-managed file; keep it
- One package per feature holds its helpers, template sensors, scripts and automations.
- Credentials go in
secrets.yamlvia!secret; it must stay out of git. Add it to.gitignorebefore the first commit. - Reusable patterns become blueprints in
blueprints/automation/<author>/<name>.yamlwith!inputselectors.
Validate before restarting
ha core check # Home Assistant OS / Supervised
docker exec homeassistant python -m homeassistant --script check_config -c /config # container
Then reload only what changed (Developer tools > YAML: automations, scripts, templates) instead of restarting. Ask before restarting Home Assistant on a live home: locks, alarms and climate automations stop during the restart.
Debugging an automation
- Open its trace (Settings > Automations > the automation > Traces): shows which trigger
fired, which condition failed and every action’s result. Needs the
id. - No trace at all: the trigger never fired. Compare the trigger with the entity’s actual state history; check for typos in entity IDs.
- Check the log for
Already running(modesingle) and template errors. - Fires twice: two automations on the same trigger, a template trigger flapping, or
unavailabletransitions; look at the state history around the event.
Custom integration skeleton
custom_components/acme_meter/
├── __init__.py # async_setup_entry / async_unload_entry
├── manifest.json
├── config_flow.py # UI setup; validates host and credentials
├── coordinator.py # DataUpdateCoordinator polling the device
├── sensor.py # CoordinatorEntity subclasses with unique_id
└── strings.json # config flow text (plus translations/en.json)
{
"domain": "acme_meter",
"name": "Acme Meter",
"version": "0.1.0",
"codeowners": ["@your-handle"],
"config_flow": true,
"documentation": "https://github.com/your-org/acme-meter",
"iot_class": "local_polling",
"requirements": ["acme-meter-client==1.4.2"]
}
- Everything is async. Blocking I/O (requests, serial, sleep) on the event loop freezes the
whole house; use an async client or
hass.async_add_executor_job. - One
DataUpdateCoordinatorper device fetches once and all entities read from it; raiseUpdateFailedon errors so entities go unavailable rather than stale. - Raise
ConfigEntryNotReadyin setup when the device is offline; Home Assistant retries. - Give every entity a
unique_idderived from the device serial, not the IP address. - Test with
pytest-homeassistant-custom-component; installing it needs network access.
Verification
- Config check passes; only the changed domains reloaded
- Trace of a real (or manually triggered) run shows the expected path
- Restart test: no spurious firing from
unavailabletransitions - Template sensors show unavailable, not 0, when a source sensor is offline
- Custom integration: no “Detected blocking call” warnings in the log
Report
**HA version:** 2025.9.2 (container)
**Changed:** packages/lighting.yaml (3 automations converted to triggers/actions syntax,
mode restart, from: "off" guard), template sensor with availability
**Checks:** check_config clean; automations reloaded; trace shows condition sun below_horizon pass
**Not done:** no restart performed; alarm automations untouched What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Home Assistant"
short_description: "Automations that fire once, at the right time"
default_prompt: "$home-assistant My hallway motion automation turns the light on after every Home Assistant restart, fix it" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.