What to keep in mind
Talking to an external service is a more deterministic domain than acting in the physical world, but it comes with its own constraints:- Protocol-based: follow defined APIs and standards
- Atomic: many operations cannot be cancelled once started
- Reliable: once working, behavior is consistent
- Network-dependent: must handle connectivity issues
Built-in examples
SendEmail
Sends email notifications, typically for alerts or status updates.SendPictureViaEmail
Sends an email with the robot’s current camera view attached.image: MainImage makes the runtime wait
for a real frame and fail the run up front if none arrives, so execute() never has to
handle a missing picture.
RetrieveEmails
Fetches recent emails from the configured account.Building a service skill
Template
execute(), not in an __init__: skill instances are built per
run, and a constructor that raises takes the whole skill off the roster instead of
failing one run with a message the agent can read.
Worked example: RetrieveEmails
A complete, runnable custom skill that fetches your latest Gmail messages over IMAP. Save it as~/innate-os/workspace/custom_skills/retrieve_emails.py on the robot. The class name is the skill name, so this is local/retrieve_emails.
Best practices
Authentication- Store credentials in environment variables or a secret manager
- Never hardcode passwords or API keys
- Fail the run with a clear message when a credential is missing, so the agent can say so
self.fail() rather than returning an error — it raises, so there is no path where a
failure silently reads as success:
- Always set explicit timeouts on network calls
- A blocking network call is not a cancel point — a Stop can’t interrupt it, so keep the timeout short enough that the robot stays responsive
- Design operations to be safely retryable where possible
- Consider partial failure scenarios
Reading robot state
A service skill often wants to send something the robot can see or know. Declare it the same way as any other feed:Cancellation
Many service operations are atomic and can’t be meaningfully interrupted. That’s fine — you don’t write anything for it. The framework latches the cancel, and the run reportsCANCELLED once your call returns; a network request in flight simply finishes first.
If a request is long enough that this matters, break the work into steps and call
self.check_cancelled() between them so a Stop lands promptly.
