Understanding Home Assistant's Data Entry Flow System and Config Flows

The data entry flow system is an asynchronous framework that guides users through integration setup using the generic FlowHandler class, while domain-specific subclasses of ConfigFlow implement the actual validation, form presentation, and configuration entry creation logic.

Home Assistant's extensible architecture relies on a robust data entry flow system to handle the configuration of integrations, devices, and services through the UI. This framework decouples the generic wizard mechanics from integration-specific logic, allowing developers to create guided setup experiences by subclassing ConfigFlow. The core implementation resides in homeassistant/data_entry_flow.py and homeassistant/config_entries.py, providing the foundation for all UI-driven configuration in the platform.

Architecture of the Data Entry Flow System

The data entry flow system separates concerns between the generic flow engine and integration-specific configuration logic. Understanding these boundaries is essential for developing custom integrations.

Core Components and Responsibilities

The system consists of three primary classes defined across the core repository:

  • FlowHandler – Located in homeassistant/data_entry_flow.py (lines 609-785), this generic engine manages flow instances, stores context, and handles form rendering, entry creation, and aborts.
  • ConfigFlow – Defined in homeassistant/config_entries.py (lines 2918-2950), this base class registers handlers by domain and adds integration-specific helpers for unique ID handling and options flow support.
  • ConfigEntryBaseFlow – Also in config_entries.py (lines ~2910-2930), this provides shared functionality for both config and options flows, including the _async_abort_entries_match method for duplicate detection.

How Config Flows Work Step-by-Step

The lifecycle of a config flow follows a structured seven-step process managed by the flow manager in homeassistant/data_entry_flow.py:

  1. Registration – Developers subclass ConfigFlow with the optional domain argument; the __init_subclass__ method automatically registers the handler in the global HANDLERS registry.
  2. Flow Initialization – The UI or automatic discovery calls hass.config_entries.flow.async_init(domain, ...), generating a unique flow_id and instantiating a FlowHandler instance.
  3. Step Execution – The flow manager calls the initial step method (defaulting to async_step_init), which typically redirects to async_step_user.
  4. Form Presentation – The flow returns self.async_show_form(step_id="user", data_schema=vol.Schema(...)), serializing the form definition to the frontend via the _async_flow_handler_to_flow_result method (lines 890-906).
  5. Input Processing – Upon submission, the flow receives user data, performs async validation (e.g., testing API connections), and checks for duplicates using _async_abort_entries_match or _abort_if_unique_id_mismatch.
  6. Entry Creation – Successful validation calls self.async_create_entry(title="...", data={...}), persisting a ConfigEntry to core.config_entries.
  7. Options Flow – For post-setup configuration, async_get_options_flow returns an OptionsFlow subclass that follows the same step pattern for modifying existing entries.

Core API Implementation Details

The config flow system exposes specific APIs for form rendering, validation, and state management. These implementations demonstrate the framework's asynchronous, step-driven design.

The FlowHandler Base Class

The FlowHandler class in homeassistant/data_entry_flow.py provides the foundational methods that all configuration wizards inherit:

class FlowHandler(Generic[_FlowContextT, _FlowResultT, _HandlerT]):
    """Handle a data entry flow."""
    cur_step: _FlowResultT | None = None
    flow_id: str = None
    hass: HomeAssistant = None
    handler: _HandlerT = None
    context: _FlowContextT = MappingProxyType({})

    @property
    def source(self) -> str | None:
        """Source that initialized the flow."""
        return self.context.get("source", None)

    def async_show_form(...):
        """Return a FORM result for the UI."""
        ...

    def async_create_entry(...):
        """Finish flow and create a ConfigEntry."""
        ...

    def async_abort(...):
        """Abort the flow."""
        ...

Key methods include async_show_form() for rendering UI steps, async_create_entry() for persisting configuration, and async_abort() for terminating flows with specific error codes.

ConfigFlow Integration Interface

The ConfigFlow class extends the base functionality with integration-specific utilities. According to the source in homeassistant/config_entries.py (lines 2918-2950), it handles automatic registration via __init_subclass__:

class ConfigFlow(ConfigEntryBaseFlow):
    """Base class for config flows with some helpers."""

    def __init_subclass__(cls, *, domain: str | None = None, **kwargs):
        super().__init_subclass__(**kwargs)
        if domain is not None:
            HANDLERS.register(domain)(cls)   # ← registers the flow

    @property
    def unique_id(self) -> str | None:
        """Return unique ID if available."""
        if not self.context:
            return None
        return self.context.get("unique_id")

    @staticmethod
    @callback
    def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
        """Get the options flow for this handler."""
        raise data_entry_flow.UnknownHandler

The unique_id property enables the system to identify existing entries and prevent duplicates across the integration domain.

Duplicate Prevention Helpers

Config flows utilize specialized abort methods defined in config_entries.py (lines ~2890-2898) to maintain data integrity:

  • _async_abort_entries_match – Aborts the flow when another entry already contains identical data.
  • _abort_if_unique_id_mismatch – Aborts if the flow's supplied unique ID does not match an existing entry's identifier.

Both helpers raise data_entry_flow.AbortFlow("already_configured") to signal the frontend that the device or service is already configured.

Building a Custom Config Flow: Practical Example

Implementing a config flow requires subclassing ConfigFlow and defining step methods that handle user interaction. Below is a minimal implementation pattern from homeassistant/components/example/config_flow.py:

from homeassistant import config_entries, data_entry_flow
import voluptuous as vol

DOMAIN = "example"


class ExampleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    """Config flow for the Example integration."""

    VERSION = 1

    async def async_step_user(self, user_input: dict | None = None):
        """First step of the flow (shown to the user)."""
        if user_input is None:
            # Show a form asking for an API key

            return self.async_show_form(
                step_id="user",
                data_schema=vol.Schema(
                    {
                        vol.Required("api_key"): str,
                    }
                ),
                errors={},
            )

        # Validate the API key (async I/O)

        try:
            await self._test_api_key(user_input["api_key"])
        except Exception:
            return self.async_show_form(
                step_id="user",
                data_schema=vol.Schema({vol.Required("api_key"): str}),
                errors={"api_key": "invalid"},
            )

        # No duplicate entries?

        await self._async_abort_entries_match(
            match_dict={"api_key": user_input["api_key"]}
        )

        # Create the entry

        return self.async_create_entry(
            title="Example Integration",
            data=user_input,
        )

    async def _test_api_key(self, api_key: str) -> None:
        """Placeholder for real async validation of the key."""
        # Perform network request here...

        return

Key implementation points include:

  • Domain Registration – The domain=DOMAIN class argument triggers automatic registration via ConfigFlow.__init_subclass__.
  • Step Methods – async_step_user handles the initial user interaction, returning async_show_form when awaiting input and async_create_entry upon successful validation.
  • Schema Validation – Voluptuous schemas define the form structure and validation rules sent to the frontend.
  • Duplicate Checking – _async_abort_entries_match ensures users cannot add the same API key twice.
  • Error Handling – Returning async_show_form with an errors dictionary displays validation failures to the user.

Summary

  • The data entry flow system provides the generic engine (FlowHandler) for multi-step configuration wizards in homeassistant/data_entry_flow.py.
  • Config flows are implemented by subclassing ConfigFlow in homeassistant/config_entries.py, which handles domain registration and unique ID management.
  • The framework uses async_show_form to render UI steps and async_create_entry to persist configuration, with built-in helpers like _async_abort_entries_match preventing duplicates.
  • All configuration entries are stored as ConfigEntry objects and managed through the config entry registry, supporting post-setup modification via options flows.

Frequently Asked Questions

What is the difference between FlowHandler and ConfigFlow?

FlowHandler is the generic base class in homeassistant/data_entry_flow.py that manages the flow lifecycle and UI communication, while ConfigFlow is the integration-specific subclass in homeassistant/config_entries.py that adds domain registration, unique ID handling, and options flow support for Home Assistant integrations.

How does a config flow prevent duplicate entries?

Config flows use helper methods such as _async_abort_entries_match and _abort_if_unique_id_mismatch, implemented in homeassistant/config_entries.py around lines 2890-2898, to check existing entries against the flow's unique ID or data. If a match is found, the flow raises AbortFlow("already_configured") to prevent duplicates.

Can a config flow have multiple steps?

Yes, config flows support multi-step wizards by defining methods named async_step_<step_id>. The flow returns self.async_show_form(step_id="next_step", ...) to progress to subsequent steps, with the flow manager in homeassistant/data_entry_flow.py handling the state transitions between steps.

What triggers a config flow to start?

Config flows are initiated when the UI calls hass.config_entries.flow.async_init(domain, ...) with the integration's domain identifier, or when automatic discovery mechanisms (like Zeroconf or SSDP) trigger the creation of a flow instance with a generated flow_id.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →