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:
❌ Silent failure
✅ Graceful error handling
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.
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
✅ Clear instructions
❌ No guidance
await self .capability_worker.speak(
"Let's start. Say 'stop' anytime to exit."
)
Logging Best Practices
What to Log
User interactions
self .worker.editor_logging_handler.info( f "User said: { user_input } " )
External API calls
self .worker.editor_logging_handler.info( f "Calling API: { url } " )
self .worker.editor_logging_handler.info( f "Response: { response.status_code } " )
State changes
self .worker.editor_logging_handler.info( f "Alarm set for { target_time } " )
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
❌ Don't log secrets
✅ Log safely
self .worker.editor_logging_handler.info( f "API key: { API_KEY } " )
Security Best Practices
Never Hardcode Secrets
❌ Hardcoded credentials
✅ Use environment config
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
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:
❌ Load entire file
✅ Stream
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
Empty input
What happens if user says nothing?
Invalid input
What if user provides malformed data?
API failures
What if the external API is down?
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
❌ Will corrupt JSON
✅ Correct pattern
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
Start with a template
Use the closest template to your use case. Don’t start from scratch.
Add logging early
Add logs before testing. They’re essential for debugging.
Test in Live Editor
Iterate quickly with Live Editor before zipping and uploading.
Handle errors
Wrap main logic in try-except-finally blocks.
Test edge cases
Try empty input, invalid data, API failures, timeouts.
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