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.| Type | Files | Description |
|---|---|---|
| Standard Interactive | main.py only | User triggers with hotwords → runs → exits with resume_normal_flow(). The original pattern. |
| Standalone Background Daemon | background.py only | Starts automatically on session. Runs in background for monitoring, logging, note-taking. Works even when Personality is asleep. |
| Interactive + Daemon | main.py + background.py | Interactive handles user requests. Daemon runs in background. They coordinate through shared file storage. |
Example: Interactive Combined Ability
main.py vs background.py
These are the most common sources of bugs when writing background daemons. Pay close attention.
| Aspect | main.py | background.py |
|---|---|---|
call() signature | call(self, worker) | call(self, worker, background_daemon_mode) |
| CapabilityWorker init | CapabilityWorker(self) | CapabilityWorker(self) |
| Triggered by | User hotwords | Automatically on session start |
| Lifecycle | Runs once, then exits | Continuous while True loop |
resume_normal_flow() | Required on every exit path | Not needed (independent thread) |
| Works in sleep mode | No — requires active session | Yes — runs even when Personality is asleep |
| Multiple instances | One at a time | Multiple daemons supported |
New SDK methods
| Method | Returns | Async | Description |
|---|---|---|---|
get_timezone() | str | No | User’s timezone (e.g. "America/Chicago"). Use for alarms, calendars, time-aware logic. |
get_full_message_history() | list | No | Full conversation transcript. Daemons use this to monitor the live conversation. |
send_interrupt_signal() | — | Yes | Stops current Personality output. Call before speak() or play_audio() from a daemon. |
Usage
Background daemon code template
Copy this as your starting point for any background daemon. Thecall() signature has an extra background_daemon_mode parameter, but the CapabilityWorker constructor is the same as main.py.
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
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 callspeak(), 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
| Step | Component | Action |
|---|---|---|
| 1 | User | Says “set an alarm for 3pm Thursday” |
| 2 | main.py | LLM parses time, writes alarm to alarms.json |
| 3 | main.py | Confirms to user, calls resume_normal_flow() |
| 4 | background.py | Polls alarms.json every ~15 seconds (running since session start) |
| 5 | background.py | Target time hits → send_interrupt_signal() |
| 6 | background.py | Plays alarm.mp3, speaks notification |
| 7 | background.py | Updates alarm status to "triggered" in alarms.json |
Sample alarms.json
Best practices
Use session_tasks.sleep() — not asyncio.sleep()
Use session_tasks.sleep() — not asyncio.sleep()
Session tasks ensure proper cleanup when the session ends.
asyncio.sleep() can leak.Keep poll intervals reasonable
Keep poll intervals reasonable
10–30 seconds is typical. For alarms, 15–30 seconds is fine.
Handle missing files gracefully
Handle missing files gracefully
The JSON file may not exist yet if
main.py hasn’t been triggered. Always check_if_file_exists() first.Use delete + write for JSON
Use delete + write for JSON
write_file() appends, which corrupts JSON. Always delete, then write the full object.Log generously
Log generously
Background daemons run silently.
editor_logging_handler is your only window into what they’re doing.Call send_interrupt_signal() before speaking
Call send_interrupt_signal() before speaking
Otherwise audio overlaps, or the system tries to transcribe your daemon’s output as user input.
Use a while True loop for sleep mode support
Use a while True loop for sleep mode support
Background daemons work even when the Personality is asleep — but only if the main function is a never-ending
while True loop.Templates & resources
| Resource | Link |
|---|---|
| Alarm Ability (Interactive Combined) | openhome-dev/abilities/templates/Alarm |
| Standalone Background Daemon | openhome-dev/abilities/templates/Background |
| SDK Reference | SDK Reference |
| Questions / support | #dev-help on Discord |
main.py and background.py to understand how they coordinate.
