Skip to main content
Build better abilities with these best practices for voice UX, error handling, security, and performance.

Voice UX Best Practices

Keep Responses Short

Voice interfaces require different UX than screens.
await self.capability_worker.speak(
    "Okay, I've successfully processed your request and sent the email "
    "to [email protected] with the subject line 'Meeting Tomorrow' and "
    "the body text you provided. The email was sent at 3:45 PM Eastern "
    "Time from your configured email account. Would you like me to do "
    "anything else for you today?"
)
Golden rule: If it takes more than 5 seconds to speak, it’s too long.

Confirm Before Destructive Actions

Always ask for confirmation before:
  • Sending emails or messages
  • Deleting data
  • Making purchases
  • Controlling smart home devices
  • Running system commands
confirmed = await self.capability_worker.run_confirmation_loop(
    f"Send email to {recipient} with subject '{subject}'?"
)

if confirmed:
    # Perform action
    await self.capability_worker.speak("Email sent.")
else:
    await self.capability_worker.speak("Cancelled.")

Provide Clear Exit Paths

For looped abilities, always tell users how to exit:
await self.capability_worker.speak(
    "I'm ready to help. Say 'stop' when you're done."
)

while True:
    user_input = await self.capability_worker.user_response()
    
    if any(w in user_input.lower() for w in EXIT_WORDS):
        await self.capability_worker.speak("Goodbye!")
        break
    
    # Process input
Never create a loop without a clear exit condition. Users will feel trapped.

Use Natural Language

Speak like a human, not a robot.
❌ Robotic✅ Natural
”Processing request. Please wait.""Let me check on that."
"Action completed successfully.""Done!"
"Error code 404: Resource not found.""I couldn’t find that."
"Would you like to proceed with the operation?""Should I go ahead?”

Handle Silence Gracefully

Users might not respond immediately.
try:
    user_input = await asyncio.wait_for(
        self.capability_worker.user_response(),
        timeout=10.0
    )
except asyncio.TimeoutError:
    await self.capability_worker.speak(
        "I didn't catch that. Let's try again later."
    )
    self.capability_worker.resume_normal_flow()
    return

Error Handling Best Practices

Never Fail Silently

Always catch exceptions and inform the user:
async def run(self):
    result = await self.fetch_data()  # Might throw exception
    await self.capability_worker.speak(result)
    self.capability_worker.resume_normal_flow()
Use try-except-finally to ensure resume_normal_flow() always gets called.

Log Everything Important

Logs are your debugging lifeline:
self.worker.editor_logging_handler.info(f"User request: {user_input}")
self.worker.editor_logging_handler.info(f"API URL: {api_url}")

try:
    response = requests.get(api_url)
    self.worker.editor_logging_handler.info(
        f"API response: {response.status_code}"
    )
except Exception as e:
    self.worker.editor_logging_handler.error(f"API error: {str(e)}")
Log levels:
  • .info() - Normal operation
  • .warning() - Recoverable issues
  • .error() - Failures
Never use print(). It doesn’t show up in logs. Always use editor_logging_handler.

Validate User Input

Don’t assume user input is clean:
user_input = await self.capability_worker.user_response()

if not user_input or len(user_input.strip()) == 0:
    await self.capability_worker.speak("I didn't catch that. Can you repeat?")
    return

# Sanitize input for API calls
import re
email = re.match(r"[^@]+@[^@]+\.[^@]+", user_input)
if not email:
    await self.capability_worker.speak("That doesn't look like a valid email.")
    return

Using resume_normal_flow() Correctly

Skills MUST Call It

async def run(self):
    await self.capability_worker.speak("Task complete.")
    self.capability_worker.resume_normal_flow()  # Required!
Without resume_normal_flow(), the agent will never respond to user input again. The speaker goes silent.

Daemons MUST NOT Call It

async def first_function(self):
    while True:
        # Background work
        await self.worker.session_tasks.sleep(5.0)
    # No resume_normal_flow() here!
Daemons run independently in the background. They don’t control conversation flow.

Call It in finally Block

Ensure it gets called even if errors occur:
async def run(self):
    try:
        # Your logic
        await self.capability_worker.speak("Done!")
    except Exception as e:
        self.worker.editor_logging_handler.error(f"Error: {e}")
        await self.capability_worker.speak("Something went wrong.")
    finally:
        self.capability_worker.resume_normal_flow()  # Always called

Exit Handling in Loops

Define Exit Words

EXIT_WORDS = {"stop", "exit", "quit", "done", "cancel", "bye", "goodbye", "leave"}

while True:
    user_input = await self.capability_worker.user_response()
    
    if any(word in user_input.lower() for word in EXIT_WORDS):
        await self.capability_worker.speak("Goodbye!")
        break
    
    # Continue processing

