Skip to main content
A skill can run other skills. You declare them the same way you declare a camera or the base — with a type annotation — and then call them like methods. This is how you build a high-level behavior out of capabilities you (and the platform) already have, without reimplementing navigation, manipulation, or speech.

Declaring a sub-skill

Import the skill’s class and annotate an attribute with it:
Calling one:
  • Blocks until the sub-skill finishes — no callbacks, no polling.
  • Raises SkillFailed if it fails, and SkillCancelled if a Stop lands, so stop-on-first-failure is just Python.
  • Returns a SkillOutput.message, .data, .status, .ok.
  • Shows up as its own step in the app, so a composed routine is legible while it runs.
Parameters are keyword arguments, taken straight from the sub-skill’s execute() signature. timeout= bounds how long a call may run:

Declaring a trained policy

Physical skills — trained ACT policies and recorded replays — are data on disk, not classes you can import from your own code. They get the same declaration block and the same call shape, in either of two spellings. The robot generates a typed reference for each of its physical skills, so the readable spelling is:
If the policy isn’t on this robot yet — you’re writing against one that’s still training — declare it by ID instead:
Either way an unknown ID fails at run start, not halfway through the routine.

A worked example

What each piece is doing

Sub-skills sit in the declaration block with everything else. battery: Battery and move: MoveStraight are declared identically — the type says what it is. A code skill and a trained policy are indistinguishable at the call site, which is the point: the caller doesn’t need to know how the sub-skill is implemented. Read structured output from .data. MoveStraight and TurnInPlace return a SkillOutput carrying a typed payload, so out.data.turned_degrees reports how far the robot actually turned. A skill that returns a bare message has .data as None. Handle failures with SkillFailed. A failing sub-skill raises, so recovery is a plain try/except:
Don’t catch it and the exception propagates, failing your skill too — which is usually what you want for a step that must succeed. Cancellation propagates for free. Every child shares the parent’s cancel latch, so one Stop unwinds the whole routine: the running child raises SkillCancelled, the base is braked, the arm halted, and the run reports CANCELLED. Never catch SkillCancelled to keep going. Everything else is a normal skill. DemoRoutine is an ordinary code-defined skill — it declares state, persists counters with self.storage, speaks with self.say(..., wait=True), and returns its message.
Because each sub-skill call is its own step, a composed routine is easy to follow in the app and easy to interrupt — the agent can cancel between steps or in the middle of a long-running one.

Dynamic IDs

When the skill to run is only known at runtime, use the invoker directly. It takes an ID string and returns a SkillOutput rather than raising on failure, so you check .ok yourself:
A cancelled routine still raises SkillCancelled out of run() — you never handle CANCELLED by hand. Prefer the declared form everywhere else: a class reference is checked by your editor, an ID string isn’t.

Overriding a sub-skill

Composition runs the class you declared, so specializing a routine means subclassing it and re-declaring the attribute with your own class — never shadowing by name:
A skill that declares itself, directly or through a descendant, is rejected when the run is wired — before execute() starts — rather than recursing on the robot.

When to compose vs. write from scratch

Composing is also how you turn a one-off demo into a reusable capability: give the class a name and a docstring, and the agent can trigger the whole chain with a single skill call.