class FindTheDog becomes find_the_dog. Defining the class is what registers it — no name property, no registration call, no file scanning.
The class docstring is the guidelines. It is the text the agent reads to decide when to call the skill, so write it for the agent, not for a reviewer. A skill states its purpose exactly once.
The execute() signature is the parameter schema. The agent sees execute(self, max_turns: int = 8) and knows what it may pass. Type hints and defaults are part of the contract, so annotate them.
Declaring what you need
Every feed — interfaces, cameras, robot state, other skills — is declared the same way: a bare type annotation on the class. The type identifies the feed, so there is nothing to wire in__init__.
execute() starts and fails the run up front if none arrives, so a declared feed is never None inside execute() and needs no guard. The wait is bounded per feed — cameras get 3 s because they start with the run, the battery gets 6 s because it only publishes every few seconds, everything else 2 s.
| None makes it best effort. battery: Battery | None is injected when a value is available and left None otherwise, and the run starts either way.
Reading a feed you didn’t declare raises immediately with the annotation to add, and because the annotations are real types your editor flags a typo before you ship.
The four interfaces:
See Robot state for the full list of state feeds, Navigation interfaces and Body control interfaces for what the interfaces do, and Composing skills for declaring other skills.
Returning a result
execute() returns the message the agent will read:
None is also success. To fail, call self.fail(message) — it raises, so there is no “and then return” to forget:
SkillOutput:
out.message, out.data, out.status and out.ok. SkillOutput also takes image=jpeg_bytes to hand the agent a picture as evidence alongside the message.
The 0.6.x
return "message", SkillResult.SUCCESS tuple still works and still normalizes correctly, but it is deprecated — new skills should return the message and call self.fail().Cancellation is the framework’s job
Write the loop as if cancel didn’t exist. Every blocking framework call raisesSkillCancelled the moment a Stop lands, the base is braked and the arm halted automatically, and the run reports CANCELLED — you don’t catch it, and you don’t need a cancel() method.
Cleanup belongs in a
try/finally inside execute(). self.on_cancel(hook) exists only to forward a cancel to an external action goal — braking the base is already automatic. Overriding cancel() is rare enough that the base class handles it for you.
Progress, speech and storage
Feedback streams a progress line to whoever launched the skill — the agent reads it live and can act on it, including cancelling you or triggering something else:wait=True blocks until playback ends:
Expensive objects: @resource
When a skill owns something costly to build, declare it with @resource. It is constructed on first access, cached for the run, and torn down at the end while the interfaces are still alive:
self expecting it to survive — that’s what self.storage is for.
Next steps
- Navigation interfaces — driving and rotating the base
- Body control interfaces — the arm SDK, the gripper, the head
- Robot state — cameras, odometry, map, lidar, battery
- Composing skills — declaring and calling other skills, including trained policies
- Physical skill examples — full-body behaviors combining navigation and manipulation
- External services & APIs — APIs, email, web services

