How to Create Custom @skill Decorators with Schema Generation Rules for LLM Agents in DimOS

To create a custom @skill decorator in DimOS, apply the @skill annotation to a type-annotated method with a complete docstring, and the framework automatically generates a JSON schema via LangChain's tool wrapper for LLM agent consumption.

In the dimensionalOS/dimos framework, any method decorated with @skill on a Module subclass becomes an automatically discoverable tool for LLM-driven agents. When a blueprint initializes, the runtime scans loaded modules, identifies callables marked with the __skill__ attribute in dimos/agents/annotation.py, and generates strict JSON schemas using LangChain's tool wrapper and Pydantic models.

Understanding the @skill Decorator Architecture

The DimOS framework implements a three-stage pipeline for exposing Python methods as LLM-callable tools. Understanding these core components ensures your custom skills integrate correctly with the agent system.

Stage 1: Decoration. The @skill decorator in dimos/agents/annotation.py marks methods by setting func.__rpc__ = True and func.__skill__ = True on the function object. This dual-flag system enables both RPC routing and tool discovery without modifying the function signature.

Stage 2: Schema Generation. The Module.get_skills() method in dimos/core/module.py introspects all class attributes, filters for callables possessing __skill__, and invokes tool(attr).args_schema.model_json_schema(). This LangChain utility inspects type hints and docstrings to build Pydantic models, then serializes them to JSON schema strings stored in SkillInfo objects.

Stage 3: Tool Construction. The agent subsystem in dimos/agents/agent.py reconstructs LangChain StructuredTool instances from the SkillInfo objects, binding the JSON schema to the underlying method for runtime execution.

Step-by-Step: Creating a Custom Skill with Schema Generation

Step 1: Apply the @skill Decorator

Import the decorator from the annotations module and apply it to any method within a Module subclass. The decorator itself performs minimal runtime modification, deferring validation until module initialization.


# dimos/agents/annotation.py

def skill(func: F) -> F:
    func.__rpc__ = True          # enables RPC routing

    func.__skill__ = True        # marks the method for tool generation

    return func

In your implementation file, use the decorator alongside the @rpc decorator for lifecycle methods:

from dimos.agents.annotation import skill
from dimos.core.module import Module

class CustomMoveSkill(Module):
    @skill
    def move(self, speed: float, duration: float = 1.0) -> str:
        """Method implementation here."""
        pass

Step 2: Define Type Annotations and Docstrings

LangChain derives the JSON schema directly from Python type hints, converting them to Pydantic field types. Every parameter must have an explicit type annotation (str, int, float, bool, or list variants of these).

The docstring is copied verbatim into the tool's description field for the LLM. Use Google-style or NumPy-style docstrings to describe parameter purposes, as these help the LLM understand argument semantics.

@skill
def move(self, speed: float, duration: float = 1.0) -> str:
    """
    Move the robot at a constant speed.

    Args:
        speed: Linear speed in meters/second. Positive moves forward,
               negative moves backward.
        duration: How many seconds to move. Defaults to 1.0 s.
    """
    return f"Moved at {speed} m/s for {duration}s"

Missing type annotations cause the generated schema to omit those arguments entirely, preventing the LLM from supplying values. Missing docstrings raise ValueError at module startup.

Step 3: Return String Responses

The agent expects string responses for text-based interactions. Your @skill method must return a str (or a specialized artefact object implementing agent_encode for image-style outputs). Non-string return types result in generic "It has started..." messages or ignored values.

Schema Generation Rules for LLM Agents

The Module.get_skills() method enforces strict constraints when generating schemas via tool(attr).args_schema.model_json_schema(). Follow these rules to ensure successful LLM integration:

  • Docstring must exist. The docstring becomes the tool's description in the JSON schema. Without it, DimOS raises ValueError during blueprint startup and omits the skill.

  • All parameters must be type-annotated. LangChain requires type hints to construct the Pydantic args_schema. Unannotated parameters disappear from the schema, making them inaccessible to the LLM.

  • Return type must be str or an artefact object. The agent processes string responses directly. Complex return objects must implement the artefact encoding protocol.

  • Use simple scalar types only. Stick to str, int, float, bool, list[str], and list[float]. Complex nested types (dict, Optional, or custom classes) cause runtime schema generation errors in the current LangChain integration.

