Skip to main content

OpenHome Ability Templates

Five templates. Four ability categories. Unlimited possibilities.
OpenHome ability templates are starter blueprints. They are intentionally minimal and focused on runtime architecture, not polished UX.

What OpenHome Actually Is

OpenHome runs AI agents that can trigger Python abilities. Those abilities can:
  • control local tools through bridge methods
  • call APIs and external services
  • process ambient audio in loops
  • store and share state over time
  • run continuously as background daemons
LLMs are central in this model. They route, transform, and summarize, while abilities execute concrete actions.

Ability Categories

CategoryTriggerLifecycleEntry File
SkillUser hotwordRuns once, exitsmain.py
Brain SkillsAgent brain routingRuns on demand when brain delegates workmain.py
Background DaemonAutomatic on session startRuns continuously until session endbackground.py
LocalUser or systemRuns on-device hardwareDevKit SDK
Notes:
  • Brain Skills templates are still being finalized.
  • Local category is under development.

Background Daemon Entry Contract

For daemon templates, background.py must be named exactly background.py.
def call(self, worker, 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.background_loop())

How To Use Templates In Dashboard

  1. Click Create on https://app.openhome.com/dashboard/home.
  2. Select Agent Ability.
  3. Fill the Create Ability form and choose any category for your ability.
  4. Fill Ability Behavior, then choose the template you want.
  5. Click Save Ability.
Create Ability Page
Template Selection

Template Directory

templates/

├── basic-template/        ← Minimal Skill skeleton
├── api-template/          ← Call an external REST API
├── loop-template/         ← Multi-turn looping Skill

├── SendEmail/             ← Fire-and-forget action
├── Local/        ← LLM-to-local-command translation
├── OpenClaw/     ← Route request to OpenClaw

├── Background/               ← Standalone background daemon
├── Alarm/                 ← Skill + Background daemon combo

└── ReadWriteFile/         ← Shared file storage / IPC pattern

Template README Sources

Start Here Templates

basic-template (Skill, minimal)

Purpose: absolute minimum lifecycle implementation.
async def run(self):
    await self.capability_worker.speak("Hi! How can I help you today?")
    user_input = await self.capability_worker.user_response()
    response = self.capability_worker.text_to_text_response(
        f"Give a short, helpful response to: {user_input}"
    )
    await self.capability_worker.run_io_loop(response + " Are you satisfied with the response?")
    await self.capability_worker.speak("Thank you for using the advisor. Goodbye!")
    self.capability_worker.resume_normal_flow()

api-template (Skill, external API)

Purpose: ask user input, call API, summarize result, exit cleanly.
async def run(self):
    await self.capability_worker.speak("Sure! What would you like me to look up?")
    user_input = await self.capability_worker.user_response()
    result = await self.fetch_data(user_input)
    if result:
        spoken = self.capability_worker.text_to_text_response(
            f"Summarize this data in one short sentence for voice: {result}"
        )
        await self.capability_worker.speak(spoken)
    else:
        await self.capability_worker.speak("Sorry, I couldn't get that information right now.")
    self.capability_worker.resume_normal_flow()

loop-template (Skill, long-running loop)

Purpose: interactive multi-turn skill with explicit exit words.
EXIT_WORDS = {"stop", "exit", "quit", "done", "cancel", "bye", "goodbye", "leave"}

async def run(self):
    await self.capability_worker.speak("I'm ready to help. Ask me anything, or say stop.")
    while True:
        user_input = await self.capability_worker.user_response()
        if not user_input:
            continue
        if any(word in user_input.lower() for word in EXIT_WORDS):
            await self.capability_worker.speak("Goodbye!")
            break
        response = self.capability_worker.text_to_text_response(
            f"Respond in one short sentence: {user_input}"
        )
        await self.capability_worker.speak(response)
    self.capability_worker.resume_normal_flow()

The Five Core Templates

1. SendEmail (Skill · Fire-and-forget)

What it demonstrates:
  • one trigger, one action, one status response
  • synchronous SDK call in async flow
  • required handoff to resume_normal_flow()
