Skip to main content

Introduction

Custom Abilities are the cornerstone of extending OpenHome’s functionality. They allow developers to:
  • Add personalized features to AI agents.
  • Integrate third-party APIs for dynamic interactions.
  • Customize logic for enhanced user engagement.
This guide walks you through:
  • Structuring and registering an Ability.
  • Using CapabilityWorker for seamless I/O management.
  • Examples showcasing how to create powerful custom Abilities.

Adding an Ability

File Structure

Each Ability resides in its folder and requires a main.py file to define the logic.

Example File: main.py

Here’s a basic template for building a new Ability:

Key Components

  • #{{register_capability}}: is essential.
  • call: Executes the Ability’s logic when triggered.

Custom API Keys (Third-Party Services)

If your Ability needs credentials for external services (for example OpenAI, SendGrid, or Twilio), configure those keys in the dashboard and read them at runtime using get_api_keys("key_name").

Setup Flow

Keys live on your account, not in the Ability. You create one in the dashboard with its value, and the Ability reads it back by name at runtime.

1. Create the key in Settings

Go to Settings → API Keys, find Third Party Keys, and click + Add. The dialog takes three things:
  • Key name: the exact name your code will ask for, for example n2yo_api_key.
  • Key value: the secret itself. It is stored masked and is never shown again in full.
  • Link to provider: the URL where the key comes from, so anyone using the Ability knows where to get their own.
Add Third Party Key dialog with fields for key name, key value, and a link to the provider Add Third Party Key dialog with fields for key name, key value, and a link to the provider Each saved key is listed with its provider link and a Linked Abilities field showing which Abilities use it.
The name must match what your code asks for, character for character. A mismatched name is the most common reason get_api_keys() returns nothing.

2. Read the value at runtime

Put the name in a constant at the top of main.py so it is easy to find and change, as the API template does:
Check for several keys at once and fail with a spoken message rather than a silent error:

3. Tell your users which keys to add

Nothing prompts a user for keys automatically, so an Ability that needs one has to say so. List each key in the Ability’s README.md, giving the exact name and where to get the value. The Orbit Ability does this well:
Go to n2yo.com/api, create a free account, copy your API Key, and add it in your OpenHome Settings → API Keys as n2yo_api_key (name must match exactly).
Then degrade gracefully. Treat a missing key as an expected state, not a crash: fall back to cached or demo data where you can, and otherwise say what is missing out loud.
Never hardcode secrets in Ability code. Always read them at runtime with get_api_keys(). Community Abilities are published as source, so anything hardcoded is public.

Reading Linked Account Tokens with get_token()

For providers that support OAuth-based account linking, OpenHome stores the user’s access token on the user’s behalf when they connect the account from Settings → Linked Accounts in the Dashboard. Inside an Ability, use get_token() to retrieve that token and call the provider’s API on behalf of the user without handling the OAuth flow yourself. This is different from Custom API Keys above: API keys are values the user pastes in for arbitrary third-party services, while get_token() returns an OAuth access token managed by OpenHome for a supported provider.

Signature

get_token() is synchronous, do not await it.

get_token() parameters

Returns

  • A non-empty str access token if the user has linked the requested provider.
  • None or an empty value if the provider is not linked for the user. Always guard the return value before using it.

Example: token lookup with a connect-account fallback

The recommended pattern is to read the token at the start of run() and, if it is missing, speak a clear instruction telling the user where to connect the account, then exit cleanly.
Substitute the provider key ("slack", "discord", "microsoft", "tesla") and the spoken instruction to match the account your Ability needs.

Reference Abilities Using get_token()

The following community Abilities use get_token("google") in production. Review them for end-to-end patterns covering token retrieval, API calls, and error handling: For the user-facing account-linking flow, see Linked Accounts in the Dashboard reference.

Making HTTP Requests

When your Ability calls an API, always route the request through self.worker.session_tasks. This lets the SDK manage the request for you: it paces outbound calls and smooths out latency, so a slow or heavy API doesn’t block your Agent or affect other Abilities. Do not call requests, httpx, or aiohttp directly, use the matching session_tasks helper instead. This applies to code you write by hand and to code generated by an AI assistant. The simplest option is get(). It works just like the requests library: give it a URL and read the response with .json().
This is all most Abilities need. get() accepts the usual arguments (params, headers, timeout, auth) and returns a standard requests.Response.

