Skip to main content

Overview

Every OpenHome Ability falls into one of three categories, each with distinct triggers, lifecycles, and use cases.

Skills

Triggered by user or brain routing. Runs once and exits.

Background Daemons

Runs automatically on session start. Loops continuously.

Local

Runs on-device hardware. Bypasses cloud sandbox.

Skills

Skills are the workhorse of OpenHome Abilities. A user says a hotword (or the brain’s routing LLM invokes it), the ability runs, does its thing, and hands control back via resume_normal_flow().

When to Use Skills

  • User-initiated actions (“play music”, “check weather”)
  • API integrations (search web, fetch data)
  • Interactive conversations (quizzes, advice)
  • One-time tasks (send email, set alarm)

Lifecycle

Entry File

main.py — Contains your main logic

Example: Basic Advisor Skill

main.py
import json
from src.agent.capability import MatchingCapability
from src.main import AgentWorker
from src.agent.capability_worker import CapabilityWorker

class BasicTemplateCapability(MatchingCapability):
    worker: AgentWorker = None
    capability_worker: CapabilityWorker = None

    #{{register capability}}

    async def run(self):
        # Greet and ask for input
        await self.capability_worker.speak("Hi! How can I help you today?")
        user_input = await self.capability_worker.user_response()
        
        # Generate response with LLM
        response = self.capability_worker.text_to_text_response(
            f"Give a short, helpful response to: {user_input}"
        )
        
        # Speak result
        await self.capability_worker.speak(response)
        
        # Exit cleanly
        self.capability_worker.resume_normal_flow()

    def call(self, worker: AgentWorker):
        self.worker = worker
        self.capability_worker = CapabilityWorker(self)
        self.worker.session_tasks.create(self.run())
Key Pattern: Skills always end with resume_normal_flow() to return control to the Agent.

Common Skill Patterns

PatternDescriptionExample
Fire-and-forgetTrigger → Execute → ExitSend email, play sound
API CallFetch data → Speak result → ExitWeather, web search
InteractiveLoop with listen → respond → exit commandQuiz game, advisor
LLM TranslatorVoice → LLM → Command → ExecuteTerminal control, smart home

Background Daemons

Background Daemons start automatically when a user connects and run in a while True loop for the entire session. No hotword needed. They can monitor conversations, poll APIs, watch for time-based events, and interrupt the main flow when something fires.

When to Use Background Daemons

  • Time-based triggers (alarms, reminders, Pomodoro timer)
  • Continuous monitoring (conversation logger, baby monitor)
  • Periodic checks (stock prices, news feed)
  • Ambient intelligence (context accumulation, user profiling)

Lifecycle

Entry File

watcher.py — Contains the daemon loop
Daemons can be standalone (watcher.py only) or combined with a Skill (main.py + watcher.py).

Example: Standalone Watcher Daemon

watcher.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 WatcherCapabilityWatcher(MatchingCapability):
    worker: AgentWorker = None
    capability_worker: CapabilityWorker = None
    background_daemon_mode: bool = False
    
    #{{register capability}}

    async def first_function(self):
        self.worker.editor_logging_handler.info(f"{time()}: Watcher Called")
        
        while True:
            # Monitor conversation history
            message_history = self.capability_worker.get_full_message_history()[-10:]
            
            for message in message_history:
                role = message.get("role", "")
                content = message.get("content", "")
                self.worker.editor_logging_handler.info(f"Role: {role}, Message: {content}")
            
            # Sleep for 20 seconds before next check
            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.worker)
        self.worker.session_tasks.create(self.first_function())

Example: Combined Skill + Daemon (Alarm)

The Alarm ability uses both main.py (to set alarms) and watcher.py (to fire them): main.py — Parses user input and writes alarm to alarms.json
watcher.py — Polls alarms.json every 5 seconds and fires alarms when due
They coordinate through shared file storage, not direct function calls.
watcher.py (simplified)
async def first_function(self):
    while True:
        # Read alarms from file
        alarms = await self._read_alarms_safe()
        
        # Check if any are due
        for alarm in alarms:
            if alarm_is_due(alarm):
                # Play alarm sound
                await self.capability_worker.play_from_audio_file("alarm.mp3")
                
                # Mark as triggered
                await self._mark_alarm_triggered(alarm)
        
        # Sleep for 5 seconds
        await self.worker.session_tasks.sleep(5.0)
