Skip to main content
A code-defined skill is a Python class that implements a robot behavior with explicit logic. One rule covers everything it consumes: annotate it.
Three things fall out of that class, and there is no boilerplate for any of them: The class name is the skill name. 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__.
A plain annotation is guaranteed. The runtime waits for the first value before 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:
Returning None is also success. To fail, call self.fail(message) — it raises, so there is no “and then return” to forget:
To attach a structured payload for a skill that calls yours, return a SkillOutput:
A caller then reads 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 raises SkillCancelled 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.
In skill code, use self.sleep(seconds). Never time.sleep(seconds).self.sleep wakes and raises the moment a Stop lands; time.sleep blocks to completion, so a skill that uses it keeps running — and keeps the robot moving — after the user pressed Stop. Sleeping is the only cancel point a loop needs.
time itself is fine for measuringtime.time() and time.monotonic() for deadlines and elapsed checks. The rule is only about blocking.
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:
Speech is fire-and-forget by default; wait=True blocks until playback ends:
Storage is a per-skill key-value store that survives restarts:

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:
Skill instances are per-run: constructed when the run starts, disposed when it ends. Don’t stash state on self expecting it to survive — that’s what self.storage is for.

Next steps