Skip to main content
The alarm template demonstrates the most advanced pattern: a Skill (main.py) and Daemon (watcher.py) working together through shared file storage.

What is it?

The Alarm template shows how to coordinate a Skill and Daemon using shared JSON files. It demonstrates:
  • Skill — Parse natural language alarm requests, write to alarms.json
  • Daemon — Poll alarms.json every 5 seconds, fire alarms when target time is reached
  • Coordination — Delete-then-write pattern for JSON files to avoid corruption
  • State management — Mark alarms as “triggered” to prevent repeat firing
  • Time handling — Parse ISO8601 timestamps with timezone awareness
This is the blueprint for any ability that needs both user interaction (Skill) and continuous background monitoring (Daemon).

When to use it

Use the alarm template pattern when you’re building:
  • Timers and alarms — Set reminders, schedule events, countdown timers
  • Task schedulers — Queue work items for later execution
  • State machines — Skills set state, daemons react to state changes
  • Async workflows — Skills collect input, daemons process in background

Architecture

User says "set an alarm for 3pm"

[Skill: main.py]
  1. Parse user request with LLM
  2. Write alarm to alarms.json
  3. Speak confirmation
  4. Exit with resume_normal_flow()

[Daemon: watcher.py] (always running)
  1. Poll alarms.json every 5 seconds
  2. Check if any alarm target_time <= now
  3. Play alarm.mp3
  4. Mark alarm as "triggered"
  5. Continue polling

Complete code