Key Pattern: Daemons use session_tasks.sleep() (not asyncio.sleep()) and never call resume_normal_flow().

Critical Rules for Daemons

RuleWhy
Use session_tasks.sleep()Ensures proper cleanup when session ends
No resume_normal_flow()Daemons don’t own the conversation
Call send_interrupt_signal() before speakingPrevents audio overlap
Delete then write for JSON fileswrite_file() appends — always delete first

Daemon vs Skill: When to Choose

Use a Skill if…Use a Daemon if…
User explicitly triggers itIt should run automatically
One-time actionContinuous monitoring
Immediate responseTime-based or event-driven
No background stateNeeds to watch conversations

Local Abilities

Local Abilities run directly on Raspberry Pi hardware, bypassing the cloud sandbox entirely. They can use unrestricted Python packages, GPIO pins, and local models.
Local Abilities are currently under active development. Full DevKit SDK coming soon.

When to Use Local

  • Direct hardware control (GPIO, sensors, LEDs)
  • Local-only processing (privacy-sensitive data)
  • Unrestricted package requirements
  • Low-latency operations
  • Desktop terminal control

Entry File

main.py — Uses exec_local_command() to bridge cloud ↔ device

Example: Mac Terminal Control

main.py
import json
from src.agent.capability import MatchingCapability
from src.main import AgentWorker
from src.agent.capability_worker import CapabilityWorker

class LocalCapability(MatchingCapability):
    worker: AgentWorker = None
    capability_worker: CapabilityWorker = None
    
    #{{register capability}}

    async def first_function(self):
        # Listen for user request
        user_inquiry = await self.capability_worker.wait_for_complete_transcription()
        
        # LLM translates speech → terminal command
        system_prompt = """You are a Mac terminal command generator. 
        Convert user requests into valid Mac terminal commands.
        Respond ONLY with the command, nothing else."""
        
        terminal_command = self.capability_worker.text_to_text_response(
            user_inquiry,
            history=[],
            system_prompt=system_prompt,
        )
        
        # Execute on local machine
        await self.capability_worker.speak(f"Running: {terminal_command}")
        response = await self.capability_worker.exec_local_command(terminal_command)
        
        # Speak result
        await self.capability_worker.speak(f"Done. {response['data']}")
        self.capability_worker.resume_normal_flow()

    def call(self, worker: AgentWorker):
        self.worker = worker
        self.capability_worker = CapabilityWorker(self)
        self.worker.session_tasks.create(self.first_function())
Key Method: exec_local_command() bridges the cloud sandbox to your local device via WebSocket.

Sandbox Escape via OpenClaw

OpenHome Abilities run in a restricted cloud sandbox. To escape it, use OpenClaw — a desktop AI agent with 2,800+ community skills:
user_inquiry = await self.capability_worker.wait_for_complete_transcription()
response = await self.capability_worker.exec_local_command(user_inquiry)
await self.capability_worker.speak(response["data"])
The speaker becomes a voice interface for your entire desktop.

Comparison Table

FeatureSkillBackground DaemonLocal
TriggerUser hotword or brain routingAutomatic on session startUser or system
LifecycleRuns once, exitsRuns continuously in loopRuns on-device
Entry Filemain.pywatcher.pymain.py
Use CaseInteractive tasksMonitoring, timersHardware, terminal
Calls resume_normal_flow()✅ Yes❌ No✅ Yes
Runs in cloud sandbox✅ Yes✅ Yes❌ No (local device)
Can interrupt conversation✅ Yes✅ Yes (via send_interrupt_signal())✅ Yes

Next Steps

Trigger Words

Learn how to configure trigger words for your Skills

Templates

Explore starter templates for each ability type

SDK Reference

Full documentation of all available SDK methods

Build Your First Ability

Follow the 5-minute quickstart guide

Build docs developers (and LLMs) love