anythingllm-skill-builder
AnythingLLM Custom Agent Skill Builder
When to Use
Use this skill when the user asks to create, build, scaffold, or generate an AnythingLLM custom agent skill. This includes requests like:
- "Create an AnythingLLM skill that..."
- "Build an agent skill for AnythingLLM"
- "Add a custom skill to my AnythingLLM"
- "Make my AnythingLLM agent able to..."
User's AnythingLLM Setup
- EasyPanel service:
basheer / anything-llm - Base API URL:
https://basheer-anything-llm.c9tnyg.easypanel.host/api/v1 - Docker volume:
storagemounted at/app/server/storage - Skills path (inside container):
/app/server/storage/plugins/agent-skills/ - API key env:
ANYTHING_LLM_KEY(in ANC app's.env) - Hot reload: Skills are hot-loaded — no restart needed. Just exit active agent session and reload page.
- AnythingLLM version requirement: Docker v1.2.2+ (user has this)
Skill File Structure
Every AnythingLLM custom skill is a folder containing exactly these files:
plugins/agent-skills/{hubId}/
├── plugin.json # Metadata, params, setup args
├── handler.js # Runtime logic (Node.js)
└── README.md # Optional documentation
CRITICAL: The folder name MUST match the hubId in plugin.json exactly.
plugin.json Schema (skill-1.0.0)
{
"active": true,
"name": "Human Readable Skill Name",
"hubId": "kebab-case-folder-name",
"schema": "skill-1.0.0",
"version": "1.0.0",
"description": "What this skill does — the LLM reads this to decide when to invoke it",
"author": "@basheer",
"author_url": "https://github.com/khaledbashir",
"license": "MIT",
"setup_args": {
"API_KEY": {
"type": "string",
"required": false,
"input": {
"type": "text",
"default": "",
"placeholder": "sk-xxxxx",
"hint": "Optional API key for the service"
},
"value": ""
}
},
"examples": [
{
"prompt": "Example user prompt that should trigger this skill",
"call": "{\"param1\": \"value1\", \"param2\": \"value2\"}"
}
],
"entrypoint": {
"file": "handler.js",
"params": {
"param1": {
"description": "What this parameter is for",
"type": "string"
}
}
},
"imported": true
}
Field Reference
| Field | Type | Required | Notes |
|---|---|---|---|
active |
boolean | YES | Set true to load the skill |
name |
string | YES | Human-readable name shown in UI |
hubId |
string | YES | Must match folder name exactly (kebab-case) |
schema |
string | YES | Always "skill-1.0.0" |
version |
string | YES | Semver (e.g., "1.0.0") |
description |
string | YES | LLM reads this to decide invocation — be specific |
author |
string | no | "@basheer" |
author_url |
string | no | "https://github.com/khaledbashir" |
license |
string | no | "MIT" |
setup_args |
object | no | UI-configurable settings (API keys, URLs, etc.) |
examples |
array | no | Few-shot examples help the LLM know when to call this |
entrypoint |
object | YES | file + params the handler expects |
imported |
boolean | YES | Always true for custom skills |
entrypoint.params types
"string"— text input"number"— numeric input"boolean"— true/false
setup_args input types
"text"— standard text field- Each setup_arg needs:
type,required,input.type,input.default,input.placeholder,input.hint
handler.js Template
// {skill-name} — AnythingLLM Custom Agent Skill
// Created for ANC Proposal Engine
module.exports.runtime = {
handler: async function ({ param1, param2 }) {
try {
this.introspect(`Processing: ${param1}...`);
// Access setup_args (configured in UI):
// const apiKey = this.runtimeArgs["API_KEY"];
// Access skill metadata:
// const { name, hubId, version } = this.config;
// Your logic here...
const result = await fetch("https://api.example.com/endpoint", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ param1, param2 }),
});
const data = await result.json();
// MUST return a string — anything else breaks the agent
return JSON.stringify(data, null, 2);
} catch (e) {
this.introspect(`Error: ${e.message}`);
this.logger("Skill error", e.message);
return `Skill failed: ${e.message}`;
}
},
};
handler.js Rules
- Must export
module.exports.runtimewith ahandlerasync function - Must return a string — anything else breaks the agent or loops forever
- Must wrap everything in try/catch — return error as string
this.introspect(msg)— shows thinking text in the AnythingLLM UIthis.logger(label, msg)— writes to server console for debuggingthis.runtimeArgs— object of setup_args values from plugin.jsonthis.config—{ name, hubId, version }metadata- Parameters come from entrypoint.params — destructure from the single argument object
require()modules inside the handler — not at top level- Bundle dependencies — node_modules must be inside the skill folder if needed
Deployment Instructions
After generating the skill files, tell the user:
-
Copy the skill folder to AnythingLLM container:
# From local machine or server docker cp ./my-skill-folder basheer-anything-llm:/app/server/storage/plugins/agent-skills/Or via EasyPanel terminal:
# Inside the container, skills live at: /app/server/storage/plugins/agent-skills/ -
Reload AnythingLLM — exit any active agent chat, refresh the browser
-
Enable the skill — go to Agent Settings > Skills, find the new skill, toggle it on
-
Configure setup_args — if the skill has setup_args, click the gear icon to set values
-
Test — start a new agent chat and try one of the example prompts
Output Location
Generate skill files at: /root/rag2/.claude/generated-skills/{hubId}/
- This staging area lets the user review before deploying
- Include a
deploy.shscript for easy container copy
Common Skill Patterns
API Proxy Skill
Wraps an external REST API so the AnythingLLM agent can call it conversationally.
Database Query Skill
Queries a database and returns formatted results. For ANC, could query the proposal DB via the Next.js API.
ANC-Specific Skills
When building skills that interact with the ANC Proposal Engine:
- API base: Use setup_arg for the URL (default:
https://basheer-natalia.prd42b.easypanel.host) - Auth: Include auth token in setup_args if the endpoint requires it
- Endpoints: See the ANC API routes in the anc-bible skill for available endpoints
Example: ANC Product Lookup Skill
// plugin.json
{
"active": true,
"name": "ANC Product Catalog Search",
"hubId": "anc-product-lookup",
"schema": "skill-1.0.0",
"version": "1.0.0",
"description": "Search the ANC LED product catalog by manufacturer, pixel pitch, or environment. Returns matching products with specs and pricing.",
"author": "@basheer",
"license": "MIT",
"setup_args": {
"ANC_API_URL": {
"type": "string",
"required": true,
"input": {
"type": "text",
"default": "https://basheer-natalia.prd42b.easypanel.host",
"placeholder": "https://your-anc-instance.com",
"hint": "Base URL of your ANC Proposal Engine"
},
"value": "https://basheer-natalia.prd42b.easypanel.host"
}
},
"examples": [
{
"prompt": "Find me LG outdoor LED panels with 4mm pitch",
"call": "{\"search\": \"LG\", \"environment\": \"outdoor\", \"pitchMax\": \"5\"}"
},
{
"prompt": "What Yaham products do we have?",
"call": "{\"search\": \"Yaham\"}"
}
],
"entrypoint": {
"file": "handler.js",
"params": {
"search": {
"description": "Text search across product name, model number, manufacturer",
"type": "string"
},
"environment": {
"description": "Filter by environment: indoor, outdoor, or indoor_outdoor",
"type": "string"
},
"pitchMin": {
"description": "Minimum pixel pitch in mm",
"type": "string"
},
"pitchMax": {
"description": "Maximum pixel pitch in mm",
"type": "string"
}
}
},
"imported": true
}
// handler.js
module.exports.runtime = {
handler: async function ({ search, environment, pitchMin, pitchMax }) {
try {
const baseUrl = this.runtimeArgs["ANC_API_URL"] || "https://basheer-natalia.prd42b.easypanel.host";
const params = new URLSearchParams();
if (search) params.set("search", search);
if (environment) params.set("environment", environment);
if (pitchMin) params.set("pitchMin", pitchMin);
if (pitchMax) params.set("pitchMax", pitchMax);
this.introspect(`Searching ANC product catalog: ${params.toString() || "all products"}...`);
const response = await fetch(`${baseUrl}/api/products?${params.toString()}`);
if (!response.ok) return `API error: ${response.status} ${response.statusText}`;
const data = await response.json();
if (!data.products || data.products.length === 0) {
return "No products found matching your criteria.";
}
const summary = data.products.map(p =>
`${p.manufacturer} ${p.displayName || p.modelNumber} — ${p.pixelPitch}mm pitch, ${p.environment}, ${p.maxNits || "N/A"} nits, ${p.cabinetWidthMm}x${p.cabinetHeightMm}mm cab`
).join("\n");
return `Found ${data.total} products:\n${summary}`;
} catch (e) {
this.introspect(`Failed to search products: ${e.message}`);
return `Product search failed: ${e.message}`;
}
},
};