import json
from datetime import datetime
from time import time
from zoneinfo import ZoneInfo

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


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

    #{{register capability}}

    # -----------------------------
    # alarms.json safe read/write
    # -----------------------------
    async def _reset_alarms_file(self, reason: str = "") -> None:
        filename = "alarms.json"
        try:
            if reason:
                self.worker.editor_logging_handler.warning(
                    f"{time()}: alarms.json reset. Reason: {reason}"
                )
            if await self.capability_worker.check_if_file_exists(filename, False):
                await self.capability_worker.delete_file(filename, False)
        except Exception as e:
            self.worker.editor_logging_handler.error(
                f"{time()}: Failed to reset alarms.json: {e}"
            )

    async def _read_alarms(self):
        filename = "alarms.json"

        if not await self.capability_worker.check_if_file_exists(filename, False):
            return []

        raw = await self.capability_worker.read_file(filename, False)
        if not (raw or "").strip():
            return []

        try:
            parsed = json.loads(raw)
            if isinstance(parsed, list):
                return parsed

            await self._reset_alarms_file("JSON is valid but not a list")
            return []
        except Exception as e:
            await self._reset_alarms_file(f"Corrupted JSON: {e}")
            return []

    async def _write_alarms(self, alarms):
        filename = "alarms.json"
        if not isinstance(alarms, list):
            alarms = []

        payload = json.dumps(alarms, ensure_ascii=False, indent=2)

        # If write_file appends, we MUST delete first to avoid: [old][new]
        await self._reset_alarms_file("Pre-write delete to avoid append-concat")

        try:
            await self.capability_worker.write_file(filename, payload, False)

            # quick verify
            verify_raw = await self.capability_worker.read_file(filename, False)
            verify_parsed = json.loads(verify_raw)
            if not isinstance(verify_parsed, list):
                await self._reset_alarms_file("Post-write verification failed (not list)")
                await self.capability_worker.write_file(filename, "[]", False)

        except Exception as e:
            await self._reset_alarms_file(f"Write failed: {e}")
            try:
                await self.capability_worker.write_file(filename, "[]", False)
            except Exception:
                pass

    # -----------------------------
    # LLM prompt
    # -----------------------------
    def _build_system_prompt(self, now, tz_name):
        return f"""
You are an alarm time parser.

Current datetime (authoritative): {now.isoformat()}
Timezone (authoritative): {tz_name}

Task:
Convert the user request into a future datetime.

Rules:
- If the user requests deleting alarms (e.g., "delete all alarms", "remove all alarms", "clear alarms"),
  respond with EXACTLY:
  DELETE_ALL_ALARMS
- If day/date is missing, ask:
  QUESTION:at what day ?
- If time is missing, ask exactly:
  QUESTION:at what time ?
- Output MUST be either:
  - DELETE_ALL_ALARMS
  - one QUESTION:... line (exactly as specified above), OR
  - valid JSON only (no extra text).
Return JSON only when complete:
{{
  "target_iso": "ISO8601 datetime with timezone offset",
  "human_time": "Friendly readable time",
  "timezone": "{tz_name}"
}}
"""

    async def first_setup(self):
        try:
            user_text = await self.capability_worker.wait_for_complete_transcription()
            original_request = user_text

            # Fast-path: if user literally says it, reset without LLM
            t0 = (user_text or "").strip().lower()
            if "delete all alarms" in t0:
                await self._reset_alarms_file("User requested delete all alarms")
                try:
                    await self.capability_worker.write_file("alarms.json", "[]", False)
                except Exception:
                    pass
                await self.capability_worker.speak("All alarms deleted.")
                return

            tz_name = self.capability_worker.get_timezone()
            try:
                tz = ZoneInfo(tz_name)
            except Exception:
                tz = ZoneInfo("UTC")

            now = datetime.now(tz=tz)
            system_prompt = self._build_system_prompt(now, tz_name)
            history = []

            for _ in range(6):
                llm_response = self.capability_worker.text_to_text_response(
                    user_text,
                    history,
                    system_prompt,
                )
                history.append({"role": "user", "content": user_text})

                if isinstance(llm_response, str):
                    if llm_response.strip() == "DELETE_ALL_ALARMS":
                        await self._reset_alarms_file("LLM delete all alarms")
                        try:
                            await self.capability_worker.write_file("alarms.json", "[]", False)
                        except Exception:
                            pass
                        await self.capability_worker.speak("All alarms deleted.")
                        return

                    if llm_response.startswith("QUESTION:"):
                        history.append({"role": "assistant", "content": llm_response})
                        question = llm_response.split("QUESTION:", 1)[1].strip()
                        user_text = await self.capability_worker.run_io_loop(question)
                        continue

                # not a question -> should be JSON
                try:
                    parsed = json.loads(llm_response)
                except Exception:
                    await self.capability_worker.speak("I couldn't understand the time. Try again.")
                    return

                if not parsed.get("target_iso"):
                    await self.capability_worker.speak("I couldn't understand the time. Try again.")
                    return

                alarm = {
                    "id": f"alarm_{int(time() * 1000)}",
                    "created_at_epoch": int(time()),
                    "timezone": parsed.get("timezone", tz_name),
                    "target_iso": parsed["target_iso"],
                    "human_time": parsed.get("human_time", parsed["target_iso"]),
                    "source_text": original_request,
                    "status": "scheduled",
                }

                alarms = await self._read_alarms()
                alarms.append(alarm)
                await self._write_alarms(alarms)

                await self.capability_worker.speak(f"Alarm set for {alarm['human_time']}.")
                return

            await self.capability_worker.speak("Too many questions. Please try setting the alarm again.")

        except Exception:
            pass
        finally:
            self.capability_worker.resume_normal_flow()

    def call(self, worker):
        try:
            worker.editor_logging_handler.info("Alarm Capability")
            self.worker = worker
            self.capability_worker = CapabilityWorker(self)
            self.worker.session_tasks.create(self.first_setup())
        except Exception as e:
            self.worker.editor_logging_handler.warning(e)

Key patterns

Coordination through shared files

The Skill and Daemon never call each other directly. They communicate through alarms.json:
[Skill writes]
alarms.json:
[
  {
    "id": "alarm_1234567890",
    "target_iso": "2026-03-04T15:00:00-08:00",
    "human_time": "3pm today",
    "status": "scheduled"
  }
]

