Skip to main content
You can ship an OpenHome Ability (and a companion dashboard) in an afternoon if you brief the LLM well. This page collects the prompts, patterns, and guardrails that make the difference between a plausible demo and something you actually leave running. Two surfaces covered: Both share the same Live Editor patterns and guardrails.

The rule of two tabs

When building, keep two browser tabs open side by side. You need to see both sides of the pipe at once.
  • Tab 1: OpenHome Live Editor → Ability logs
  • Tab 2: Your terminal (Claude + CLI) or Replit (server logs)
If a POST fails, you see it in both within a second. Fastest feedback loop voice AI has ever had.

Vibe coding Abilities with Claude

The OpenHome CLI was designed so an AI coding agent — Claude, Cursor, anything that supports tool use — can drive the build loop end-to-end.

Setup

1

Install the CLI

npx openhome-cli
Paste your OpenHome API key when prompted.
2

Point Claude at the project

In your terminal, launch Claude Code (or your preferred agent) in the directory where your Ability lives. Claude has full tool-use access to the CLI.
3

Give Claude context

Share the SDK Reference and Simple Abilities Cookbook up front. These two documents are the entire operating context for Ability development.

The context you must give Claude

Before asking Claude to write any Ability code, paste or reference these four things:
  1. SDK Reference — every method, every sandbox rule, every prompt pattern
  2. Simple Abilities Cookbook — the 100+ examples so Claude understands the Ability shape
  3. Voice-First Best Practices — the UX rules that distinguish a demo from a product
  4. What you want to build — one paragraph, plain English, with concrete triggers and behaviors
Without all four, Claude will generate plausible-looking but wrong code — the worst outcome.

Scaffolding prompt for a new Ability

Copy and adapt:
I want to build an OpenHome Ability.

CONTEXT (read first):
- SDK Reference: https://docs.openhome.com/api-sdk/sdk-reference
- Cookbook: https://docs.openhome.com/guides/building-abilities/cookbook
- Voice-First rules: https://docs.openhome.com/guides/best-practices/voice-first

THE ABILITY:
Name: Meeting Notes
Category: skill (hotword-triggered)
Trigger words: "take notes", "start meeting notes", "note this meeting"
Behavior:
  - On trigger, start recording with self.capability_worker.start_audio_recording()
  - Loop listening for "meeting finished" via user_response()
  - When finished, stop recording, get audio bytes
  - POST to Deepgram with diarize=true, utterances=true, smart_format=true
  - LLM-summarize the diarized transcript into action items + speaker summary
  - Speak a 2-sentence recap, write the full notes to a persistent file
  - resume_normal_flow() on every exit path

CONSTRAINTS:
- Follow every sandbox rule in the SDK Reference
- Use session_tasks.create(), not asyncio.create_task
- No print(), use editor_logging_handler
- Keep speak() calls to 1–2 sentences

Architect the solution first (files, loops, patterns). Then write the code.

Two-shot development

The worst failure of AI-assisted coding is generating a plausible implementation of the wrong design. Two-shot it:
1

Shot 1 — architecture in prose

Before any code: “Walk me through the architecture. What files, what loops, what prompts, what storage patterns? Flag any tradeoffs.”
2

Shot 2 — full code

Only after you’ve agreed on the shape: “Now write the full implementation. Follow every sandbox rule.”
Talking through the shape before typing is the single highest-leverage habit in Ability development.

Sandbox rules to pre-brief

Claude doesn’t know OpenHome’s sandbox until you tell it. The ones it gets wrong most often:
  • No asyncio.create_task — use self.worker.session_tasks.create()
  • No asyncio.sleep — use self.worker.session_tasks.sleep()
  • No print() — use self.worker.editor_logging_handler.info() / .error()
  • No raw open() — use the file storage API (write_file, read_file, check_if_file_exists)
  • No top-level import os, import signal, import json outside the register block
  • #{{register capability}} is a literal comment tag, not a function call
  • Every main.py exit path must call resume_normal_flow() — even exception handlers and timeouts
Pre-brief Claude on these once, and it’ll stop making the same mistakes.

Log-driven iteration

When something breaks, paste the live Live Editor logs directly into Claude and ask it to diagnose.
  • Don’t summarize
  • Don’t filter
  • Don’t try to identify the problem yourself first
The logs are the ground truth. Let the model see them raw.

Deploy and test in a loop

The CLI’s superpower is that Claude can build → deploy → test without you clicking anything:
1. Claude writes main.py
2. Claude: `openhome deploy ability .`  (CLI packages and uploads)
3. Claude: `openhome chat --agent <your agent>`  (chat with the Ability)
4. Claude reads the response, adjusts main.py, loops
This is the productivity unlock. See OpenHome CLI for the full command surface.

Vibe coding dashboards with the Replit Agent

Replit’s Agent is good, but only as good as the brief you give it. Be explicit about three things: what is POSTing in, what the frontend will poll, and that state lives in memory.

Master prompt for the backend

Copy this, adapt the <ability> placeholder, paste it into the Replit Agent:
Build a Flask server that receives POSTs from an external Python client
(an OpenHome ambient voice ability) and serves a polling-based dashboard.

ENDPOINTS TO ACCEPT (all JSON POST):
- POST /api/<ability>/session_start — fires once when ability starts
- POST /api/<ability>/update — fast cycle updates every 10s
  Contains: tone, scene, speaker_state, running_log, cycle_id, elapsed
- POST /api/<ability>/deep — slow cycle updates every 60s
  Contains: deep_insight, emotional_arc, notable_moments
