OpenClaw Plugins We Built: Extending AI Agent Capabilities
TL;DR: A walkthrough of the OpenClaw plugins HouseofMVPs has built, what each does, the architecture behind them, and how building plugins for open source AI agent infrastructure directly translates to building production AI agents for clients.
Why We Build OpenClaw Plugins
When a client asks us to build an AI agent, the first question is always: what can the agent actually do? The answer lives in its tools. An agent without tools is just a well prompted chatbot. An agent with the right tools can query databases, send notifications, look up customer records, and take action in external systems. For context on why tools are the defining feature of agents, read what an AI agent is before exploring the specific plugin implementations below.
OpenClaw's plugin architecture is where we prototype that tool layer before committing to a production build. A plugin is a TypeScript module that registers tools with the agent runtime. Writing one takes a few hours. Testing it in a real conversation loop takes another hour. If the tool works the way we expected in the prototype, building the production version is straightforward because the design is already validated.
Over the past year we have built five plugins that we use across internal tools and client prototypes. Here is what each does and what we learned building it.
The Plugin Architecture
Before getting into specific plugins, here is how the system works.
Every OpenClaw plugin exports a single register function that receives the plugin API:
import type { PluginAPI } from "@open-claw/types";
export function register(api: PluginAPI) {
api.registerTool({
name: "tool_name",
description: "When to use this tool and what it returns",
parameters: {
type: "object",
properties: {
param1: { type: "string", description: "What this parameter does" },
},
required: ["param1"],
},
handler: async (params) => {
// Do the work
return "Result the agent can read and relay to the user";
},
});
}
The description field is load bearing. The LLM reads it to decide when to call the tool. A vague description produces unpredictable tool invocation. A precise description makes the agent reliable.
Parameters follow JSON Schema. The LLM uses this schema to extract the right values from conversation context and pass them to your handler. If a user says "search for TypeScript rate limiting libraries" and your tool has a query parameter of type string, the model extracts "TypeScript rate limiting libraries" and passes it as the query. No parsing code required.
The handler returns a string (or any JSON serializable value that gets stringified). The agent reads this return value and incorporates it into its response.
Plugin 1: Web Search
The most requested plugin in every project. Without web search the agent can only work from its training data, which means it cannot look up current documentation, check recent news, or verify whether a library is still maintained.
export function register(api: PluginAPI) {
api.registerTool({
name: "web_search",
description:
"Search the web for current information. Use this when the user asks about recent events, needs documentation for a specific library version, or asks a question where current information matters. Returns a list of result titles, URLs, and summaries.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query. Be specific for better results.",
},
max_results: {
type: "number",
description: "Number of results to return (default 5, max 10)",
},
},
required: ["query"],
},
handler: async ({ query, max_results = 5 }) => {
const response = await fetch(
`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${max_results}`,
{
headers: {
Accept: "application/json",
"X-Subscription-Token": process.env.BRAVE_API_KEY!,
},
}
);
const data = await response.json();
return data.web.results
.map(
(r: any) => `${r.title}\n${r.url}\n${r.description}`
)
.join("\n\n");
},
});
}
We use Brave Search API because it has a generous free tier and does not require a Google Cloud project. The response format (title, URL, description per result) is exactly what the agent needs to give a useful answer without overwhelming the context window with full page contents.
The key design decision is returning structured text rather than raw JSON. The agent reads the formatted string directly and can relay specific results to the user without further processing.
Plugin 2: CRM Lookup
For client agents that answer customer questions or assist sales teams, the agent needs access to customer records. This plugin queries a CRM (in our case HubSpot, but the pattern works for any CRM with an API) and returns contact and deal information.
export function register(api: PluginAPI) {
api.registerTool({
name: "crm_lookup",
description:
"Look up a customer or contact in the CRM by email address or company name. Returns contact details, associated deals, last activity date, and any notes on the account. Use this when the user asks about a specific customer or company.",
parameters: {
type: "object",
properties: {
identifier: {
type: "string",
description:
"Email address or company name to look up",
},
type: {
type: "string",
enum: ["email", "company"],
description: "Whether identifier is an email or company name",
},
},
required: ["identifier", "type"],
},
handler: async ({ identifier, type }) => {
const token = process.env.HUBSPOT_API_TOKEN!;
const filter =
type === "email"
? { propertyName: "email", operator: "EQ", value: identifier }
: { propertyName: "company", operator: "EQ", value: identifier };
const searchRes = await fetch(
"https://api.hubapi.com/crm/v3/objects/contacts/search",
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
filterGroups: [{ filters: [filter] }],
properties: ["firstname", "lastname", "email", "company", "hs_last_sales_activity_date", "notes_last_contacted"],
}),
}
);
const data = await searchRes.json();
if (!data.results?.length) return "No contact found matching that identifier.";
const contact = data.results[0].properties;
return [
`Name: ${contact.firstname} ${contact.lastname}`,
`Company: ${contact.company}`,
`Email: ${contact.email}`,
`Last contacted: ${contact.notes_last_contacted ?? "unknown"}`,
`Last sales activity: ${contact.hs_last_sales_activity_date ?? "none"}`,
].join("\n");
},
});
}
The critical security consideration here is channel gating. This tool should only be available in internal Slack or Discord channels, never in a public facing bot. In the OpenClaw plugin registration, restrict it:
api.registerTool({
name: "crm_lookup",
channels: ["slack", "cli"], // Internal channels only
// ...
});
This pattern carries into production agent builds directly. Every tool that touches customer data gets access control at the tool registration level, not just at the application level.
Plugin 3: PostgreSQL Query Tool
For internal tools where the agent needs to answer questions about operational data, direct database query capability is transformative. Instead of building a custom reporting UI, the agent runs queries and explains the results in natural language.
export function register(api: PluginAPI) {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
api.registerTool({
name: "query_database",
description:
"Run a read only SQL query against the application database. Use this to answer questions about user counts, revenue, activity, or any operational data. Always use SELECT statements only. Returns query results as formatted text.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description:
"A valid PostgreSQL SELECT query. No INSERT, UPDATE, DELETE, or DDL statements.",
},
description: {
type: "string",
description:
"Plain English description of what the query answers, for context",
},
},
required: ["query", "description"],
},
handler: async ({ query, description }) => {
if (/\b(insert|update|delete|drop|truncate|alter|create)\b/i.test(query)) {
return "Error: Only SELECT queries are allowed.";
}
try {
const result = await pool.query(query);
const rows = result.rows.slice(0, 20); // Cap at 20 rows
const formatted = rows
.map((row) =>
Object.entries(row)
.map(([k, v]) => `${k}: ${v}`)
.join(", ")
)
.join("\n");
return `Query: ${description}\nRows returned: ${result.rowCount}\n\n${formatted}`;
} catch (err: any) {
return `Query error: ${err.message}`;
}
},
});
}
Two safety decisions here: the regex check blocks write operations at the handler level (belt and suspenders — the database user should also be read only), and the result is capped at 20 rows to avoid flooding the context window with a full table dump.
This plugin is the foundation of a business intelligence agent. Pair it with web search and a well written SOUL.md about the business domain, and the agent can answer operational questions that previously required a BI tool or a data analyst.
Plugin 4: File Management
For developer and operations agents that need to work with files on the server, this plugin handles reading, writing, listing, and deleting files within a sandboxed directory.
import { readFile, writeFile, readdir, unlink } from "fs/promises";
import path from "path";
const SANDBOX_DIR = process.env.FILE_SANDBOX_DIR ?? "./agent-files";
function safePath(filename: string): string {
const resolved = path.resolve(SANDBOX_DIR, filename);
if (!resolved.startsWith(path.resolve(SANDBOX_DIR))) {
throw new Error("Path traversal attempt blocked");
}
return resolved;
}
export function register(api: PluginAPI) {
api.registerTool({
name: "read_file",
description: "Read the contents of a file in the agent workspace.",
parameters: {
type: "object",
properties: {
filename: { type: "string", description: "Filename to read" },
},
required: ["filename"],
},
handler: async ({ filename }) => {
const contents = await readFile(safePath(filename), "utf8");
return contents.slice(0, 8000); // Context window budget
},
});
api.registerTool({
name: "write_file",
description:
"Write content to a file in the agent workspace. Creates the file if it does not exist.",
parameters: {
type: "object",
properties: {
filename: { type: "string" },
content: { type: "string" },
},
required: ["filename", "content"],
},
handler: async ({ filename, content }) => {
await writeFile(safePath(filename), content, "utf8");
return `File ${filename} written successfully.`;
},
});
api.registerTool({
name: "list_files",
description: "List files available in the agent workspace.",
parameters: { type: "object", properties: {} },
handler: async () => {
const files = await readdir(SANDBOX_DIR);
return files.join("\n");
},
});
}
The safePath function is non negotiable. Without path traversal protection, a prompt injection attack could read arbitrary files on the server. Always resolve paths relative to a fixed sandbox directory and verify the resolved path stays inside the sandbox.
Plugin 5: Slack Notification
Sometimes the agent needs to push information out rather than responding to a query. This plugin lets the agent send notifications to Slack channels, which is useful for monitoring agents that watch for conditions and alert the team.
export function register(api: PluginAPI) {
api.registerTool({
name: "send_slack_notification",
description:
"Send a notification message to a Slack channel. Use this to alert the team about important events, errors, or status updates. Do not use for responses to the user in the current conversation.",
parameters: {
type: "object",
properties: {
channel: {
type: "string",
description: "Slack channel name without the # symbol (e.g. 'alerts')",
},
message: {
type: "string",
description: "The message to send. Keep it concise.",
},
urgency: {
type: "string",
enum: ["low", "medium", "high"],
description:
"Urgency level. High prepends an alert emoji to the message.",
},
},
required: ["channel", "message", "urgency"],
},
handler: async ({ channel, message, urgency }) => {
const prefix = urgency === "high" ? "ALERT: " : "";
const response = await fetch("https://slack.com/api/chat.postMessage", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
channel: `#${channel}`,
text: `${prefix}${message}`,
}),
});
const data = await response.json();
return data.ok ? "Notification sent." : `Failed: ${data.error}`;
},
});
}
The urgency enum is a small but useful addition. It lets the agent make a judgment call about importance rather than defaulting to all caps alerts. The LLM decides urgency level based on context, and the handler translates that into a prefix.
How This Translates to Client Work
Each of these plugins maps directly to a capability we build into production AI agents for clients:
| Plugin | Production Equivalent |
|---|---|
| Web search | Retrieval augmented generation with live search |
| CRM lookup | CRM API integration with role based access |
| PostgreSQL query | Database tool use with query validation and audit logging |
| File management | Document processing with secure storage |
| Slack notification | Event driven alerting and workflow triggers |
The prototype validates the design. When we tell a client we can build an agent that answers customer questions using live CRM data, we have already proven the architecture works in an OpenClaw prototype. The production build adds proper error handling, rate limiting, monitoring, and infrastructure, but the core tool design is already tested. Use the AI Agent ROI Calculator to estimate the time savings from the specific tools you are planning before committing to the build.
Our AI agent development work starts from exactly this process: prototype in OpenClaw, validate with real usage, build production on the Anthropic API with the same tool patterns.
If you want to build your own plugins, our OpenClaw plugin tutorial walks through the full process step by step. For a deeper look at how we run production agents on OpenClaw, see our technical guide on building AI agents with OpenClaw.
To understand the broader OpenClaw architecture before diving into plugins, start with what OpenClaw is.
Build With an AI-Native Agency
Free: 14-Day AI MVP Checklist
The exact checklist we use to ship production-ready MVPs in 2 weeks. Enter your email to download.
OpenClaw Plugin Development Starter Kit
TypeScript boilerplate for OpenClaw plugins with type definitions, error handling patterns, and test setup.
Frequently Asked Questions
Frequently Asked Questions
Free Estimate in 2 Minutes
Already know your scope? Book a Fixed-Price Scope Review
