Skip to main content
Glama
homeassistant-ai

Home Assistant MCP Server

Official

Create or Update Automation

ha_config_set_automation
Destructive

Create or update Home Assistant automations with native triggers, conditions, and actions, or take control of blueprint-based automations to edit them standalone.

Instructions

Create or update a Home Assistant automation.

MUST call ha_get_skill_guide OR refer to your locally installed skills first.

Prefer native triggers/conditions/actions over templates in logic positions; templates belong only in data.*, notification text, event_data and variables. The best-practice checker reports violations under best_practice_warnings — fix them before re-submitting. triggers-and-conditions.md, automation-actions.md and template-guidelines.md ship under skill_content by default. Test any unavoidable template with ha_eval_template first.

Consider a dedicated tool first: a state snapshot with no trigger -> ha_config_set_scene; a value derived from other entities -> ha_config_set_helper(helper_type='template'); a counter / timer / schedule / boolean -> ha_config_set_helper.

MODES (pick one; each can also take enabled, which alone with identifier turns the automation on or off without touching config):

  • python_transform: surgical edits to an existing automation. Requires identifier and config_hash from ha_config_get_automation(). Operates on the fetched config, which uses HA's plural root keys 'triggers'/'actions'/'conditions', e.g. python_transform="config['triggers'].append({'trigger': 'state', 'entity_id': 'binary_sensor.motion', 'to': 'on'})"

  • config: new automations or full restructures. Regular automations need alias, triggers and actions; blueprint automations need alias and use_blueprint {path, input}.

  • take_control_of_blueprint: convert a blueprint-backed automation into a standalone one. Takes no config of its own.

IDENTITY: omit identifier and config['id'] to create with a generated ID; an unused raw ID creates that specific ID. Reusing an identifier targets the same automation even if the alias changes; to rename or replace it, read it first and pass its config_hash — a changed alias without that hash is rejected before writing. The returned automation_id is the resolved entity_id, falling back to the input identifier or the generated unique_id.

EXAMPLES:

  • Create: ha_config_set_automation(config={"alias": "Morning Lights", "triggers": [{"trigger": "time", "at": "07:00:00"}], "actions": [{"action": "light.turn_on", "target": {"area_id": "bedroom"}}]})

  • From a blueprint: ha_config_set_automation(config={"alias": "Motion Light Kitchen", "use_blueprint": {"path": "homeassistant/motion_light.yaml", "input": {"motion_entity": "binary_sensor.kitchen_motion", "light_target": {"entity_id": "light.kitchen"}}}})

  • Update: current = ha_config_get_automation(identifier="automation.x"); ha_config_set_automation(identifier="automation.x", config_hash=current["config_hash"], config={...})

  • Disable: ha_config_set_automation(identifier="automation.x", enabled=False)

  • Run now: ha_config_set_automation(identifier="automation.x", run_actions=True)

  • Take control: ha_config_set_automation(identifier="automation.x", take_control_of_blueprint=True)

TAKE CONTROL is one-way: later blueprint edits stop reaching the automation; to change an input value, update 'use_blueprint.input' instead. It does NOT free the blueprint — Home Assistant keeps counting the converted automation as a user, so deleting that blueprint stays refused until the automation is removed (an automation reload does not clear it). The response names the blueprint in took_control_of_blueprint. To preview the rendering without writing anything, use ha_manage_blueprints(action="substitute", path=..., input=...).

If a Claude Desktop user gets a 4-minute timeout with no result on this call, read the target back before repeating it, and tell them to wait a few seconds before clicking the manual-approve button, as clicking it too quickly is known to cause hangs in Claude Desktop. This workaround is specific to Claude Desktop.