Which helper should I use?

Most Abilities can just use get(). You only need the others if you want an async call or prefer a different library. When you do, the choice depends on how your method is written:
  • Normal def method → use a sync helper and call it directly, with no await: get() or httpx_get().
  • async def method (for example, a background-daemon loop) → use an async helper and await it: get_async(), httpx_get_async(), or aiohttp_get_async(). Async helpers don’t freeze the rest of your Ability while the request is running, so prefer them whenever your method is already async.
requests has no async version of its own, so get_async() is there to give you a requests-style request that you can await. Every library has the same helpers, so pick the one you’re comfortable with:

Examples

With requests and httpx, you read the response directly with .json(), .text, and .status_code. With aiohttp, reading the body is also asynchronous, so use await resp.json() and await resp.text().

Understanding CapabilityWorker

The CapabilityWorker class simplifies I/O interactions, enabling:
  • Speech synthesis: Using text-to-speech (TTS).
  • Listening for user input: Capturing and processing responses.
  • Running interaction loops: Supporting conversational flows.

CapabilityWorker Quick Reference

Use these functions directly on self.capability_worker.

Conversation

Speak text to the user using the configured TTS.
Wait for a single user reply and return the transcription.
Speak once, then wait for one reply.
Ask a yes/no question and return True or False.
Wait for the user’s full transcription.
Get full session message history.
Get the current user’s timezone.
Get the user’s approximate location from their IP. Returns a dict with city, region, country, postal, timezone, ip, loc, and more.
Get linked account access token.
Get a custom API key value by key name.
Append context/instructions to the active Agent prompt.
Return control back to normal Agent flow when your ability is done.

Text Generation

Return plain text from the model (no speech).
Alternate text generation routed through OpenRouter.
Web-search-backed short answer.
Tool-calling flow.

File Helpers

Read a file.
Write content to a file.
Delete a file.
Check if a file exists.
List user data files.
Note: Storage Scope Usage
  • Use in_ability_directory=False for persistent user-level storage shared across abilities.
  • Use in_ability_directory=True for ability-scoped data that should remain isolated within the ability session.

Context Storage (Key-Value)

Audio and Streaming

Reach for play_from_audio_file() or play_audio() for short audio bundled with your Ability, such as a chime, a sound effect, or a recorded line. Reach for stream_music_from_url() below for the minutes-long, interruptible case.

Music Playback

For audio that runs for minutes and can be interrupted by voice, use stream_music_from_url. It plays a progressive mp3 link, blocks until playback genuinely ends, and reports why it ended. Your Ability owns only the branch on outcome. The audio pipeline, the device buffer, music mode, and the recovery afterwards are all inside the call.
Play a track:
Returns exactly {"outcome", "position", "sent"}. position is measured at the halt instant rather than after cleanup, so it is an honest number for logs. Resuming needs nothing from it.
While a stream is live the Ability is in music mode: it receives no transcriptions, and must not call speak() or run_io_loop(). Speak before the call or after it returns. However playback ends, including the error path, the client is audible again and the platform is listening when the call returns.
No duration is asked for, and no position is tracked. The engine reads the byte rate off the mp3’s own frame header, so a track with missing or wrong metadata still plays and resumes correctly.
Do not assume a stream url handed back by an authenticated API is self-signed. SoundCloud’s is not: without the header its CDN answers 401, and 429 once a few tracks have been tried in a row. If every track comes back unplayable, check auth first.
A resume is the same call again with the same url, which makes playback a loop. Four rules live in your code, not in the function:
  1. Resolve the url once per track, not per pass. A fresh url reads as a new track and restarts it from the top. This is the one cost of the url being the resume signal: if a signed link expires while the user sits paused, re-resolving restarts the track.
  2. Only "paused" loops. finished, stopped, error, and unplayable all leave.
  3. An unclear pause reply should stop, not resume. A user who asked for silence must not get the track back because a reply did not parse.
  4. announce on the first pass only, or every resume re-announces the title.
To end playback from your own code (a timer, a device event), set the same events the platform sets, from a task other than the one awaiting the stream:
See Example 4: Music Playback for the complete Ability with the loop already written, or start from the music-template directly. Only two functions are yours to fill in:
Everything else in that file, the pause menu, the outcome branch, the resume_normal_flow() on exit, can be left alone.

WebSocket / Device Actions

Using Specific Voice IDs for Text-to-Speech

