Skip to main content
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 skillsYou need low-level interface control the sub-skills don’t expose
You want each step visible and separately cancellableThe steps are tightly coupled and shouldn’t be interrupted mid-sequence
You’re mixing scripted skills and learned policiesEverything 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.