Composing skills is a pre-release feature. The innate.skills calling
convention shown here is not part of a stable OS build yet, so the import path,
the result shape, and SkillFailed behavior may change without notice. Treat
this page as a preview, not a stable reference.
A skill can call other skills. Import them from innate.skills and call them like
ordinary functions — the runtime runs each one to completion, then returns control to
your code. This lets you build a high-level behavior out of the capabilities you (and
the platform) already have, without re-implementing navigation, manipulation, or speech.
The idea
Every shipped and custom skill is exposed as a callable in innate.skills. Inside your
own execute(), calling one:
- Blocks until the sub-skill finishes — no callbacks or polling.
- Raises
SkillFailed if the sub-skill fails, so you can try/except around it.
- Returns a result whose
.data holds the sub-skill’s typed output.
- Shows up as its own step in the app timeline, so a composed routine is legible
while it runs.
Learned policies and scripted skills share the exact same call shape — pick_socks()
(an ACT policy) is called the same way as turn_in_place() (scripted). The caller
doesn’t need to know how the sub-skill is implemented.
A worked example
This is run_routine_demo, a shipped skill you can read in full at
~/innate-os/workspace/innate_skills/run_routine_demo.py. It talks, emotes, shuffles
back and forth, turns, and attempts a learned pick — chaining six different skills.
from innate import RobotState, RobotStateType, Skill, SkillFailed, SkillResult
from innate.skills import (
arm_zero_position,
head_emotion,
move_straight,
pick_socks,
turn_in_place,
)
class RunRoutineDemo(Skill):
"""Demo of a chained routine: skills are imported functions, calls block,
failures raise SkillFailed, and each call is its own step in the app."""
battery = RobotState(RobotStateType.LAST_BATTERY)
@property
def name(self):
return "run_routine_demo"
def guidelines(self):
return (
"Run the demo routine: talk, emote, shuffle, turn, and try to pick a "
"sock. Use when the user asks for the demo."
)
def execute(self):
runs = self.storage.get("runs", 0) + 1
self.storage["runs"] = runs
arm_zero_position()
head_emotion(emotion="excited")
self.say(f"Demo number {runs}. Watch this.", wait=True)
for distance in (0.2, -0.2):
move_straight(distance=distance)
turn = turn_in_place(angle_degrees=90)
self.say(f"I turned {turn.data.turned_degrees:.0f} degrees.")
turn_in_place(angle_degrees=-90, timeout=20)
try:
pick_socks(timeout=60) # learned policy, same call shape
except SkillFailed:
head_emotion(emotion="disappointed")
self.say("No socks today.")
if self.battery:
self.say(f"Battery at {self.battery['percentage']:.0%}.")
head_emotion(emotion="proud")
self.say("All done!")
return "Demo complete", SkillResult.SUCCESS
What each piece is doing
Import skills as functions. from innate.skills import arm_zero_position, … pulls
in the skills you want to chain. Anything installed on the robot — shipped or custom —
is available here.
Pass parameters as keyword arguments. A sub-skill’s parameters are just function
arguments: move_straight(distance=distance), turn_in_place(angle_degrees=90). Pass
timeout= to bound how long a call may run (turn_in_place(angle_degrees=-90, timeout=20)).
Read a sub-skill’s output from .data. Skills that return structured results expose
them on the result object. Here turn = turn_in_place(...) and then
turn.data.turned_degrees reports how far the robot actually turned.
Handle failures with SkillFailed. A sub-skill that fails raises SkillFailed
rather than returning an error tuple. Wrap fallible calls in try/except to recover —
the demo shrugs off a missed pick and keeps going:
try:
pick_socks(timeout=60)
except SkillFailed:
head_emotion(emotion="disappointed")
self.say("No socks today.")
If you don’t catch it, the exception propagates and your composing skill fails too —
which is often exactly what you want for a step that must succeed.
Everything else is a normal skill. The composing skill is still an ordinary
code-defined skill: it declares
RobotState dependencies
(battery), persists data across runs with self.storage, speaks with self.say(..., wait=True), and returns a (message, SkillResult) tuple.
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 the routine between steps, or right in
the middle of a long-running sub-skill.
When to compose vs. write from scratch
| Reach for composition when… | Write a flat skill when… |
|---|
| The building blocks already exist as skills | You need low-level interface control the sub-skills don’t expose |
| You want each step visible and separately cancellable | The steps are tightly coupled and shouldn’t be interrupted mid-sequence |
| You’re mixing scripted skills and learned policies | Everything is a few interface calls with no reusable sub-behavior |
Composing skills is also how you turn a one-off demo into a reusable capability: name the
routine, give it guidelines(), and the agent can trigger the whole chain with a single
skill call.