async def email_sender(self):
    status = self.capability_worker.send_email(
        host="smtp.gmail.com",
        port=465,
        sender_email="test@gmail.com",
        sender_password="app-password",
        receiver_email="receiver_test@gmail.com",
        cc_emails=[],
        subject="Test Email",
        body="Hello from OpenHome!",
        attachment_paths=["testfile.txt"],
    )
    await self.capability_worker.speak(
        "Email has been sent successfully." if status else "Failed to send email"
    )
    self.capability_worker.resume_normal_flow()
Production upgrades:
  • collect recipient/body from user, do not hardcode
  • add confirmation step before send
  • secure credential handling

2. OpenHome-local (Skill · LLM-as-translator)

What it demonstrates:
  • user speech to terminal command generation
  • local execution bridge with exec_local_command()
  • second LLM pass to explain execution result
user_inquiry = await self.capability_worker.wait_for_complete_transcription()
terminal_command = self.capability_worker.text_to_text_response(
    user_inquiry, [], self.get_system_prompt()
).strip()
await self.capability_worker.speak(f"Running command: {terminal_command}")
response = await self.capability_worker.exec_local_command(terminal_command)
result = self.capability_worker.text_to_text_response(
    f"check if the command successfully ran? response is: {response}",
    [{"role": "user", "content": user_inquiry}],
    "Explain success/failure in simple spoken language.",
)
await self.capability_worker.speak(result)
self.capability_worker.resume_normal_flow()
Production upgrades:
  • command allowlist / denylist
  • confirmation before high-risk commands
  • timeout and error boundaries

3. openclaw-template (Skill · Sandbox escape)

What it demonstrates:
  • pass-through request routing to OpenClaw
  • no terminal generation layer, direct local AI routing
user_inquiry = await self.capability_worker.wait_for_complete_transcription()
await self.capability_worker.speak("Sending Inquiry to OpenClaw")
response = await self.capability_worker.exec_local_command(user_inquiry)
await self.capability_worker.speak(response["data"])
self.capability_worker.resume_normal_flow()
Production upgrades:
  • robust response validation
  • fallback for unmatched tools
  • timeout + retry strategy

4. Background + Alarm (Background daemon pattern)

What it demonstrates:
  • continuous background loop with session_tasks.sleep()
  • reading session history / file state periodically
  • skill + daemon coordination through shared storage
Background loop pattern:
async def first_function(self):
    while True:
        history = self.capability_worker.get_full_message_history()[-10:]
        for message in history:
            self.worker.editor_logging_handler.info(
                f"Role: {message.get('role','')}, Message: {message.get('content','')}"
            )
        await self.worker.session_tasks.sleep(20.0)
Alarm background fire pattern:
if now >= target_dt:
    await self.capability_worker.send_interrupt_signal()
    await self.capability_worker.play_from_audio_file("alarm.mp3")
    await self._mark_alarm_triggered(alarms, alarm_id)
Critical daemon rules:
  • use session_tasks.sleep(), not asyncio.sleep()
  • do not call resume_normal_flow() in daemon loops
  • interrupt before daemon speech/audio when needed
  • persist JSON safely (see ReadWriteFile rules)
  • background file name must be exactly background.py

5. loop-template a.k.a. Log-My-Life pattern (Skill · Ambient observer)

What it demonstrates:
  • long-running session loop
  • periodic capture/process/respond
  • explicit phrase-based exit
Core ambient loop shape:
while self.is_running:
    self.capability_worker.start_audio_recording()
    exit_requested = await self.wait_for_interval_or_exit()
    self.capability_worker.stop_audio_recording()
    audio_bytes = self.capability_worker.get_audio_recording()
    await self.process_chunk(audio_bytes, recording_length)
    if exit_requested:
        self.is_running = False
Important audio detail:
  • get_audio_recording() may return cumulative recording data.
  • If chunking externally, track prior byte offset and slice new data before transcription/analysis.

Utility Pattern: ReadWriteFile

Purpose: safe shared-state and IPC pattern across Skill and Daemon.
if await self.capability_worker.check_if_file_exists("temp_data.txt", in_ability_directory=False):
    await self.capability_worker.write_file(
        "temp_data.txt",
        f"\n{time()}: {user_response}",
        in_ability_directory=False
    )
