Skills
The Skills API is experimental. APIs and behavior may still change in future releases.
Skills is a mechanism for equipping an LLM with reusable, self-contained behavioral instructions. A skill bundles a name, a short description, and a body of instructions (its content), together with optional resources (e.g., references, assets, templates, etc.). The LLM loads a skill on demand, keeping the initial context small and only pulling in the detailed instructions when they are actually needed.
Skills are designed according to the Agent Skills specification.
Creating Skills
From the File System
Typically, each skill lives in its own directory containing a SKILL.md file.
The file must start with a YAML front matter block that declares the skill's name and description.
Everything below the front matter becomes the skill's content — the instructions given to the LLM
when it activates the skill.
skills/
├── docx/
│ ├── SKILL.md
│ └── references/
│ └── tracked-changes.md ← loaded as a resource
└── data-analysis/
└── SKILL.md
Example SKILL.md:
---
name: docx
description: Edit and review Word documents using tracked changes
---
When the user asks you to edit a Word document:
1. Always use tracked changes so edits can be reviewed.
...
Any file in the skill directory (other than SKILL.md itself and files under a scripts/
subdirectory) is automatically loaded as a SkillResource that the LLM can read on demand.
Use FileSystemSkillLoader from the langchain4j-skills module to load skills from the file system:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-skills</artifactId>
<version>1.13.0-beta23</version>
</dependency>
// Load all skills found in immediate subdirectories:
List<FileSystemSkill> skills = FileSystemSkillLoader.loadSkills(Path.of("skills/"));
// Or load a single skill by its directory:
FileSystemSkill skill = FileSystemSkillLoader.loadSkill(Path.of("skills/docx"));
From the Classpath
ClassPathSkillLoader works like FileSystemSkillLoader but resolves skill directories from
the classpath instead of the filesystem. This is useful when skills are bundled inside your
JAR or located under src/main/resources:
src/main/resources/
└── skills/
├── docx/
│ ├── SKILL.md
│ └── references/
│ └── tracked-changes.md
└── data-analysis/
└── SKILL.md
// Load all skills from a classpath directory:
List<FileSystemSkill> skills = ClassPathSkillLoader.loadSkills("skills");
// Or load a single skill:
FileSystemSkill skill = ClassPathSkillLoader.loadSkill("skills/docx");
By default, ClassPathSkillLoader uses the thread's context class loader.
You can pass a custom ClassLoader if needed:
FileSystemSkill skill = ClassPathSkillLoader.loadSkill("skills/docx", myClassLoader);
The same SKILL.md format, resource loading rules, and scripts/ exclusion apply
as with FileSystemSkillLoader.
Programmatically
Skills do not have to be file-system based. You can create them from any source — a database, a remote API, generated at runtime — using the builder API:
Skill skill = Skill.builder()
.name("incident-response")
.description("Step-by-step runbook for diagnosing and resolving production incidents")
.content("""
When a production alert fires:
1. Call `fetchRecentLogs(serviceName)` to retrieve the last 5 minutes of logs.
2. Call `checkServiceHealth(serviceName)` to get current health metrics.
3. Based on the findings, call `createIncidentTicket(summary, severity)`.
4. If severity is CRITICAL, also call `pageOnCall(incidentId)`.
""")
.build();
You can also attach resources programmatically:
SkillResource reference = SkillResource.builder()
.relativePath("references/tone-guide.md")
.content("Use warm, concise language. Avoid jargon.")
.build();
Skill skill = Skill.builder()
.name("customer-support")
.description("Handles customer support inquiries")
.content("Follow the tone guide in references/tone-guide.md ...")
.resources(List.of(reference))
.build();
Modes
Skills can be integrated with an AI Service in two distinct modes, depending on how much control and trust you need.
Tool Mode (Recommended)
Class: Skills (from the langchain4j-skills module)
This corresponds to the Tool-based agents integration approach described in the Agent Skills specification.
In this mode, the LLM activates a skill to receive step-by-step instructions, then carries
them out by calling the tools you have explicitly registered.
The LLM has no access to the file system at inference time — all skill content and
resources are loaded into memory upfront (e.g. via FileSystemSkillLoader), and the activate_skill
and read_skill_resource tools returns that preloaded content rather than reading from disk.
Because only your pre-defined tools can be invoked, there is no risk of arbitrary code execution.
Registered Tools
| Tool | When registered |
|---|---|
activate_skill | Always. The LLM calls this to load a skill's full instructions into the context. |
read_skill_resource | When at least one skill has resources. The LLM calls this to read individual reference files. |
| Skill-scoped tools | After the skill is activated. |
How It Works
- The system message lists the available skills (names and descriptions) so the LLM can choose.
- The user asks a question that requires a specific skill.
- The LLM calls
activate_skill("my-skill")to receive its instructions. - The LLM follows those instructions to complete the task, optionally reading resource files along the way.
Example Skill
Skills describe the policy — the exact order of calls, required arguments, error-handling steps, and worked examples — while the actual execution stays in type-safe, tested Java code:
---
name: process-order
description: Processes a customer order end-to-end
---
To process an order:
1. Call `validateOrder(orderId)` to check the order is valid.
2. Call `reserveInventory(orderId)` to reserve the required stock.
3. Only if reservation succeeds, call `chargePayment(orderId)`.
4. Finally, call `sendConfirmationEmail(orderId)`.
If any step fails, call `rollbackOrder(orderId)` before reporting the error.
Wiring It Up
Pass the ToolProvider from Skills to your AI Service builder alongside your regular tools.
Use formatAvailableSkills() to inject the skill catalogue into the system message so
the LLM knows which skills it can activate:
Skills skills = Skills.from(FileSystemSkillLoader.loadSkills(Path.of("skills/")));
MyAiService service = AiServices.builder(MyAiService.class)
.chatModel(chatModel)
.tools(new OrderTools()) // your tools
.toolProvider(skills.toolProvider()) // or .toolProviders(myToolProvider, skills.toolProvider()) if you already have a tool provider configured
.systemMessage("You have access to the following skills:\n" + skills.formatAvailableSkills()
+ "\nWhen the user's request relates to one of these skills, activate it first using the `activate_skill` tool before proceeding.")
.build();
formatAvailableSkills() returns an XML-formatted block listing each skill's name and description:
<available_skills>
<skill>
<name>process-order</name>
<description>Processes a customer order end-to-end</description>
</skill>
<skill>
<name>data-analysis</name>
<description>Analyse tabular data and produce charts</description>
</skill>
</available_skills>
Customisation
The name, description, and parameter metadata of each tool can be overridden through the corresponding config class on the builder:
Skills skills = Skills.builder()
.skills(mySkills)
.activateSkillToolConfig(ActivateSkillToolConfig.builder()
.name(...) // tool name (default: "activate_skill")
.description(...) // tool description
.parameterName(...) // parameter name (default: "skill_name")
.parameterDescription(...) // parameter description
.throwToolArgumentsExceptions(...) // throw ToolArgumentsException instead of ToolExecutionException (default: false)
.build())
.readResourceToolConfig(ReadResourceToolConfig.builder()
.name(...) // tool name (default: "read_skill_resource")
.description(...) // tool description
.skillNameParameterName(...) // skill_name parameter name (default: "skill_name")
.skillNameParameterDescription(...) // skill_name parameter description
.relativePathParameterName(...) // relative_path parameter name (default: "relative_path")
.relativePathParameterDescription(...) // static description (takes precedence over provider)
.relativePathParameterDescriptionProvider(...) // dynamic description based on available resources
.throwToolArgumentsExceptions(...) // throw ToolArgumentsException instead of ToolExecutionException (default: false)
.build())
.build();