- POST /api/<ability>/heartbeat — lightweight keepalive every 10s when idle
- POST /api/<ability>/session_end — fires once when ability stops

READ ENDPOINTS (GET, returns JSON):
- GET /api/state — returns the current consolidated state object
- GET /api/history — returns the last N cycle snapshots

STORAGE:
- In-memory Python dict. No database. Last-write-wins per session_id.
- Keep the last 100 cycle snapshots in a collections.deque.
- On cold start, state is empty; the ability will re-flood within one cycle.

FRONTEND:
- Single index.html, vanilla JS, polls /api/state every 2 seconds.
- Tabs: LIVE, TIMELINE, SPEAKERS, INSIGHTS, RAW DATA.
- Use Tailwind via CDN. Dark theme. No frameworks.
- Each tab reads from the same STATE object; no extra API calls per tab.

CORS:
- Allow all origins. The POSTs come from a sandboxed environment
  with no fixed IP address.

ROBUSTNESS:
- Every endpoint must accept unexpected fields without erroring.
- Log the full payload to console on receipt for debugging.
- Return {"ok": true} on success. Never 500 on a missing field.
- Nested dicts should merge, not overwrite; lists should replace.

Bind to host="0.0.0.0", port=8080.

Frontend-specific prompt

Run this as a second prompt after the backend is working. Mixing backend and frontend in one prompt produces muddled output.
Build a single index.html dashboard that polls GET /api/state every 2s
and renders the current state of an ambient voice ability.

LAYOUT:
- Fixed top header: session ID, elapsed time, connection status dot
  (green = updated in last 15s, amber = 15–60s stale, red = >60s)
- Five tabs below: LIVE, TIMELINE, SPEAKERS, INSIGHTS, RAW DATA
- Tab content fills the rest of the viewport

LIVE TAB:
- Large card showing current room energy / dominant tone
- Grid of speaker cards (one per speaker in speaker_registry)
- Each card: name/id, gender+age, current tone, energy bar, vocal quality

TIMELINE TAB:
- Vertical list of running_log entries, newest at top
- Each entry: timestamp badge, observation text
- Subtle color coding by entry type (arrival, topic shift, tone shift)

SPEAKERS TAB:
- Expanded speaker profiles with voice_signature, emotional_arc, undercurrent
- "Present" / "Left the room" badges

INSIGHTS TAB:
- The latest deep_insight payload rendered as a narrative card

RAW DATA TAB:
- Pretty-printed JSON of the full STATE object, in a scrollable pre block

STYLE:
- Tailwind via CDN. Dark theme (slate-900 background, slate-100 text).
- Accent: indigo-400. Use rounded-2xl cards with subtle ring-1 ring-slate-700.
- No frameworks. No build step. One file.
See Build a Companion Dashboard for the full architecture this dashboard is serving.

OpenHome Live Editor patterns

The Live Editor is where most of your Ability development happens. A few patterns will save you hours.

Prompts at the top of the file

Every configurable LLM prompt, every URL, every constant belongs at the top of main.py as a named constant. When you want to fork an Ability for a new persona, you change the constants — not the logic. This rule is absolute.
# =============================================================================
# PROMPTS
# =============================================================================

SYSTEM_PROMPT = """..."""
INTENT_PROMPT = """..."""
FAREWELL_PROMPT = """..."""

# =============================================================================
# CONSTANTS
# =============================================================================

DASHBOARD_URL = "https://..."
HEARTBEAT_INTERVAL = 10
MAX_SPEAKERS = 4

Use the register-capability tag, not a function call

Inside your MatchingCapability class, use the literal comment tag #{{register capability}} — not register_capability(). The platform rewrites the tag at load time. Writing the function call manually throws sandbox errors.
class YourCapability(MatchingCapability):
    worker: AgentWorker = None
    capability_worker: CapabilityWorker = None

    #{{register capability}}

Log generously in dev, sparely in production

During development, log every cycle, every POST, every API response. editor_logging_handler is your only window into the Ability’s behavior. Before shipping to the Marketplace, trim log lines to the bare minimum — one line per cycle, plus errors. A silent Ability in production is a good Ability.

Shared patterns and guardrails

These apply whether you’re vibe coding the Ability or the dashboard.

Forbidden imports and patterns

Don’tUse instead
import os, import signal, raw open()Platform helpers — play_from_audio_file(), file storage API
import redis, RedisHandler, connection_manager(direct infra access is blocked)
print()self.worker.editor_logging_handler.info() / .error()
asyncio.sleep, asyncio.create_tasksession_tasks.sleep(), session_tasks.create()
Full list: SDK Reference → Sandbox rules.

The session_tasks pattern

All background work — heartbeats, parallel LLM calls, dashboard POSTs — goes through self.worker.session_tasks.create(coro). This guarantees clean shutdown when the session ends.
# Good
self.worker.session_tasks.create(self.heartbeat_loop())

# Bad — blocked by sandbox
asyncio.create_task(self.heartbeat_loop())

The two feedback loops

LayerFeedback mechanism
Ability code in the Live Editoreditor_logging_handler.info() — visible live in the Editor’s log pane
Dashboard POSTsReplit’s server logs — every request dumped to console
OpenHome CLILocal terminal output + openhome-cli log lines
Use whichever surface is live for the layer you’re debugging. Don’t try to guess.

Never send raw audio to a dashboard

Keep audio local to the Ability. Send transcripts, summaries, tone descriptors, metadata — never PCM or WAV bytes.
  • 1 minute of 16kHz 16-bit audio = 2 megabytes
  • 1 minute of transcript = 2 kilobytes

See also