Tell Users How to Exit

await self.capability_worker.speak(
    "Let's start. Say 'stop' anytime to exit."
)

Logging Best Practices

What to Log

1

User interactions

self.worker.editor_logging_handler.info(f"User said: {user_input}")
2

External API calls

self.worker.editor_logging_handler.info(f"Calling API: {url}")
self.worker.editor_logging_handler.info(f"Response: {response.status_code}")
3

State changes

self.worker.editor_logging_handler.info(f"Alarm set for {target_time}")
4

Errors

self.worker.editor_logging_handler.error(f"Failed to parse JSON: {e}")

What NOT to Log

Never log sensitive data:
  • Passwords
  • API keys
  • Personal information (emails, phone numbers)
  • Private conversations
self.worker.editor_logging_handler.info(f"API key: {API_KEY}")

Security Best Practices

Never Hardcode Secrets

API_KEY = "sk-1234567890abcdef"
DB_PASSWORD = "mypassword123"

Validate Commands Before Execution

When using exec_local_command(), always validate:
command = await self.capability_worker.user_response()
await self.capability_worker.exec_local_command(command)  # User could say "rm -rf /"

Sanitize File Paths

import os

def is_safe_path(path: str) -> bool:
    # Prevent directory traversal
    if ".." in path:
        return False
    
    # Only allow specific directories
    allowed_dirs = ["/home/user/documents", "/tmp"]
    abs_path = os.path.abspath(path)
    
    return any(abs_path.startswith(d) for d in allowed_dirs)

if not is_safe_path(user_provided_path):
    await self.capability_worker.speak("Invalid file path.")
    return

Performance Best Practices

Use Async Properly

import time

time.sleep(5)  # Blocks entire event loop
await self.capability_worker.speak("Done")

Stream Large Responses

Don’t load everything into memory:
data = requests.get(large_file_url).content  # Could be GBs

Clean Up Resources

async def run(self):
    file_handle = None
    
    try:
        file_handle = open("data.txt", "r")
        data = file_handle.read()
        # Process data
    except Exception as e:
        self.worker.editor_logging_handler.error(f"Error: {e}")
    finally:
        if file_handle:
            file_handle.close()
        self.capability_worker.resume_normal_flow()

Testing Best Practices

Test Edge Cases

1

Empty input

What happens if user says nothing?
2

Invalid input

What if user provides malformed data?
3

API failures

What if the external API is down?
4

Timeout

What if operations take too long?

Use Logging for Debugging

self.worker.editor_logging_handler.info(f"Starting ability with input: {user_input}")
self.worker.editor_logging_handler.info(f"Step 1 complete")
self.worker.editor_logging_handler.info(f"API returned: {response}")
self.worker.editor_logging_handler.info(f"Exiting ability")

File Operations Best Practices

Always Delete Before Writing JSON

await self.capability_worker.write_file("data.json", json.dumps(data))
# write_file() appends - calling twice creates: [old][new]
write_file() appends by default. Always delete first when writing structured data.

Handle Missing Files

if not await self.capability_worker.check_if_file_exists("config.json", False):
    # File doesn't exist - use defaults
    config = {"default": "value"}
else:
    raw = await self.capability_worker.read_file("config.json", False)
    try:
        config = json.loads(raw)
    except json.JSONDecodeError:
        # Corrupted file - use defaults
        config = {"default": "value"}

Development Workflow

1

Start with a template

Use the closest template to your use case. Don’t start from scratch.
2

Add logging early

Add logs before testing. They’re essential for debugging.
3

Test in Live Editor

Iterate quickly with Live Editor before zipping and uploading.
4

Handle errors

Wrap main logic in try-except-finally blocks.
5

Test edge cases

Try empty input, invalid data, API failures, timeouts.
6

Commit and monitor

Commit changes and monitor production logs for issues.

Quick Reference

✅ Do

  • Keep voice responses under 5 seconds
  • Confirm before destructive actions
  • Always call resume_normal_flow() in Skills
  • Use try-except-finally for error handling
  • Log user interactions and errors
  • Validate user input
  • Use session_tasks.sleep() instead of asyncio.sleep()
  • Delete files before writing JSON
  • Test edge cases thoroughly

❌ Don’t

  • Use verbose responses
  • Forget resume_normal_flow() in Skills
  • Call resume_normal_flow() in daemons
  • Use print() for logging
  • Hardcode secrets or credentials
  • Run unvalidated commands with exec_local_command()
  • Use asyncio.sleep() in long-running operations
  • Log sensitive data
  • Fail silently without user feedback
  • Create loops without exit conditions

Next Steps

Templates

Explore available templates

Common Patterns

Reusable code patterns

Testing Guide

Test your abilities

SDK Reference

Complete API documentation

Build docs developers (and LLMs) love