This page is intended for developers who want to create custom agent skills for AnythingLLM.
Rules & Guidelines
The
handler.jsfile must export aruntimeobject with ahandlerfunction.The
handlerfunction must accept a single argument which is an object containing the parameters defined in theplugin.jsonentrypointproperty, if any.The
handlerfunction must return a string value, anything else may break the agent invocation or loop indefinitely.You must use
requireto import any modules you need from the NodeJS standard library or any modules you have bundled with your custom agent skill.You must use
awaitwhen making any calls to external APIs or services.You must wrap your entire custom agent skill in a
try/catchblock and return any error messages to the agent at invocation time.
How AnythingLLM determines when a skill is complete
A custom agent skill does not set a completion flag. After the handler
function resolves, AnythingLLM adds its returned string to the agent
conversation. The agent model then decides whether to call another tool or use
the result to answer the user.
A skill invocation and the overall user task finish at different points:
- The skill invocation finishes when its
handlerresolves and returns a string. - The overall task finishes when the agent model determines that the returned result satisfies the user's request.
To reduce unnecessary repeated calls:
- Return a concise, self-contained result that clearly states what happened.
- State whether the operation succeeded or failed and include any relevant output, identifiers, or next steps.
- Use a specific skill
descriptionand precise parameter descriptions so the model understands when and how to call it. - Add one to three representative
examplesinplugin.json. - Return errors as strings from the
catchblock instead of returning an empty value or allowing the handler to fail silently. - Do not rely on
this.introspectorthis.loggerto communicate completion; always return the final outcome from thehandler. - Make operations with external side effects idempotent when possible because the model may decide to retry a call.
AnythingLLM limits how many tools an agent can chain in one response. You can change this limit from the Agent Skills settings panel by increasing Max Tool Calls to any number. This limit is a safety guard rather than a skill-completion signal, so a skill should still return enough information for the model to determine that the requested work is complete.
Available runtime properties and methods
this.runtimeArgs
The this.runtimeArgs object contains the arguments that were passed to the setup_args from the plugin.json file.
You can access the value of a specific argument by using the propertyName as the key.
// plugin.json excerpt
// "setup_args": {
// "OPEN_METEO_API_KEY": {
// "type": "string",
// "required": false,
// "input": {
// "type": "text",
// "default": "YOUR_OPEN_METEO_API_KEY",
// "placeholder": "sk-1234567890",
// "hint": "The API key for the open-meteo API"
// },
// "value": "sk-key-for-service",
// }
// },
this.runtimeArgs["OPEN_METEO_API_KEY"]; // 'sk-key-for-service'this.introspect
The this.introspect function is used to log "thoughts" or "observations" to the user interface while the agent is running.
this.introspect("Hello, world!"); // must be a string - will be shown to userthis.logger
The this.logger function is used to log messages to the console. This is useful for debugging your custom agent skill via logs.
this.logger("Hello, world!"); // must be a string - will be printed to console while the agent is runningthis.config
The this.config object contains the configuration for your custom agent skill. Useful for when you need to know the name of your custom agent skill or the version or for logs.
this.config.name; // 'Get Weather'
this.config.hubId; // 'open-meteo-weather-api'
this.config.version; // '1.0.0'this.requestToolApproval
The this.requestToolApproval method pauses the agent and asks the user to approve a potentially destructive action before your skill performs it. It shows the same Approve/Reject card that AnythingLLM's built-in tools use (for example, the Gmail send/reply tools), and resolves once the user responds.
Use this whenever your skill is about to do something irreversible or high-impact — deleting records, sending messages, making purchases, writing to external systems, etc.
const approval = await this.requestToolApproval({
payload: { recordId }, // optional: arbitrary data shown/recorded alongside the request
description: `Permanently delete record ${recordId}? This cannot be undone.`,
});
if (!approval.approved) return approval.message; // user rejected - stop and report back
// ...proceed with the destructive actionIt returns a { approved, message } object:
approved(boolean) —trueif the user approved (or if approval is not required in the current context),falseif they rejected.message(string) — a human-readable result message. On rejection, return this string from your handler so the agent and user know the action was declined.
Both arguments are optional — payload defaults to {} and description defaults to null — but you should always pass a clear description so the user understands exactly what they are approving.
A few behaviors worth knowing:
The approval is keyed to your skill automatically (by its
hubId). You cannot pass askillNameto impersonate another tool, and the "Always allow" whitelist a user grants applies only to your skill.In non-interactive contexts where there is no user to approve (for example, a scheduled agent run), the method resolves as approved (
{ approved: true, message: "Approval not required in this context." }) so your skill still runs.The user has 120 seconds to respond. If they do not respond in time, the request is treated as rejected.
Example handler.js
Objective: Get the weather for a given location latitude and longitude using the open-meteo API.
// handler.js
// NOT RECOMMENDED: We're using an external module here for demonstration purposes
// this would be a module we bundled with our custom agent skill and would be located in the same folder as our handler.js file
// Do not require modules outside of the plugin folder. It is recommended to use require within a function scope instead of the global scope.
// const _ExternalApiCaller = require('./external-api-caller.js');
module.exports.runtime = {
handler: async function ({ latitude, longitude }) {
const callerId = `${this.config.name}-v${this.config.version}`;
try {
this.introspect(
`${callerId} called with lat:${latitude} long:${longitude}...`
);
const response = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t_weather=true&hourly=temperature_2m,relativehumidity_2m,windspeed_10m`
);
const data = await response.json();
const averageTemperature = this._getAverage(data, "temperature_2m");
const averageHumidity = this._getAverage(data, "relativehumidity_2m");
const averageWindSpeed = this._getAverage(data, "windspeed_10m");
return JSON.stringify({
averageTemperature,
averageHumidity,
averageWindSpeed,
});
} catch (e) {
this.introspect(
`${callerId} failed to invoke with lat:${latitude} long:${longitude}. Reason: ${e.message}`
);
this.logger(
`${callerId} failed to invoke with lat:${latitude} long:${longitude}`,
e.message
);
return `The tool failed to run for some reason. Here is all we know ${e.message}`;
}
},
// Helper function to get the average of an array of numbers!
_getAverage(data, property) {
return (
data.hourly[property].reduce((a, b) => a + b, 0) /
data.hourly[property].length
);
},
// Recommended: Use this method to call external APIs or services
// by requiring the module in the function scope and only if the code execution reaches that line
// this is to prevent any unforseen issues with the global scope and module loading/unloading.
// This file should be placed in the same folder as your handler.js file.
_doExternalApiCall(myProp) {
const _ScopedExternalCaller = require("./external-api-caller.js");
return _ScopedExternalCaller.doSomething(myProp);
},
};