else:
    await self.capability_worker.write_file(
        "temp_data.txt",
        f"{time()}: {user_response}",
        in_ability_directory=False
    )
file_data = await self.capability_worker.read_file("temp_data.txt", in_ability_directory=False)
For JSON files:
  • always use delete-then-write when replacing full file content
  • append mode is still good for .txt and .log activity streams

Utility Pattern: .md Context Injection

Purpose: feed ambient context into the Agent prompt through persistent markdown files.
content = "## Emotional State\n- Current: frustrated (confidence: 0.87)\n"
exists = await self.capability_worker.check_if_file_exists("audio_emotion.md", in_ability_directory=False)
if exists:
    await self.capability_worker.delete_file("audio_emotion.md", in_ability_directory=False)
await self.capability_worker.write_file("audio_emotion.md", content, in_ability_directory=False)
Rules:
  • only persistent .md files are injected into Agent context
  • reserve user_profile.md and user_summary.md for platform memory background ownership
  • keep each injected .md file short and current-state focused
  • expect ~60-90 seconds before a newly written .md file is reflected in responses
See: Agent Memory & Context Injection

Utility Pattern: Key-Value Context Storage

Purpose: structured state for preferences, conversation workflows, feature flags, and cache metadata. Notes:
  • Key-value methods are synchronous (do not await).
  • Store JSON dictionaries (dict) as values.
existing = self.capability_worker.get_single_key("conversation_456_state")
if existing:
    self.capability_worker.update_key(
        "conversation_456_state",
        {"step": "confirmed", "intent": "book_flight", "destination": "Dubai"},
    )
else:
    self.capability_worker.create_key(
        "conversation_456_state",
        {"step": "awaiting_confirmation", "intent": "book_flight", "destination": "Dubai"},
    )
Related methods:
  • create_key(key, value)
  • update_key(key, value)
  • delete_key(key)
  • get_all_keys()
  • get_single_key(key)
  • missing-key-safe pattern: read with get_single_key() before update_key(), create when absent

How Templates Map To Ability Types

TemplateTypeKey SDK Methods
SendEmailSkillsend_email(), speak(), resume_normal_flow()
OpenHome-localSkillwait_for_complete_transcription(), text_to_text_response(), exec_local_command()
openclaw-templateSkillwait_for_complete_transcription(), exec_local_command(), speak()
BackgroundBackground Daemonget_full_message_history(), session_tasks.sleep()
AlarmSkill + Daemonread_file(), write_file(), send_interrupt_signal(), play_from_audio_file(), session_tasks.sleep()
loop-templateSkill (long-running)start_audio_recording(), stop_audio_recording(), get_audio_recording(), text_to_text_response()
ReadWriteFileUtility / IPCcheck_if_file_exists(), read_file(), write_file(), delete_file()
Context StorageUtility / Statecreate_key(), update_key(), get_single_key(), get_all_keys(), delete_key()
basic-templateSkillspeak(), user_response(), run_io_loop(), resume_normal_flow()
api-templateSkilluser_response(), text_to_text_response(), resume_normal_flow()

Critical Technical Rules

RuleWhy
Call resume_normal_flow() on every Skill exit pathReturns control to the main agent flow
Do not use resume_normal_flow() inside daemon loopsDaemons are independent long-running tasks
Use call(self, worker, background_daemon_mode) in background.pyEnsures daemon startup contract is correct
Prefer session_tasks.sleep() over asyncio.sleep()Ensures proper cleanup on session end
Treat JSON writes carefullyDefault write_file mode appends and can corrupt JSON
Use persistent .md files for ambient prompt contextMemory background injects user-level markdown into Agent prompt
Use key-value context storage for structured statecreate_key/update_key/get_single_key reduce JSON file bookkeeping
Validate local command executionPrevent unsafe/destructive operations

What You Can Build From These

  • voice-composed email flows
  • local machine operator abilities
  • OpenClaw orchestration flows
  • background profilers, summarizers, and schedulers
  • ambient capture and extraction assistants
These templates are the foundation layer. Start with the closest pattern, keep lifecycle rules strict, and then harden for production.