Trigger Trampolines Eliminated with build_callback_automation
Common entity trigger classes have been replaced with lightweight forwarder structs that fit inline in the callback system. The new build_callback_automation() API eliminates per-trigger object allocations. Several entity callback signatures have also changed to pass state as an argument.
This is a breaking change for external components in ESPHome 2026.4.0 and later.
Background
Section titled “Background”- PR #15174: Eliminate trigger trampolines with deduplicated forwarder structs
- PR #15198:
alarm_control_panel— Migrate triggers to callback automation - PR #15199:
lock— Migrate LockStateTrigger to callback automation - PR #15200:
media_player— Migrate triggers to callback automation
Previously, each automation trigger created a separate C++ object that existed solely to forward a callback to an Automation. For example:
button press → callback → ButtonPressTrigger::trigger() → Automation::trigger()Now a lightweight forwarder struct collapses this into the callback itself:
button press → callback → TriggerForwarder::operator()() → Automation::trigger()The forwarder fits in the Callback::ctx_ field — no additional storage needed.
Memory savings
Section titled “Memory savings”| Config | Platform | RAM Saved | Flash Saved |
|---|---|---|---|
| ratgdo (garage door) | ESP8266 | 88 bytes | 224 bytes |
| multi-sensor device | ESP32-IDF | 208 bytes | 280 bytes |
What’s Changing
Section titled “What’s Changing”1. Callback signatures changed
Section titled “1. Callback signatures changed”Several entity callback signatures changed to pass state as an argument, enabling single-pointer forwarders:
// alarm_control_panel — Beforevoid add_on_state_callback(std::function<void()> &&callback);// Aftertemplate<typename F> void add_on_state_callback(F &&callback);// Callback signature: void(AlarmControlPanelState)
// lock — Beforevoid add_on_state_callback(std::function<void()> &&callback);// Aftertemplate<typename F> void add_on_state_callback(F &&callback);// Callback signature: void(LockState)
// media_player — Beforevoid add_on_state_callback(std::function<void()> &&callback);// Aftertemplate<typename F> void add_on_state_callback(F &&callback);// Callback signature: void(MediaPlayerState)2. Automation::trigger_ field removed
Section titled “2. Automation::trigger_ field removed”The trigger_ protected field on Automation (set in constructor, never read) has been removed.
Who This Affects
Section titled “Who This Affects”- External components registering callbacks on
alarm_control_panel,lock, ormedia_player— must update callback signature to accept the state parameter - External components accessing
Automation::trigger_— this field no longer exists
Migration Guide
Section titled “Migration Guide”Callback signature changes
Section titled “Callback signature changes”// alarm_control_panel — Beforethis->parent_->add_on_state_callback([this]() { auto state = this->parent_->get_state(); // ...});
// Afterthis->parent_->add_on_state_callback([this](AlarmControlPanelState state) { // state is passed directly, no need to call get_state()});// lock — Beforethis->parent_->add_on_state_callback([this]() { auto state = this->parent_->state; // ...});
// Afterthis->parent_->add_on_state_callback([this](LockState state) { // state is passed directly});// media_player — Beforethis->parent_->add_on_state_callback([this]() { auto state = this->parent_->state; // ...});
// Afterthis->parent_->add_on_state_callback([this](MediaPlayerState state) { // state is passed directly});Python codegen migration
Section titled “Python codegen migration”If your external component uses build_automation() with trigger classes, migrate to build_callback_automations() (PR #15246):
# Beforefrom esphome import automation
MyStateTrigger = my_ns.class_("MyStateTrigger", automation.Trigger.template(cg.bool_))MyPressTrigger = my_ns.class_("MyPressTrigger", automation.Trigger.template())
CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_ON_STATE): automation.validate_automation( {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MyStateTrigger)} ), cv.Optional(CONF_ON_PRESS): automation.validate_automation( {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MyPressTrigger)} ),})
async def to_code(config): for conf in config.get(CONF_ON_STATE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [(bool, "x")], conf) for conf in config.get(CONF_ON_PRESS, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf)# After — no trigger classes needed, no CONF_TRIGGER_ID in schemafrom esphome import automation
_CALLBACK_AUTOMATIONS = ( automation.CallbackAutomation(CONF_ON_STATE, "add_on_state_callback", [(bool, "x")]), automation.CallbackAutomation(CONF_ON_PRESS, "add_on_press_callback"),)
CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_ON_STATE): automation.validate_automation({}), cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),})
async def to_code(config): await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)build_automation() and all Trigger subclasses remain available for triggers that need mutable state beyond a single Automation* pointer.
Timeline
Section titled “Timeline”- ESPHome 2026.4.0 (April 2026): Trigger classes removed, callback signatures changed
- No deprecation period — these are signature changes and class removals
Finding Code That Needs Updates
Section titled “Finding Code That Needs Updates”# Find alarm_control_panel/lock/media_player callback registrationsgrep -rn 'add_on_state_callback' your_component/Questions?
Section titled “Questions?”If you have questions about migrating your external component, please ask in:
- ESPHome Discord - #devs channel
- ESPHome GitHub Discussions
Related Documentation
Section titled “Related Documentation”Copyright © 2026 ESPHome - A project from the Open Home Foundation
Comments
Feel free to leave a comment here to discuss this post with others. You can ask questions, share your experience, or suggest improvements. If you have a question about a specific feature or issue, please consider using the ESPHome Discord. Stick to English and follow ESPHome's code of conduct. These comments exist on a discussion on GitHub, so you can also comment there directly if you prefer.