The CapabilityWorker class supports the use of specific Voice IDs for text-to-speech (TTS) functionality. This allows you to customize the voice used for speech synthesis by specifying a Voice ID from the provided list.

Available Voice IDs

You can use any of the following Voice IDs for TTS:

text_to_speech Function

The text_to_speech function converts the provided text into speech using the specified Voice ID and streams it to the user via WebSocket.

text_to_speech parameters

  • text (str): The text to be converted into speech.
  • voice_id (str): The Voice ID to be used for speech synthesis.

Exiting an Ability

An Ability returns control to the Agent when it finishes its work by calling resume_normal_flow(). Beyond that normal completion, there are two ways to leave a Skill Ability on demand: the built-in exit phrases the SDK recognizes, and exit handling you add in your own code.

Built-in exit phrases

While a Skill Ability is active, the SDK recognizes a fixed set of spoken exit phrases. Speaking any of them ends the current Ability and returns control to the Agent’s conversation flow. This is the same outcome as calling resume_normal_flow(), but initiated by the user rather than by the Ability. This is built in and requires no additional code, so a user always has a way out of an Ability, including one that is mid-conversation or waiting for input. The recognized exit phrases are:
  • “open home exit”
  • “openhome exit”
  • “open home quit”
  • “openhome quit”
  • “open home, quit”
  • “open home, exit”
  • “exit open home”
  • “quit open home”
  • “exit, open home”
  • “quit, open home”
Note: Built-in exit phrases apply to Skill Abilities.

Handling exit in your own code

You can also end an Ability from your own code. Detect an exit condition in the user’s input, such as your own stop words, and call resume_normal_flow() to return control to the Agent.

Advanced CapabilityWorker Functions

Audio Processing Functions

The CapabilityWorker provides comprehensive audio handling capabilities:
  • play_audio: Play audio content directly or file objects
  • play_from_audio_file: Play audio files stored in the capability directory
  • send_audio_data_in_stream: Stream processed audio data over WebSocket

Text Generation Functions

Multiple options for text generation:
  • text_to_text_response: Standard text generation with history and system prompts
  • generate_ttt_using_openrouter: Alternate text generation using OpenRouter
  • llm_search: Web-search-backed short answer
  • llm_tools: Tool-calling with the model

Streaming and Communication

Advanced communication features:
  • stream_init and stream_end: Manage audio streaming sessions
  • stream_music_from_url: Play a progressive mp3 link, blocking until playback genuinely ends, and report why it ended. See Music Playback for the full reference and Example 4 for a complete Ability.
  • send_data_over_websocket: Send custom data over WebSocket
  • send_interrupt_signal: Interrupt ongoing output and hand control back to user input
  • send_agent_message_without_audio: Send a text reply without TTS
  • send_devkit_action: Trigger a DevKit action
  • send_devkit_mqtt_action: Trigger a DevKit MQTT action to control a smart device. See Controlling MQTT Devices for the full reference and an example Ability.

Context and Session Helpers

  • get_timezone: Read the current user’s timezone for local-time-aware behavior
  • get_token: Read the access token of a user-linked provider account ("google", "slack", "discord", "microsoft", "tesla"). See Reading Linked Account Tokens with get_token() for the full reference and a run() example with a fallback message when the account isn’t connected.
  • get_api_keys: Read custom API key values from Settings → API Keys by key name
  • get_full_message_history: Read full session message history for context-aware responses
  • update_personality_agent_prompt: Append context/instructions to the Agent personality prompt
  • create_key / update_key / delete_key: Manage structured key-value context storage
  • get_single_key / get_all_keys: Read one or all stored context entries

Recording and Local Audio

  • get_audio_recording: Load the latest user recording bytes
  • get_audio_recording_length: Duration in seconds for the latest recording
  • flush_audio_recording: Clear the current recording before a new capture
  • play_from_audio_file: Play an audio file stored in the Ability directory

Session Task Utilities (replace raw asyncio usage)

To ensure Abilities run within the agent’s managed lifecycle, avoid using raw asyncio helpers directly.
  • Use self.worker.session_tasks.sleep(seconds: float) instead of asyncio.sleep(...):
These helpers ensure proper cancellation, cleanup, and session scoping.

Background Daemon Entry Point (background.py)

