Implementing Automations
ESPHome automations consist of three building blocks:
- Triggers fire when something happens (state change, button press, etc.)
- Actions do something (set a value, call a service, etc.)
- Conditions check whether something is true (is the component on? is the value above a threshold?)
This page covers how to implement all three in a component. For a minimal working example, see the empty_automation starter component.
NOTE
Mark primitives final
The Action, Trigger, and Condition subclasses shown below are leaf classes — nothing in ESPHome derives from them — so they are marked final. See Marking leaf classes as final for the rationale.
NOTE
All Python examples below assume the standard ESPHome imports are present: esphome.codegen as cg, esphome.config_validation as cv, relevant constants from esphome.const, and typing imports (ConfigType from esphome.types, MockObj from esphome.cpp_generator, TemplateArgsType from esphome.automation).
Triggers
Section titled “Triggers”There are two ways to connect a trigger to an automation. Use the callback method (preferred) for simple cases where the forwarder only needs a single pointer. Use the Trigger class method when the forwarder needs extra state beyond a single pointer.
Callback method (preferred)
Section titled “Callback method (preferred)”The callback method eliminates a separate Trigger C++ class entirely. Instead, a lightweight forwarder struct is registered directly as a callback on the parent component. The forwarder must be pointer-sized (a single Automation* field) so it fits inline in Callback::ctx_ without heap allocation.
Built-in forwarders in esphome/core/automation.h:
| Forwarder | Callback signature | Behavior |
|---|---|---|
TriggerForwarder<Ts...> | void(const Ts&...) | Forwards all args to Automation<Ts...>::trigger() |
TriggerOnTrueForwarder | void(bool) | Triggers Automation<> only when true |
TriggerOnFalseForwarder | void(bool) | Triggers Automation<> only when false |
Python
Section titled “Python”No trigger class declaration is needed. The schema uses an empty dict:
from esphome import automation
CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(MyComponent), cv.Optional(CONF_ON_STATE): automation.validate_automation({}),}).extend(cv.COMPONENT_SCHEMA)In to_code, use build_callback_automation:
async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for conf in config.get(CONF_ON_STATE, []): await automation.build_callback_automation( var, "add_on_state_callback", [(bool, "x")], conf )The arguments to build_callback_automation:
parent— the component variablecallback_method— name of the C++ method to register the callback (e.g."add_on_state_callback")args— template args as[(type, name)]tuples, exposed as variables in the user’sthen:block. This controls theAutomation<Ts...>template parameters, not the callback signature. When using a custom forwarder, the forwarder’soperator()signature must match the callback, butargscan differ (e.g.args=[]withTriggerOnTrueForwarderwhich receivesboolbut triggersAutomation<>)config— the automation config dictforwarder(optional) — override the defaultTriggerForwarder<Ts...>params,forward,when(optional) — reshape or filter the callback arguments, see Different callback arguments or a filter; they cannot be combined withforwarder
For boolean filtering (e.g. on_press / on_release on a void(bool) callback), pass a forwarder. Note that the forwarder receives the bool from the callback but triggers Automation<> with no args:
for conf_key, forwarder in ( (CONF_ON_PRESS, automation.TriggerOnTrueForwarder), (CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),): for conf in config.get(conf_key, []): await automation.build_callback_automation( var, "add_on_state_callback", [], conf, forwarder=forwarder )Several callback automations on one parent go in a module-level _CALLBACK_AUTOMATIONS tuple; each CallbackAutomation entry takes the config key, the method name, the args and, optionally, the same forwarder, params, forward and when keywords:
_CALLBACK_AUTOMATIONS = ( automation.CallbackAutomation( CONF_ON_CONNECT, "set_on_connect", [(cg.bool_, "session_present")] ), automation.CallbackAutomation( CONF_ON_DISCONNECT, "set_on_disconnect", [(MQTTClientDisconnectReason, "reason")] ),)
async def to_code(config: ConfigType) -> None: ... await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)No C++ trigger class is needed — the component just needs the callback registration method:
class MyComponent : public Component { public: // Templatized to accept both std::function and lightweight forwarder structs template<typename F> void add_on_state_callback(F &&callback) { this->state_callback_.add(std::forward<F>(callback)); }
protected: CallbackManager<void(bool)> state_callback_;};When the state changes, call the callback:
void MyComponent::publish_state(bool value) { this->state_ = value; this->state_callback_.call(value);}Custom forwarders
Section titled “Custom forwarders”For state-specific filtering (e.g. trigger only when entering a particular enum state), define a custom forwarder in the component’s automation.h. The forwarder receives the enum value as a callback argument and only triggers when it matches the compile-time template parameter:
#include "esphome/core/automation.h"
// Must be pointer-sized (single Automation* field) to avoid heap allocation.template<MyState State> struct StateEnterForwarder { Automation<> *automation; void operator()(MyState state) const { if (state == State) this->automation->trigger(); }};
static_assert(sizeof(StateEnterForwarder<MY_STATE_ACTIVE>) <= sizeof(void *));static_assert(std::is_trivially_copyable_v<StateEnterForwarder<MY_STATE_ACTIVE>>);Then in Python:
StateEnterForwarder = my_ns.class_("StateEnterForwarder")
# In to_code:await automation.build_callback_automation( var, "add_on_state_callback", [], conf, forwarder=StateEnterForwarder.template(cg.RawExpression("MY_STATE_ACTIVE")),)Different callback arguments or a filter
Section titled “Different callback arguments or a filter”When the parent callback’s parameters are not the automation’s arguments, or the trigger should only fire for some values, pass params, forward and when to build_callback_automation. The helper then generates a capture-less lambda instead of a forwarder struct; being empty, the lambda is stored inline by Callback and std::function alike, so nothing is allocated and no Trigger class is needed.
paramsare the parent callback’s parameters as[(type, name)], rendered asconst T &.forwardare the expressions passed totrigger(); the default is the parameter names. Write the parent asautomation.parent_ref(var), which names it from global scope so a parameter cannot shadow it, and compose lookups on it withMockObjcalls rather than f-strings of C++.whenis a filter the callback returns early on: a plain string, or anApplyCallthat compares against config values as conditions do. AnApplyCallwhose config keys are absent is skipped, so an optional filter needs noif. The filter names the parent as{parent}, see Filters on the parent’s state.
A select’s callback carries only the index, while on_value exposes the option text and the index:
parent = automation.parent_ref(var)index = cg.RawExpression("index")await automation.build_callback_automation( var, "add_on_state_callback", [(cg.StringRef, "x"), (cg.size_t, "i")], conf, params=[(cg.size_t, "index")], forward=[cg.StringRef(parent.option_at(index)), index],)Generated:
select_test_select->add_on_state_callback([](const std::remove_cvref_t<size_t> & index) -> void { ::automation_id->trigger(StringRef(::select_test_select->option_at(index)), index);});Filters on the parent’s state
Section titled “Filters on the parent’s state”A filter that checks the parent writes it as {parent}, which renders as the parent named from global scope. The entry then needs nothing from to_code and sits in the module-level table. Valve on_open and on_closed fire only in their end state:
_CALLBACK_AUTOMATIONS = ( automation.CallbackAutomation( CONF_ON_OPEN, "add_on_state_callback", when="{parent}->is_fully_open()" ), automation.CallbackAutomation( CONF_ON_CLOSED, "add_on_state_callback", when="{parent}->is_fully_closed()" ),)Generated for a valve with the id the_valve:
the_valve->add_on_state_callback([]() -> void { if (!(::the_valve->is_fully_open())) return; ::automation_id->trigger();});A filter that mixes the callback’s parameters and the parent is one expression, for example "state == improv::STATE_STOPPED && !{parent}->is_failed()". build_trigger_callback has no parent unless it is given one with parent=, and a filter that names {parent} without it is rejected.
Automations that receive the parent
Section titled “Automations that receive the parent”A callback that carries nothing while the automation receives the parent (fan on_state, the display menu triggers) is one line. The third argument is a single (type, name) tuple for the automation’s one argument, not a list:
await automation.build_parent_callback_automation( var, "add_on_state_callback", (Fan.operator("ptr"), "x"), conf)Generated for a fan with the id the_fan:
the_fan->add_on_state_callback([]() -> void { ::automation_id->trigger(::the_fan);});Registration with extra arguments
Section titled “Registration with extra arguments”When the registration takes extra arguments, use build_trigger_callback with the same keywords. It builds the automation and returns the lambda, and the component writes the registration call itself, so constant arguments such as a topic and qos are ordinary call arguments. mqtt.on_message with its optional payload filter:
for conf in config.get(CONF_ON_MESSAGE, []): callback = await automation.build_trigger_callback( [(cg.std_string, "x")], conf, params=[(cg.std_string, "topic"), (cg.std_string, "payload")], forward=["payload"], when=automation.ApplyCall( "StringRef(payload) == {}", ((CONF_PAYLOAD, cg.std_string, automation.string_ref_literal),), ), ) cg.add(var.subscribe(cg.progmem_string(conf[CONF_TOPIC]), callback, conf[CONF_QOS]))Generated for payload: "ON" on ESP8266, where cg.progmem_string keeps the topic literal in flash and string_ref_literal renders the payload as a flash literal (StringRef("ON", 2) on other platforms):
mqtt_client->subscribe(progmem_string(ESPHOME_F("livingroom/ota_mode")), [](const std::remove_cvref_t<std::string> & topic, const std::remove_cvref_t<std::string> & payload) -> void { if (!(StringRef(payload) == ESPHOME_F("ON"))) return; ::automation_id->trigger(payload);}, 1);Trigger class method
Section titled “Trigger class method”Use this when the trigger needs mutable state beyond a single Automation* pointer (e.g. tracked previous state for edge detection, or timing logic). A forwarder struct must be pointer-sized with only an Automation* field, so any additional state requires a full Trigger subclass.
Python
Section titled “Python”Declare the trigger class and reference it in the schema:
TurnOnTrigger = my_ns.class_( "TurnOnTrigger", automation.Trigger.template())
CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(MyComponent), cv.Optional(CONF_ON_TURN_ON): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger), } ),}).extend(cv.COMPONENT_SCHEMA)In to_code, use build_automation:
async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf)Several such triggers on one parent go in a module-level _TRIGGER_AUTOMATIONS tuple of (conf_key, args) pairs. build_trigger_automations instantiates each entry’s class from its CONF_TRIGGER_ID, passing var to the constructor (nothing when var is None), and builds the automations:
_TRIGGER_AUTOMATIONS = ( (CONF_ON_TURN_ON, []), (CONF_ON_TURN_OFF, []), (CONF_ON_SPEED_SET, [(cg.int_, "x")]),)
async def to_code(config: ConfigType) -> None: ... await automation.build_trigger_automations(var, config, _TRIGGER_AUTOMATIONS)Define the trigger class in automation.h. This example fires only on the off-to-on transition, requiring mutable state (last_on_) that cannot be stored in a pointer-sized forwarder:
class TurnOnTrigger final : public Trigger<> { public: explicit TurnOnTrigger(MyComponent *parent) : parent_(parent) { parent->add_on_state_callback([this](bool state) { if (state && !this->last_on_) this->trigger(); this->last_on_ = state; }); this->last_on_ = parent->get_state(); }
protected: MyComponent *parent_; bool last_on_;};The trigger needs to track mutable state (last_on_) across callback invocations. A forwarder struct must contain only a single Automation* field — it cannot store extra state. When the trigger logic requires additional fields, use a Trigger subclass instead. The Trigger base class manages the connection to the Automation object.
Hub that fires its own automations
Section titled “Hub that fires its own automations”Use this when the component itself decides when an event fires and passes the values, for example a keypad hub that fires on_key with the key code, and nothing else listens for that event. The hub keeps an AutomationList sized at build time and triggers it directly: each automation costs one pointer-sized slot (4 bytes on 32-bit chips), there is no Trigger object and no heap vector, and an unused list compiles out.
Python
Section titled “Python”List each event once with its count define and trigger arguments, then call build_automation_lists:
_AUTOMATION_LISTS = ( automation.AutomationList(CONF_ON_KEY, "MY_HUB_ON_KEY_COUNT", [(cg.uint8, "x")]),)
async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await automation.build_automation_lists(var, config, _AUTOMATION_LISTS)The schema uses plain automation.validate_automation() with no trigger class. For each automation, the helper calls add_<conf_key>_automation(automation) on the hub (add_on_key_automation above). The count define is the largest number of automations any one hub instance has for that event; it is not emitted when no hub uses the event. Add each define to esphome/core/defines.h for static analysis.
class MyHub : public Component { public:#ifdef MY_HUB_ON_KEY_COUNT void add_on_key_automation(Automation<uint8_t> *automation) { this->key_automations_.add(automation); }#endif
protected: void on_key_(uint8_t key) {#ifdef MY_HUB_ON_KEY_COUNT this->key_automations_.trigger(key);#endif }
#ifdef MY_HUB_ON_KEY_COUNT AutomationList<MY_HUB_ON_KEY_COUNT, uint8_t> key_automations_;#endif};trigger() runs every automation in the order they were added. It shares one copy of its loop between lists of different sizes with the same arguments. When building an argument costs something, such as formatting a string, check empty() first and build it once per event, not once per automation.
Prefer a callback (above) when the parent already has a callback list for the event, or when other code listens to it too.
When each automation needs its own stored value next to it, such as a threshold the hub compares against, build the automations one at a time with automation.new_automation(args, conf) and pass each one to the hub’s own setter with that value. It returns the Automation<args> with its actions and no trigger.
When to use which method
Section titled “When to use which method”| Scenario | Method | Example |
|---|---|---|
| Simple forwarding | Callback | button on_press — TriggerForwarder<> forwards directly |
| Boolean filtering | Callback with built-in forwarder | binary_sensor on_press/on_release — TriggerOnTrueForwarder / TriggerOnFalseForwarder |
| Enum state filtering | Callback with custom forwarder | lock on_lock/on_unlock — LockStateForwarder<State> checks enum, single pointer (esphome/esphome#15199, pending) |
| Different callback arguments or a constant filter | Callback with params, forward, when | select on_value — forwards the option text looked up from the index |
| Callback carries nothing, automation gets the parent | build_parent_callback_automation | fan on_state — forwards ::the_fan |
| Registration takes extra arguments | build_trigger_callback | mqtt on_message — the component passes the lambda to subscribe(topic, callback, qos), filtered on the configured payload |
| Hub fires the event itself, no other listeners | AutomationList | wiegand on_key — AutomationList<WIEGAND_ON_KEY_COUNT, uint8_t> (esphome/esphome#21790, pending) |
| Extra state needed | Trigger class | fan on_turn_on — FanTurnOnTrigger tracks last_on_ for edge detection (mutable state, can’t be a forwarder) |
| Complex logic (timing, state machine) | Trigger class | binary_sensor on_multi_click — MultiClickTrigger with timing, cooldown, and multiple state fields |
Triggers with short-lived arguments
Section titled “Triggers with short-lived arguments”Some triggers pass arguments that are only valid while the callback runs: a StringRef into a receive buffer that the next message overwrites, a JsonObject into a document freed when the callback returns, a span into a buffer the hub reuses. Deferring actions (delay, wait_until, script.wait, …) keep the trigger arguments and use them later — DelayAction copies them by value, which copies the pointer but not the data — so a lambda that runs after them reads stale or freed memory.
If the argument cannot be copied cheaply, reject deferring actions at config validation. automation.validate_synchronous_automation() is validate_automation() with that check added:
cv.Optional(CONF_ON_JSON): automation.validate_synchronous_automation( "on_json and on_data", "the received data"),The first argument says where the automation is and the second what is short-lived; both go into the error message:
Deferring actions (delay, wait_until, script.wait, ...) are not allowed in on_json and on_data: thereceived data is only valid while the automation runs. Copy what you need into globals first, thendefer in a separate script or automation.The other parameters (extra_schema, extra_validators, single) are passed to validate_automation().
When the automation is one alternative among others, for example inside cv.Any(...), chain automation.validate_no_deferring_actions() after it with cv.All(...) so the check runs on the validated value; it ignores values without an action list, such as a returning lambda:
cv.Optional(CONF_ON_NO_RESPONSE): cv.All( cv.Any(cv.returning_lambda, automation.validate_automation(single=True)), automation.validate_no_deferring_actions( "modbus_client handlers", "the request/response data" ),),Both helpers use automation.has_non_synchronous_actions(), which also finds deferring actions nested in if, repeat and similar actions. It depends on the synchronous= flag each action is registered with, see Actions; an action registered without the flag is treated as synchronous=False (with a warning at registration), so it counts as deferring.
Actions
Section titled “Actions”Actions are template classes that perform an operation when invoked by an automation.
Actions that only forward values
Section titled “Actions that only forward values”Most actions do nothing but pass configured values on to their parent. Register those with automation.register_apply_action; no C++ class and no builder are written, and the action is always synchronous:
automation.register_apply_action( "my_component.set_gains", cv.Schema({ cv.Required(CONF_ID): cv.use_id(MyComponent), cv.Required(CONF_KP): cv.templatable(cv.float_), cv.Optional(CONF_KI): cv.templatable(cv.float_), }), automation.ApplyField(CONF_KP, "set_kp", cg.float_), automation.ApplyField(CONF_KI, "set_ki", cg.float_),)The action is the core ApplyAction<Ts...>, which stores one function pointer. Code generation folds the parent and every configured field into one stateless function: constants become immediates, user lambdas are called inline with the trigger arguments, and an absent optional key emits nothing, so the action costs one pointer however many fields it has. With kp: 1.5 in the config the generated function body is my_component->set_kp(1.5f);.
ApplyField(conf_key, target, type_)forwards one key:targetis a setter name, or a statement template when it contains{}(for example"position = {}"; double a literal brace). A template may name the parent as{parent}.type_is the C++ type a user lambda must return. A plain string is raw C++ type text and may use{parent}when the type is only known per instance.conf_keymay be a tuple of keys to read a nested section.std::stringconstants are emitted as a plain literal, or asprogmem_string(ESPHOME_F(...))on ESP8266 so the literal stays in flash.const_fn=renders a constant whencg.safe_expis not the right spelling; it receives the action config and the value. A!lambdavalue bypasses it, so the target must also accept a plaintype_argument.
ApplyCall("set_range({}, {})", ((CONF_LOW, cg.float_), (CONF_HIGH, cg.float_)))folds several keys into one statement; each arg may carry a thirdconst_fnelement. It is skipped when none of the keys is set and a partial set is a config error. A call with no keys, such asApplyCall("stop()")or a trailingApplyCall("publish_state()"), is always emitted, in the order given. AnApplyCalltemplate may name the parent as{parent}too.ApplyBytes(CONF_DATA, "transmit_packet({})")forwards a byte payload. The key is validated withcv.templatable(...)over a byte list or string, and the target’s single{}expands todata, len. A constant payload is a sharedPROGMEMtable passed directly, so it costs no RAM; on ESP8266 it is first copied out, to the stack up tostack=bytes (default 64) and to the heap beyond that, unlessprogmem_target=names a call that reads flash itself, such asuart’swrite_array_progmem. A user lambda returnsstd::vector<uint8_t>and its data and size are passed. The template may name the parent as{parent}.call="make_call"is for actions that build a call object: every statement, follow-up calls included, targets the call object returned byparent->make_call(), andperform()on it is appended last.
cover.control and cover.template.publish in the ESPHome repository are in-tree examples; sx127x.send_packet and uart.write show ApplyBytes. The hand-written class below is for actions whose play() has logic beyond forwarding values.
Python
Section titled “Python”When play() has its own logic but the builder only needs to look up the parent component and construct the C++ object, register the action with one call and no builder function:
MyAction = my_ns.class_("MyAction", automation.Action)
automation.register_simple_action( "my_component.do_something", MyAction, cv.Schema({cv.GenerateID(): cv.use_id(MyComponent)}), synchronous=True,)register_simple_action passes the object named by config[CONF_ID] to the constructor. Two sibling helpers cover the other constructor shapes:
register_parented_actionfor a class deriving fromParented<T>: the object is constructed without arguments andset_parent()receives the parent.register_bare_actionfor a constructor that takes no arguments at all, typically an action that reaches a global singleton.
Set synchronous=True if the action completes immediately (no async operations like delays or waits). Set synchronous=False if the action defers play_next_() to a later point (e.g. after a delay or async operation completes).
These helpers are available in ESPHome 2026.10.0 and later (esphome/esphome#19321). When the builder must also set fields, or on earlier versions, use the @automation.register_action decorator on a builder function instead (see the templatable example below). The builder body that matches each helper is:
register_simple_action: awaitcg.get_variable(config[CONF_ID])and returncg.new_Pvariable(action_id, template_arg, parent).register_parented_action: createvar = cg.new_Pvariable(action_id, template_arg), thenawait cg.register_parented(var, config[CONF_ID])and returnvar.register_bare_action: returncg.new_Pvariable(action_id, template_arg)with no lookup.
template<typename... Ts> class MyAction final : public Action<Ts...> { public: explicit MyAction(MyComponent *parent) : parent_(parent) {}
void play(const Ts &...) override { this->parent_->do_something(); }
protected: MyComponent *parent_;};The register_parented_action shape derives from Parented<T> instead, which supplies set_parent() and the parent_ member, so the class declares no constructor:
template<typename... Ts> class MyAction final : public Action<Ts...>, public Parented<MyComponent> { public: void play(const Ts &...) override { this->parent_->do_something(); }};For a hand-written action class that accepts templatable values:
template<typename... Ts> class SetValueAction final : public Action<Ts...> { public: explicit SetValueAction(MyComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(bool, state)
void play(const Ts &...x) override { this->parent_->set_state(this->state_.value(x...)); }
protected: MyComponent *parent_;};Values passed to a TEMPLATABLE_VALUE setter from Python codegen should always be wrapped with cg.templatable(), even when the value is a literal constant. The setter’s storage is TemplatableFn for trivially copyable types (bool, int, float, enums, pointers) and TemplatableValue otherwise; TemplatableFn holds only a function pointer and will not accept a raw C++ value, so failing to wrap compiles on 2026.3.x and earlier only when the setter type is TemplatableValue, which accepts raw constants:
@automation.register_action( "my_component.set_state", SetValueAction, cv.Schema({ cv.GenerateID(): cv.use_id(MyComponent), cv.Required(CONF_STATE): cv.templatable(cv.boolean), }), synchronous=True,)async def set_state_to_code( config: ConfigType, action_id: MockObj, template_arg: MockObj, args: TemplateArgsType) -> MockObj: parent = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, parent) template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) cg.add(var.set_state(template_)) return varSame imports as the earlier Python snippets on this page apply (cg, cv, automation, constants, typing aliases); CONF_STATE and SetValueAction are defined by the component itself.
Passing a raw value such as cg.add(var.set_state(config[CONF_STATE])) worked on 2026.3.x and earlier for literal constants but fails to compile on 2026.4.0 and later, because trivially copyable types use TemplatableFn (pointer-sized function-pointer storage, 4 bytes on 32-bit platforms), for which implicit conversion from raw values is not possible. See the TemplatableFn blog post for details.
Conditions
Section titled “Conditions”Conditions are template classes that return a boolean to control automation flow.
Conditions that only test the parent
Section titled “Conditions that only test the parent”Most conditions are one expression on their parent. Register those with automation.register_apply_condition; no C++ class and no builder are written:
automation.register_apply_condition( "my_component.is_active", cv.Schema({cv.GenerateID(): cv.use_id(MyComponent)}), "is_active()",)The condition is the core ApplyCondition<Ts...>, which stores one function pointer to a stateless function that returns the expression applied to the parent, my_component->is_active() here. To compare against a configured value pass an ApplyCall instead of a string, with the same {} placeholders, (conf_key, type_) args and const_fn as for actions: automation.ApplyCall("state == {}", ((CONF_STATE, cg.bool_),)) generates my_component->state == true for state: true and calls a user lambda inline. Every key named by the call must be present in the config. The expression is appended to parent->, so it must start with a parent member; a leading ! would generate my_component->!is_active(), so negate with == false. The template may name the parent as {parent} later in the expression, but {parent} cannot start it, so it does not get around the leading ! limit.
cover.is_open and rtttl.is_playing in the ESPHome repository are in-tree examples. The hand-written class below is for a check() that needs more than one expression on the parent.
Python
Section titled “Python”MyCondition = my_ns.class_("MyCondition", automation.Condition)
automation.register_simple_condition( "my_component.is_ready", MyCondition, cv.Schema({cv.GenerateID(): cv.use_id(MyComponent)}),)register_parented_condition and register_bare_condition mirror the action helpers. A condition whose builder must also set fields uses the @automation.register_condition decorator on a builder function. The builder mirrors the templatable action example above with condition_id in place of action_id; register_condition takes no synchronous= parameter.
template<typename... Ts> class MyCondition final : public Condition<Ts...> { public: explicit MyCondition(MyComponent *parent) : parent_(parent) {} bool check(const Ts &...) override { if (!this->parent_->is_active()) return false; return this->parent_->error_count() == 0; }
protected: MyComponent *parent_;};Copyright © 2026 ESPHome - A project from the Open Home Foundation