Skip to main content
Abilities can run background threads alongside the main conversation. Add a file called background.py to any Ability folder and it runs automatically when the user connects to a Personality, staying alive as an independent thread for the entire session. No hotword trigger needed.

What this unlocks

  • Background polling — check a file or API on a timer
  • Proactive notifications — interrupt the conversation when something fires
  • Scheduled tasks — monitor for time-based events like alarms
  • Ambient monitoring — watch the live conversation for note-taking or summarization
This is additive. Existing main.py-only Abilities work exactly as before.

The four Ability categories

See Ability Types for the full breakdown. Background Daemons are one of the four.

File structure

Every Ability is built from one or two files, regardless of which category you pick in the Dashboard.
TypeFilesDescription
Standard Interactivemain.py onlyUser triggers with hotwords → runs → exits with resume_normal_flow(). The original pattern.
Standalone Background Daemonbackground.py onlyStarts automatically on session. Runs in background for monitoring, logging, note-taking. Works even when Personality is asleep.
Interactive + Daemonmain.py + background.pyInteractive handles user requests. Daemon runs in background. They coordinate through shared file storage.

Example: Interactive Combined Ability

AlarmAbility/
├── main.py           # Interactive — set an alarm
├── background.py     # Background — fire the alarm
├── config.json       # Required
└── alarm.mp3         # Supporting files
The background file must be named exactly background.py. No other filename will be detected by the platform.

main.py vs background.py

These are the most common sources of bugs when writing background daemons. Pay close attention.
Aspectmain.pybackground.py
call() signaturecall(self, worker)call(self, worker, background_daemon_mode)
CapabilityWorker initCapabilityWorker(self)CapabilityWorker(self)
Triggered byUser hotwordsAutomatically on session start
LifecycleRuns once, then exitsContinuous while True loop
resume_normal_flow()Required on every exit pathNot needed (independent thread)
Works in sleep modeNo — requires active sessionYes — runs even when Personality is asleep
Multiple instancesOne at a timeMultiple daemons supported

New SDK methods

MethodReturnsAsyncDescription
get_timezone()strNoUser’s timezone (e.g. "America/Chicago"). Use for alarms, calendars, time-aware logic.
get_full_message_history()listNoFull conversation transcript. Daemons use this to monitor the live conversation.
send_interrupt_signal()YesStops current Personality output. Call before speak() or play_audio() from a daemon.

Usage

# Get user timezone (synchronous)
tz = self.capability_worker.get_timezone()

# Get conversation history (synchronous)
history = self.capability_worker.get_full_message_history()

# Interrupt before speaking from a background daemon (async)
await self.capability_worker.send_interrupt_signal()
await self.capability_worker.speak("Your alarm is going off!")

Background daemon code template

Copy this as your starting point for any background daemon. The call() signature has an extra background_daemon_mode parameter, but the CapabilityWorker constructor is the same as main.py.
import json
from src.agent.capability import MatchingCapability
from src.main import AgentWorker
from src.agent.capability_worker import CapabilityWorker
from time import time


class YourWatcherCapability(MatchingCapability):
    worker: AgentWorker = None
    capability_worker: CapabilityWorker = None
    background_daemon_mode: bool = False

    #{{register capability}}

    async def watcher_loop(self):
        self.worker.editor_logging_handler.info(
            "%s: Watcher started" % time()
        )
        while True:
            # --- your background logic here ---
            self.worker.editor_logging_handler.info(
                "%s: Watcher cycle" % time()
            )
            await self.worker.session_tasks.sleep(20.0)

    def call(self, worker: AgentWorker, background_daemon_mode: bool):
        self.worker = worker
        self.background_daemon_mode = background_daemon_mode
        self.capability_worker = CapabilityWorker(self)
        self.worker.session_tasks.create(self.watcher_loop())
Ordering matters. self.worker and self.background_daemon_mode must be set before calling CapabilityWorker(self). The constructor reads from self internally — if these aren’t set first, it will fail.

Key behaviors

Works in sleep mode

Background daemons continue running even when the Personality is in sleep mode. The daemon is an independent thread — it does not depend on the main conversation flow being active. This means your daemon can:
  • Monitor and transcribe ambient audio without a wake word
  • Process conversations happening around the device
  • Interject when it detects something relevant (via send_interrupt_signal())
  • Build RAG-style user context summaries in the background
The only requirement is that the daemon’s main function runs in a never-ending while True loop.

Multiple watchers

You can have multiple background daemons running simultaneously. Each is its own independent thread. For example, you could run an alarm daemon, a note-taking daemon, and a conversation summarizer all at the same time.

Full speak capability

Background daemons have the same ability to speak as interactive Abilities. A watcher can call speak(), play_audio(), text_to_speech(), and all other CapabilityWorker methods. Just call send_interrupt_signal() first to avoid audio overlap with any active conversation.

Coordination pattern

The primary way interactive and background components communicate is through shared persistent file storage. Both files read and write to the same user-scoped files.

Example: Alarm Ability

StepComponentAction
1UserSays “set an alarm for 3pm Thursday”
2main.pyLLM parses time, writes alarm to alarms.json
3main.pyConfirms to user, calls resume_normal_flow()
4background.pyPolls alarms.json every ~15 seconds (running since session start)
5background.pyTarget time hits → send_interrupt_signal()
6background.pyPlays alarm.mp3, speaks notification
7background.pyUpdates alarm status to "triggered" in alarms.json

Sample alarms.json

[
  {
    "id": "alarm_1772046000778",
    "created_at_epoch": 1772046000,
    "timezone": "America/Los_Angeles",
    "target_iso": "2026-02-26T00:06:00-08:00",
    "human_time": "12:01 AM on Thursday, Feb 26, 2026",
    "source_text": "Can you set an alarm for me?",
    "status": "scheduled"
  }
]

Best practices

Session tasks ensure proper cleanup when the session ends. asyncio.sleep() can leak.
10–30 seconds is typical. For alarms, 15–30 seconds is fine.
The JSON file may not exist yet if main.py hasn’t been triggered. Always check_if_file_exists() first.
write_file() appends, which corrupts JSON. Always delete, then write the full object.
Background daemons run silently. editor_logging_handler is your only window into what they’re doing.
Otherwise audio overlaps, or the system tries to transcribe your daemon’s output as user input.
Background daemons work even when the Personality is asleep — but only if the main function is a never-ending while True loop.

Templates & resources

ResourceLink
Alarm Ability (Interactive Combined)openhome-dev/abilities/templates/Alarm
Standalone Background Daemonopenhome-dev/abilities/templates/Background
SDK ReferenceSDK Reference
Questions / support#dev-help on Discord
The Alarm template is the best reference for the Interactive Combined pattern. Study both main.py and background.py to understand how they coordinate.