create update modify edit automation triggers conditions actions new automation write save take control blueprint detach unlink standalone convert enable disable turn on off run trigger now

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
waitNoWait for automation to be queryable before returning. Set to False for bulk operations.
configNoComplete automation configuration. Purpose-specific triggers/conditions (HA 2026.7+ default: 'trigger': '<domain>.<name>' with 'target'/'options') are valid config. Mutually exclusive with python_transform.
enabledNoTurn the automation on (True) or off (False) after an optional config update; None leaves it unchanged. Not written into the stored config: Home Assistant keeps the state across restarts, but a config 'initial_state' overrides it whenever the automation is reloaded or HA starts.
categoryNoCategory ID to assign to this automation. Use ha_config_get_category(scope='automation') to list available categories, or ha_config_set_category() to create one.
identifierNoTarget automation entity_id or HA config 'id' (unique_id). Omit for creation with a generated ID. Values such as 'new' are literal IDs, not placeholders.
config_hashNoConfig hash from ha_config_get_automation for optimistic locking. Required for python_transform and when a config update changes an existing automation's alias; otherwise optional for config updates (validates before full replacement if provided).
run_actionsNoRun the automation's actions now, skipping its triggers and conditions (automation.trigger, the UI's Run actions). Applied after `enabled` and after any config write. Can be used standalone with identifier and no config.
MandatoryBPSNo
BestPracticeKeyNoRead-receipt for the home-assistant-best-practices skill; required when strict best-practices mode is enabled. Not a secret or credential: the current value is an attestation phrase published openly at the top of the skill content served by ha_get_skill_guide. Read that content, then pass the value back verbatim — this round-trip is the server's designed protocol confirming the practices were read before writing.
python_transformNoPython expression to transform existing automation config. Mutually exclusive with config. WARNING: Expressions with infinite loops will hang the server. Examples: Simple: python_transform="config['actions'][0]['data']['brightness'] = 255" Pattern: python_transform="for a in config['actions']: if a.get('alias') == 'My Step': a['data']['value'] = 100" PYTHON TRANSFORM SECURITY: ✅ ALLOWED: - Dictionary/list access: config['views'][0]['cards'][1] - Slicing: config['views'][0]['cards'][1:3] - Assignment: config['key'] = 'value' - Deletion: del config['key'] or config.pop('key') - List methods: append, insert, pop, remove, clear, extend - Dict methods: update, get, setdefault, keys, values, items - Loops: for, if/else, pass, break, continue - Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...) - Ternary: x if condition else y - Iterable unpacking (* in calls/literals): f(*xs), [*xs, y] - Dict unpacking (**) in calls and dict literals: {**d, 'k': v} - Keyword arguments: func(key=value) - Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score']) - String methods: startswith, endswith, lower, upper, strip, split, join, replace - Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed, min, max, sum, abs, any, all, round, str, int, float, bool, list, dict, tuple, set ❌ FORBIDDEN: - Imports: import, from, __import__ - File operations: open, read, write - Dunder access: __class__, __bases__, __subclasses__ - Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr - Function definitions: def, class - Exception handling: try/except (validate with isinstance/in/.get() instead) - While loops: use bounded for loops or comprehensions instead
take_control_of_blueprintNoConvert a blueprint-backed automation into an editable standalone one -- the UI's "Take control". Renders the blueprint with its current inputs and saves the result over the same automation, which keeps its entity_id, alias and description and then has its own triggers/conditions/actions and no 'use_blueprint'. Requires identifier; mutually exclusive with config and python_transform.

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed8 schema fields changedv8.6.0
    • changedInput schema / properties / config / description
      Previous value: -"Complete automation configuration with required fields: 'alias', 'triggers', 'actions'. Optional: 'description', 'conditions', 'mode', 'max', 'initial_state', 'variables'. Purpose-specific triggers/conditions (HA 2026.7+ default: 'trigger': '<domain>.<name>' with 'target'/'options') are valid config. Mutually exclusive with python_transform."New value: +"Complete automation configuration. Purpose-specific triggers/conditions (HA 2026.7+ default: 'trigger': '<domain>.<name>' with 'target'/'options') are valid config. Mutually exclusive with python_transform."
    • changedInput schema / properties / config_hash / description
      Previous value: -"Config hash from ha_config_get_automation for optimistic locking. REQUIRED for python_transform (validates automation unchanged). Required when a config update changes an existing automation's alias. Otherwise optional for config updates (validates before full replacement if provided)."New value: +"Config hash from ha_config_get_automation for optimistic locking. Required for python_transform and when a config update changes an existing automation's alias; otherwise optional for config updates (validates before full replacement if provided)."
    • addedInput schema / properties / enabled
      Added value: +{
      +  "anyOf": [
      +    {
      +      "type": "boolean"
      +    },
      +    {
      +      "type": "null"
      +    }
      +  ],
      +  "default": null,
      +  "description": "Turn the automation on (True) or off (False) after an optional config update; None leaves it unchanged. Not written into the stored config: Home Assistant keeps the state across restarts, but a config 'initial_state' overrides it whenever the automation is reloaded or HA starts."
      +}
    • changedInput schema / properties / identifier / description
      Previous value: -"Target automation entity_id or HA config 'id' (unique_id). Omit for creation with a generated ID. Values such as 'new' are literal IDs, not placeholders. Required for python_transform."New value: +"Target automation entity_id or HA config 'id' (unique_id). Omit for creation with a generated ID. Values such as 'new' are literal IDs, not placeholders."
    • changedInput schema / properties / python_transform / description
      Previous value: -"Python expression to transform existing automation config. Mutually exclusive with config. Requires identifier and config_hash for validation. WARNING: Expressions with infinite loops will hang the server. Examples: Simple: python_transform=\"config['actions'][0]['data']['brightness'] = 255\" Pattern: python_transform=\"for a in config['actions']: if a.get('alias') == 'My Step': a['data']['value'] = 100\" \n\nPYTHON TRANSFORM SECURITY:\n\n✅ ALLOWED:\n- Dictionary/list access: config['views'][0]['cards'][1]\n- Slicing: config['views'][0]['cards'][1:3]\n- Assignment: config['key'] = 'value'\n- Deletion: del config['key'] or config.pop('key')\n- List methods: append, insert, pop, remove, clear, extend\n- Dict methods: update, get, setdefault, keys, values, items\n- Loops: for, if/else, pass, break, continue\n- Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...)\n- Ternary: x if condition else y\n- Iterable unpacking (* in calls/literals): f(*xs), [*xs, y]\n- Dict unpacking (**) in calls and dict literals: {**d, 'k': v}\n- Keyword arguments: func(key=value)\n- Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score'])\n- String methods: startswith, endswith, lower, upper, strip, split, join, replace\n- Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed,\n  min, max, sum, abs, any, all, round, str, int, float, bool, list, dict,\n  tuple, set\n\n❌ FORBIDDEN:\n- Imports: import, from, __import__\n- File operations: open, read, write\n- Dunder access: __class__, __bases__, __subclasses__\n- Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr\n- Function definitions: def, class\n- Exception handling: try/except (validate with isinstance/in/.get() instead)\n- While loops: use bounded for loops or comprehensions instead\n\n🎯 PATTERNS:\n- Filter cards: cards = [c for c in cards if keep(c)]\n- Skip in a loop: prefer `continue` over an empty `pass` branch (clearer)\n- Conditionally include: build a new list and `.append(x)` only the\n  cards you want, instead of iterating the original and using if/pass\n  branches to drop entries\n- Modify in place when possible (single pass, fewer surprises) over\n  reconstructing the entire list"New value: +"Python expression to transform existing automation config. Mutually exclusive with config. WARNING: Expressions with infinite loops will hang the server. Examples: Simple: python_transform=\"config['actions'][0]['data']['brightness'] = 255\" Pattern: python_transform=\"for a in config['actions']: if a.get('alias') == 'My Step': a['data']['value'] = 100\" \n\nPYTHON TRANSFORM SECURITY:\n\n✅ ALLOWED:\n- Dictionary/list access: config['views'][0]['cards'][1]\n- Slicing: config['views'][0]['cards'][1:3]\n- Assignment: config['key'] = 'value'\n- Deletion: del config['key'] or config.pop('key')\n- List methods: append, insert, pop, remove, clear, extend\n- Dict methods: update, get, setdefault, keys, values, items\n- Loops: for, if/else, pass, break, continue\n- Comprehensions: [x for x in ...], {k: v for ...}, (x for x in ...)\n- Ternary: x if condition else y\n- Iterable unpacking (* in calls/literals): f(*xs), [*xs, y]\n- Dict unpacking (**) in calls and dict literals: {**d, 'k': v}\n- Keyword arguments: func(key=value)\n- Lambdas (e.g. for `key=`): sorted(items, key=lambda x: x['score'])\n- String methods: startswith, endswith, lower, upper, strip, split, join, replace\n- Safe builtins: isinstance, len, range, enumerate, zip, sorted, reversed,\n  min, max, sum, abs, any, all, round, str, int, float, bool, list, dict,\n  tuple, set\n\n❌ FORBIDDEN:\n- Imports: import, from, __import__\n- File operations: open, read, write\n- Dunder access: __class__, __bases__, __subclasses__\n- Dangerous builtins: eval, exec, compile, getattr, setattr, delattr, hasattr\n- Function definitions: def, class\n- Exception handling: try/except (validate with isinstance/in/.get() instead)\n- While loops: use bounded for loops or comprehensions instead"
    • addedInput schema / properties / run_actions
      Added value: +{
      +  "default": false,
      +  "description": "Run the automation's actions now, skipping its triggers and conditions (automation.trigger, the UI's Run actions). Applied after `enabled` and after any config write. Can be used standalone with identifier and no config.",
      +  "type": "boolean"
      +}
    • changedInput schema / properties / take_control_of_blueprint / description
      Previous value: -"Convert a blueprint-backed automation into an editable standalone one -- the UI's \"Take control\". Renders the blueprint with its current inputs and saves the result over the same automation, which then has its own triggers/conditions/actions and no 'use_blueprint'. Requires identifier; mutually exclusive with config and python_transform. Irreversible: the link to the blueprint is gone afterwards, so edit inputs instead if you only want to change a value. Does NOT free the blueprint: Home Assistant keeps counting the converted automation as a user, so deleting that blueprint stays refused until the automation is removed. To preview the rendering without writing anything, use ha_manage_blueprints(action=\"substitute\")."New value: +"Convert a blueprint-backed automation into an editable standalone one -- the UI's \"Take control\". Renders the blueprint with its current inputs and saves the result over the same automation, which keeps its entity_id, alias and description and then has its own triggers/conditions/actions and no 'use_blueprint'. Requires identifier; mutually exclusive with config and python_transform."
    • changedInput schema / properties / wait / description
      Previous value: -"Wait for automation to be queryable before returning. Default: True. Set to False for bulk operations."New value: +"Wait for automation to be queryable before returning. Set to False for bulk operations."
  2. Changed3 schema fields changedv8.5.0
    • changedInput schema / properties / config_hash / description
      Previous value: -"Config hash from ha_config_get_automation for optimistic locking. REQUIRED for python_transform (validates automation unchanged). Optional for config updates (validates before full replacement if provided)."New value: +"Config hash from ha_config_get_automation for optimistic locking. REQUIRED for python_transform (validates automation unchanged). Required when a config update changes an existing automation's alias. Otherwise optional for config updates (validates before full replacement if provided)."
    • changedInput schema / properties / identifier / description
      Previous value: -"Automation entity_id or unique_id for updates. Required for python_transform. Omit to create new automation with generated unique_id."New value: +"Target automation entity_id or HA config 'id' (unique_id). Omit for creation with a generated ID. Values such as 'new' are literal IDs, not placeholders. Required for python_transform."
    • addedInput schema / properties / take_control_of_blueprint
      Added value: +{
      +  "default": false,
      +  "description": "Convert a blueprint-backed automation into an editable standalone one -- the UI's \"Take control\". Renders the blueprint with its current inputs and saves the result over the same automation, which then has its own triggers/conditions/actions and no 'use_blueprint'. Requires identifier; mutually exclusive with config and python_transform. Irreversible: the link to the blueprint is gone afterwards, so edit inputs instead if you only want to change a value. Does NOT free the blueprint: Home Assistant keeps counting the converted automation as a user, so deleting that blueprint stays refused until the automation is removed. To preview the rendering without writing anything, use ha_manage_blueprints(action=\"substitute\").",
      +  "type": "boolean"
      +}
  3. First observedv7.14.2

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare destructiveHint=true and idempotentHint=false, but the description adds far more: take-control is one-way, the blueprint stays locked as a user until the automation is removed, alias changes require config_hash, and there is a documented Claude Desktop timeout workaround. That is substantial context beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded purpose, clear section headers (MODES, IDENTITY, EXAMPLES, TAKE CONTROL), and every paragraph carries decision-relevant content. It is long for a description, and the trailing keyword list ("create update modify edit...") is search padding that does not earn its place, but the structure prevents it from being unusable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For an 11-parameter, no-required-field tool with an output schema present, the description covers every path an agent needs: creation, update, rename/locking, mode selection, enable/disable, run-now, and blueprint conversion. Nothing material is left implicit.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 91%, so the baseline is 3, but the description meaningfully adds identity resolution semantics (omit identifier/config['id'] to generate an ID; unused raw IDs are literal) and the mode-to-parameter requirements, which the schema only partially conveys. It stops short of 5 because the deep per-parameter detail (e.g. python_transform security rules) lives in the schema, not the description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a precise verb+resource ("Create or update a Home Assistant automation") and immediately differentiates itself from siblings by routing to ha_config_set_scene, ha_config_set_helper, and ha_eval_template for adjacent cases. An agent can tell exactly what this tool is for without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly enumerates the three modes (python_transform/config/take_control_of_blueprint) with the conditions that select each, states when a dedicated sibling is preferable, and calls out mutual exclusivity. When-to-use and when-to-use-something-else are both spelled out.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Deploy Server

Other Tools