[Daemon reads, fires alarm, writes back]
alarms.json:
[
  {
    "id": "alarm_1234567890",
    "target_iso": "2026-03-04T15:00:00-08:00",
    "human_time": "3pm today",
    "status": "triggered",
    "triggered_at_epoch": 1741047600
  }
]

Delete-then-write pattern for JSON

write_file() appends to existing files. Always delete the file first when writing JSON objects to avoid corruption like [old][new].
# CORRECT: Delete before writing
await self.capability_worker.delete_file("alarms.json", False)
await self.capability_worker.write_file(
    "alarms.json",
    json.dumps(alarms, ensure_ascii=False, indent=2),
    False
)
# WRONG: Writing without deleting first
await self.capability_worker.write_file("alarms.json", json.dumps(alarms))  # ❌ Corrupts JSON

LLM-powered time parsing

The Skill uses a conversational LLM loop to parse natural language time expressions:
for _ in range(6):  # Max 6 clarification questions
    llm_response = self.capability_worker.text_to_text_response(
        user_text,
        history,
        system_prompt,
    )
    
    if llm_response.startswith("QUESTION:"):
        # LLM needs more info
        question = llm_response.split("QUESTION:", 1)[1].strip()
        user_text = await self.capability_worker.run_io_loop(question)
        continue
    
    # LLM returned JSON with parsed time
    parsed = json.loads(llm_response)
    alarm = {
        "target_iso": parsed["target_iso"],
        "human_time": parsed["human_time"],
        "status": "scheduled"
    }
This pattern handles:
  • Relative times: “in 30 minutes”, “tomorrow at noon”
  • Absolute times: “3pm”, “March 4th at 2:30pm”
  • Natural language: “when I get home”, “next Friday morning”
  • Ambiguous input: Ask follow-up questions to clarify

Timezone-aware datetime handling

# Get user's timezone from session
tz_name = self.capability_worker.get_timezone()
tz = ZoneInfo(tz_name)

# Always use timezone-aware datetimes
now = datetime.now(tz=tz)
target_dt = datetime.fromisoformat(alarm["target_iso"])

# Compare with timezone awareness
if now >= target_dt:
    # Fire alarm

State management with status field

Prevent duplicate alarm firing by tracking status:
# Only fire alarms with status="scheduled"
for alarm in alarms:
    if alarm.get("status") != "scheduled":
        continue
    
    if now >= alarm["target_time"]:
        # Fire alarm
        await self.capability_worker.play_from_audio_file("alarm.mp3")
        
        # Mark as triggered
        alarm["status"] = "triggered"
        alarm["triggered_at_epoch"] = int(time())
        await self._write_alarms(alarms)

SDK methods used

MethodPurpose
wait_for_complete_transcription()Capture full user utterance without speaking first
text_to_text_response()Send prompts to LLM for time parsing
run_io_loop()Speak clarification question and wait for answer
speak()Confirm alarm was set
resume_normal_flow()Exit Skill after setting alarm
get_timezone()Get user’s timezone for datetime calculations
check_if_file_exists()Check if alarms.json exists before reading
read_file()Load alarms.json contents
write_file()Save updated alarms.json
delete_file()Delete alarms.json before writing (prevents append corruption)
play_from_audio_file()Play alarm sound when time is reached
session_tasks.sleep()Sleep between polling intervals (Daemon)

Real-world examples

Build on this template to create:
  • Reminder system — Set voice reminders with natural language
  • Pomodoro timer — 25-minute work intervals with break notifications
  • Medication tracker — Remind user to take pills at scheduled times
  • Workout timer — Set intervals for HIIT workouts
  • Baby monitor — Set feeding/diaper change reminders
  • Meeting alerts — Parse calendar events and fire pre-meeting reminders
  • Smart home scheduler — Queue device commands for later execution
  • Task queue — Skills add tasks, daemons process them asynchronously

Next steps

File Storage API

Complete reference for file operations

Watcher Template

Learn more about background daemons

Audio Playback

Play custom sounds and music

Ability Types

Understand Skills vs Daemons vs Local abilities

Build docs developers (and LLMs) love