Background daemons run automatically when a session starts. Use a separate background.py file with this entry signature:
Daemon rules:
  • Keep daemon logic inside a continuous while True loop.
  • Use await self.worker.session_tasks.sleep(...) between cycles.
  • Do not call resume_normal_flow() inside daemon loops.
  • Call await self.capability_worker.send_interrupt_signal() before daemon speech/audio.

Example 1: Basic Capability

This Ability creates a daily life advisor that:
  1. Asks the user for a problem: Initiates a conversation to gather user input.
  2. Provides advice: Offers a solution based on user input.
  3. Collects feedback: Captures user satisfaction with the advice.

Example 1 code

Key Functions

  • speak: Introduces the advisor and provides the solution.
  • user_response: Captures user input (e.g., their problem).
  • run_io_loop: Combines speaking the solution and listening for feedback.
  • resume_normal_flow: Resumes the agent’s default workflow after interaction.

Example 2: Weather Capability

This Ability integrates a weather API to fetch and share weather updates based on user-provided locations.

Example 2 code

Example 2 key features

  • External API Call: Fetches real-time weather data.
  • Geolocation: Validates and processes user-provided locations.
  • Error Handling: Provides meaningful feedback for invalid inputs.

Allowed/Disallowed Libraries and Patterns

The following imports, keywords, and patterns are not allowed in Abilities. Use the safe alternatives.

Blocked Imports and Keywords

Guidance:
  • Avoid direct storage/infra access. Use platform-provided helpers within CapabilityWorker/worker or request an API if needed.
  • Use the provided logging (editor_logging_handler) instead of prints.
  • For files, prefer platform abstractions and per-user capability folders, and ask for an approved helper if you need persistent storage.
  • For HTTP GET requests, always use the self.worker.session_tasks helpers (get, get_async, httpx_get, httpx_get_async, aiohttp_get_async). Never call requests, httpx, or aiohttp directly. See Making HTTP Requests.

Security Guidance

Avoid insecure or unsafe patterns such as runtime assert checks, exec() of dynamic code, binding servers to all interfaces, hardcoded secrets, swallowing exceptions, insecure deserialization (pickle/dill/shelve/marshal), weak hashes like MD5, or weak cipher modes (e.g., ECB). If you have a special case, request approval and an approved wrapper/utility.

Conclusion

Building Abilities in OpenHome empowers developers to create custom functionalities for AI agents. With the examples like the Basic Advisor and Weather Capability, you can:
  • Core Communication: Use speak, run_io_loop, and user_response for basic interactions.
  • Advanced Audio: Play custom audio files, and stream audio data.
  • Text Generation: Leverage multiple text-to-text options with history and system prompts.
  • Voice Customization: Use specific voice IDs for varied and engaging responses.
  • External APIs: Integrate third-party services for dynamic functionality.
The examples demonstrate everything from basic conversational flows to advanced audio processing and device control. The CapabilityWorker provides all the tools needed to create sophisticated, interactive Abilities.
Start creating innovative Abilities and push the boundaries of voice AI with OpenHome! 🎉
Note: For GET requests to third-party APIs, use self.worker.session_tasks.get() (see Making HTTP Requests). It takes the same arguments as the requests module’s get() and returns the same response. For other request types, use the requests module and avoid other libraries. If you need a library that isn’t available, you can request that we add it.

Example 3: Read/Write File (from example_main.py)

This is the simplest pattern for per-user storage.

Example 4: Music Playback

stream_music_from_url() plays a track and blocks until playback genuinely ends, then reports why in result["outcome"]. Only "paused" continues the loop, and a resume is the same call again with the same url. See the Music Playback reference above for the full parameter and outcome tables.

Example 4 key features

  • One await covers the whole track. The audio pipeline, the device buffer, music mode and the recovery afterwards are all inside the call, so speak() works on the very next line no matter how playback ended.
  • No duration, no position, no byte offset. The engine reads the byte rate off the mp3’s own frame header, so a track with missing or wrong metadata still plays and resumes correctly.
  • The URL is resolved once per track, outside the loop: handing the same url back is what resumes a pause, so a fresh url would restart the track instead.
  • auth is positional, so it cannot be forgotten. Pass "" for a host that wants no Authorization header.
  • An unclear pause reply stops. A user who asked for silence must not get the track back because a reply didn’t parse.
While a stream is live the Ability is in music mode: it receives no transcriptions, and must not call speak() or run_io_loop(). Speak before the call or after it returns.