Complete Example: Building a Custom Move Skill

The following implementation demonstrates a valid custom skill following all schema generation rules:


# file: dimos/agents/skills/custom_move.py

from dimos.agents.annotation import skill
from dimos.core.core import rpc
from dimos.core.module import Module

class CustomMoveSkill(Module):
    """Expose basic robot movement as an LLM-callable skill."""

    @rpc
    def start(self) -> None:
        super().start()          # initialise streams, RPC, etc.

    @skill
    def move(self, speed: float, duration: float = 1.0) -> str:
        """
        Move the robot at a constant speed.

        Args:
            speed: Linear speed in meters/second. Positive moves forward,
                   negative moves backward.
            duration: How many seconds to move. Defaults to 1.0 s.
        """
        # Insert robot-specific command here

        print(f"[CustomMoveSkill] speed={speed}, duration={duration}")
        return f"Moved at {speed} m/s for {duration}s"

# Blueprint entry – discovered automatically by `dimos list`

custom_move_skill = CustomMoveSkill.blueprint

When the blueprint launches, Module.get_skills() generates the following JSON schema (simplified):

{
  "type": "object",
  "properties": {
    "speed": {"type": "number"},
    "duration": {"type": "number", "default": 1.0}
  },
  "required": ["speed"]
}

The LLM receives this schema as part of the tool definition, enabling it to generate structured calls like {"speed": 0.5, "duration": 2}.

Integrating Custom Skills into Blueprints

After creating your skill module, expose it to the agent by importing the blueprint variable into your target agent configuration:

from dimos.agents.skills.custom_move import custom_move_skill
from dimos.core.blueprint import autoconnect

my_blueprint = autoconnect(
    robot_modules,
    custom_move_skill,          # added skill module

    agent,
)

Validate the integration by running the agent tests:

pytest dimos/agents/test_agent.py -v

This verifies that get_skills() discovers your method and that agent.py successfully reconstructs the StructuredTool without schema validation errors.

Summary

  • Apply @skill from dimos/agents/annotation.py to methods in Module subclasses to mark them for LLM exposure.
  • Provide complete type annotations for all parameters so Module.get_skills() in dimos/core/module.py can generate valid JSON schemas via LangChain's tool wrapper.
  • Write detailed docstrings that become the tool descriptions consumed by the LLM.
  • Return strings from skill methods to ensure proper agent response handling.
  • Import the skill's blueprint into your agent configuration file to register it with the runtime.

Frequently Asked Questions

What happens if I forget to type-annotate a parameter in a @skill method?

If a parameter lacks a type annotation, LangChain's tool() wrapper cannot generate a corresponding field in the Pydantic args_schema. Consequently, the parameter disappears from the JSON schema in SkillInfo, and the LLM has no mechanism to supply that argument during tool invocation. Always annotate parameters with simple types like str, float, or int.

Can I use complex nested types like dict or Optional in @skill parameters?

No. The current DimOS implementation in dimos/core/module.py uses LangChain's default schema generator, which supports only simple scalar types (str, int, float, bool) and basic list variants (list[str], list[float]). Complex nested types, Optional wrappers, or custom class annotations cause runtime errors during model_json_schema() serialization.

How does DimOS convert the generated schema into a callable tool for the LLM?

The agent subsystem in dimos/agents/agent.py receives the SkillInfo object containing the JSON schema string. It deserializes the schema with json.loads(skill.args_schema) and constructs a LangChain StructuredTool, binding the schema to a wrapped function that routes calls back to your module method. This StructuredTool is then passed to the LLM agent for invocation.

Where should I place new skill modules in the DimOS codebase?

Create new skill modules under dimos/agents/skills/ for general capabilities, or within robot-specific packages for hardware-tied functionality. Ensure the file exports a blueprint variable (e.g., custom_move_skill = CustomMoveSkill.blueprint) so the autoconnect system in your agent configuration can discover and load the module during blueprint initialization.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →