Skip to content

Scheduler

This page covers how to migrate AppDaemon scheduler calls to Hassette's self.scheduler attribute.

Overview

AppDaemon exposes scheduler helpers as methods directly on self: self.run_in(...), self.run_daily(...). They return an opaque handle you pass to self.cancel_timer(handle) to cancel the job.

Hassette exposes the scheduler as a separate attribute self.scheduler. Methods use named parameters, and they return a ScheduledJob object you cancel with .cancel(). Handlers can be async or sync, and they don't need to follow a fixed signature.

Callback Signatures

AppDaemon requires schedule callbacks to follow def my_callback(self, **kwargs). The kwargs dictionary includes any data you passed when scheduling plus an internal __thread_id value. The documentation recommends not using async functions due to the threading model.

Hassette scheduled jobs can be any callable — async or sync, with any parameters. If you pass keyword arguments when scheduling, declare them as parameters on your handler:

from hassette import App, AppConfig


class MyConfig(AppConfig):
    color_name: str = "red"


class NightLight(App[MyConfig]):
    # function which will be called at startup and reload
    async def on_initialize(self):
        # Schedule a daily callback that will call run_daily_callback() at 7pm every night
        job = await self.scheduler.run_daily(self.run_daily_callback, at="19:00")
        self.logger.info("Scheduled job: %r", job)

        # 2025-10-13 19:57:02.670 INFO hassette.NightLight.0.on_initialize:11 - Scheduled job: ScheduledJob(name='run_daily_callback', owner=NightLight.0)

    # Our callback function will be called by the scheduler every day at 7pm
    async def run_daily_callback(self):
        # Call to Home Assistant to turn the porch light on
        await self.api.turn_on("light.office_light_1", color_name=self.app_config.color_name)

Method Equivalents

AppDaemon Hassette Hassette Notes
self.run_in(cb, 60) self.scheduler.run_in(cb, delay=60) Delay in seconds
self.run_once(cb, time(7, 30)) self.scheduler.run_once(cb, at="07:30") "HH:MM" string or ZonedDateTime
self.run_every(cb, "now", 300) self.scheduler.run_every(cb, seconds=300) Interval via hours=, minutes=, seconds=
self.run_minutely(cb) self.scheduler.run_minutely(cb) Every 1 minute by default
self.run_hourly(cb, time(0, 30)) self.scheduler.run_hourly(cb) Every 1 hour by default
self.run_daily(cb, time(7, 30)) self.scheduler.run_daily(cb, at="07:30") Wall-clock, DST-safe (cron-backed)
self.cancel_timer(handle) job.cancel() Cancel via the returned job object

run_daily is now wall-clock-aligned

Hassette's run_daily uses a cron-based trigger internally. It fires at the specified wall-clock time every day, correctly handling DST transitions. This is different from the old interval-based approach that could drift by an hour across DST boundaries.

Side-by-Side Comparison

from appdaemon.plugins.hass import Hass


class NightLight(Hass):
    # function which will be called at startup and reload
    def initialize(self):
        # Schedule a daily callback that will call run_daily_callback() at 7pm every night
        self.run_daily(self.run_daily_callback, "19:00:00")

    # Our callback function will be called by the scheduler every day at 7pm
    def run_daily_callback(self, **kwargs):
        # Call to Home Assistant to turn the porch light on
        self.turn_on("light.porch")
from hassette import App, AppConfig


class MyConfig(AppConfig):
    color_name: str = "red"


class NightLight(App[MyConfig]):
    # function which will be called at startup and reload
    async def on_initialize(self):
        # Schedule a daily callback that will call run_daily_callback() at 7pm every night
        job = await self.scheduler.run_daily(self.run_daily_callback, at="19:00")
        self.logger.info("Scheduled job: %r", job)

        # 2025-10-13 19:57:02.670 INFO hassette.NightLight.0.on_initialize:11 - Scheduled job: ScheduledJob(name='run_daily_callback', owner=NightLight.0)

    # Our callback function will be called by the scheduler every day at 7pm
    async def run_daily_callback(self):
        # Call to Home Assistant to turn the porch light on
        await self.api.turn_on("light.office_light_1", color_name=self.app_config.color_name)

Migration Example

The following shows a typical AppDaemon pattern converted to Hassette:

from datetime import time

def initialize(self):
    self.run_in(self.delayed_task, 60)
    self.run_daily(self.morning_task, time(7, 30))
    handle = self.run_every(self.periodic_task, "now", 300)
from hassette import App


class MySchedulerApp(App):
    async def on_initialize(self):
        await self.scheduler.run_in(self.delayed_task, delay=60)
        await self.scheduler.run_daily(self.morning_task, at="07:30")
        job = await self.scheduler.run_every(self.periodic_task, seconds=300)

    async def delayed_task(self):
        pass

    async def morning_task(self):
        pass

    async def periodic_task(self):
        pass

Key changes:

  • Access via self.scheduler instead of calling directly on self
  • run_daily takes an at="HH:MM" string instead of a time object or start= parameter
  • run_every takes hours=, minutes=, seconds= keyword arguments instead of a positional interval
  • run_cron takes a cron expression string instead of keyword fields (hour=, minute=, etc.)
  • Jobs return rich ScheduledJob objects instead of opaque handles
  • Cancel with job.cancel() instead of self.cancel_timer(handle)

Blocking Work in Scheduler Callbacks

In AppDaemon, every callback runs in its own thread, so you can do blocking IO safely. In Hassette, the scheduler automatically runs sync callables in a thread pool, regardless of whether you're using App or AppSync. This means:

  • Write the callback as a plain (non-async) def — the scheduler detects that it's not a coroutine and runs it in a thread automatically.
  • Use AppSync only if you also want sync lifecycle hooks (on_initialize_sync, on_shutdown_sync, etc.) — not because you need scheduler callbacks to run in threads.

If your callback is async def, it runs in the event loop directly. For blocking IO inside an async callback, use asyncio.to_thread() or self.task_bucket.run_in_thread().

See Also