Tutorial: Como estou estudando o desenvolvimento de plugins com Tools para o OpenClaw
Estou começando a olhar para o OpenClaw não apenas como uma ferramenta de automação com agentes, mas como uma plataforma que pode ser expandida por meio de plugins. Ainda não uso o OpenClaw como meu ambiente principal de desenvolvimento. No meu fluxo diário, continuo usando o Windsurf, especialmente com o SWE para tarefas mais simples e o Claude Opus para tarefas mais complexas. Mesmo assim, o modelo de plugins do OpenClaw me chamou atenção porque ele permite criar Tools próprias, ou seja, capacidades específicas que um agente pode chamar para executar tarefas reais, consultar sistemas externos, organizar dados, buscar informações ou acionar fluxos automatizados.
Meu interesse nesse tipo de plugin nasce de uma necessidade prática. Estou trabalhando em uma série de plugins para atender demandas pessoais e profissionais. Alguns agentes já funcionam no meu fluxo atual, mesmo fora do OpenClaw. Tenho, por exemplo, um agente que consulta diariamente a internet em busca de vagas de emprego e outro que pesquisa artigos sobre sistemas embarcados e IoT, gerando uma página de resumo com links para os principais conteúdos encontrados. A ideia é transformar esse tipo de automação em Tools bem estruturadas, reutilizáveis e integráveis a agentes mais sofisticados.
O que é um plugin de Tool no OpenClaw
Um plugin de Tool é uma extensão que adiciona novas ações ao OpenClaw. Em vez de o agente ficar limitado apenas à conversa, ele pode chamar uma função externa definida por mim. Essa função pode consultar uma API, ler uma base de dados, buscar informações na internet, gerar um relatório, criar arquivos, acionar um script local ou integrar algum serviço.
Na prática, uma Tool funciona como uma função fortemente descrita. Ela tem nome, descrição, parâmetros de entrada, validação de tipos e uma função execute, que contém a lógica real. Essa estrutura é importante porque o agente precisa saber quando usar a ferramenta, quais dados enviar e que tipo de resposta esperar.
O desenvolvimento é feito com Node.js, TypeScript e ESM. Segundo o tutorial de referência, os requisitos principais são Node.js 22 ou superior, TypeScript com saída ESM, o pacote typebox para definir schemas e OpenClaw na versão 2026.5.17 ou superior, que exporta o SDK openclaw/plugin-sdk/tool-plugin.
Estrutura básica de um plugin
Um plugin simples segue uma estrutura parecida com esta:
my-plugin/
├── src/
│ ├── index.ts
│ └── index.test.ts
├── dist/
├── package.json
├── openclaw.plugin.json
├── tsconfig.json
└── README.md
O arquivo mais importante é o src/index.ts, porque nele defino o plugin e suas Tools. A pasta dist/ recebe o código compilado. O package.json declara scripts, dependências e o ponto de extensão do OpenClaw. O arquivo openclaw.plugin.json contém os metadados do plugin, normalmente gerados pelo próprio comando de build do OpenClaw.
Para iniciar um plugin, o fluxo sugerido é:
openclaw plugins init my-plugin --name "My Plugin"
cd my-plugin
npm install
Esse comando cria uma base inicial com um exemplo simples, geralmente uma Tool do tipo echo, útil para entender o funcionamento mínimo do SDK.
Criando a primeira Tool
A estrutura básica de um plugin usa defineToolPlugin. Eu começo importando o Type do typebox e o defineToolPlugin do SDK do OpenClaw:
import { Type } from "typebox";
import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
export default defineToolPlugin({
id: "personal-research-tools",
name: "Personal Research Tools",
description: "Plugins pessoais para automação de pesquisas, vagas e artigos técnicos.",
configSchema: Type.Object({
apiKey: Type.Optional(Type.String({ description: "Chave de API opcional." })),
baseUrl: Type.Optional(Type.String({ description: "URL base opcional." })),
}),
tools: (tool) => [
tool({
name: "echo_text",
label: "Echo Text",
description: "Retorna o texto recebido, útil para teste inicial do plugin.",
parameters: Type.Object({
input: Type.String({ description: "Texto que será retornado pela Tool." }),
}),
async execute({ input }, config, context) {
context.signal?.throwIfAborted();
return {
input,
length: input.length,
};
},
}),
],
});
Esse exemplo ainda não resolve um problema real, mas mostra o esqueleto fundamental. O campo id identifica o plugin. O campo name apresenta o nome legível. A descrição ajuda o OpenClaw e o usuário a entenderem a finalidade do plugin. O configSchema define configurações globais do plugin, como chaves de API, URLs, tokens ou parâmetros de ambiente. Já o bloco tools declara uma ou mais ferramentas disponíveis para o agente.
Entendendo os parâmetros com Typebox
O OpenClaw precisa entender quais dados uma Tool espera receber. Para isso, o tutorial usa typebox, que permite criar schemas de forma declarativa. Por exemplo:
parameters: Type.Object({
query: Type.String({
description: "Termo de busca que será usado na pesquisa.",
}),
limit: Type.Optional(Type.Number({
description: "Quantidade máxima de resultados.",
})),
})
Essa abordagem é muito importante porque evita que o agente envie parâmetros soltos ou malformados. Em sistemas baseados em agentes, validação de entrada não é detalhe: é segurança, previsibilidade e confiabilidade. Quando uma Tool acessa internet, arquivos, APIs ou banco de dados, eu preciso saber exatamente quais parâmetros ela aceita.
Criando uma Tool para buscar vagas de emprego
Pensando no meu agente pessoal que consulta diariamente vagas de emprego, eu poderia começar com uma Tool chamada search_jobs. Ela receberia uma palavra-chave, uma localização e um limite de resultados. A implementação real poderia usar uma API externa, um mecanismo de busca ou uma base previamente indexada.
Um primeiro esqueleto seria assim:
import { Type } from "typebox";
import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
export default defineToolPlugin({
id: "career-research-tools",
name: "Career Research Tools",
description: "Tools para pesquisa de vagas de emprego e oportunidades profissionais.",
configSchema: Type.Object({
baseUrl: Type.Optional(Type.String({
description: "URL base do serviço de busca de vagas.",
})),
apiKey: Type.Optional(Type.String({
description: "Chave de API para serviços externos.",
})),
}),
tools: (tool) => [
tool({
name: "search_jobs",
label: "Search Jobs",
description: "Pesquisa vagas de emprego com base em palavra-chave, localização e limite de resultados.",
parameters: Type.Object({
keyword: Type.String({
description: "Palavra-chave da vaga, como firmware, embedded, IoT ou backend.",
}),
location: Type.Optional(Type.String({
description: "Localização desejada para a busca.",
})),
limit: Type.Optional(Type.Number({
description: "Quantidade máxima de vagas retornadas.",
})),
}),
async execute({ keyword, location, limit = 10 }, config, context) {
context.signal?.throwIfAborted();
/*
* Aqui entraria a chamada real para uma API de vagas,
* consulta em banco local, scraping autorizado ou mecanismo próprio.
*/
return {
query: {
keyword,
location: location ?? "remote",
limit,
},
results: [
{
title: "Embedded Software Engineer",
company: "Example Company",
location: location ?? "Remote",
url: "https://example.com/job/embedded-software-engineer",
},
],
};
},
}),
],
});
Neste ponto, o importante não é a API em si, mas a forma de encapsular a capacidade. A Tool não precisa saber sobre a conversa inteira. Ela precisa receber parâmetros claros, executar uma tarefa bem delimitada e devolver um resultado estruturado.
Criando uma Tool para pesquisar artigos de Embarcados e IoT
Outro caso que já faz parte do meu fluxo é a consulta de artigos sobre sistemas embarcados e IoT. Nesse cenário, uma Tool poderia receber temas como FreeRTOS, Zephyr, STM32, ESP32, IoT security, Edge AI ou TinyML, buscar conteúdos recentes e retornar uma lista de links resumidos.
Um modelo inicial seria:
tool({
name: "search_embedded_articles",
label: "Search Embedded Articles",
description: "Pesquisa artigos técnicos recentes sobre sistemas embarcados, IoT e computação de borda.",
parameters: Type.Object({
topics: Type.Array(Type.String(), {
description: "Lista de temas pesquisados, como FreeRTOS, STM32, ESP32, IoT ou TinyML.",
}),
maxResults: Type.Optional(Type.Number({
description: "Número máximo de artigos retornados.",
})),
}),
async execute({ topics, maxResults = 10 }, config, context) {
context.signal?.throwIfAborted();
/*
* Aqui eu poderia integrar:
* - APIs de busca;
* - RSS feeds;
* - indexadores próprios;
* - mecanismos RAG;
* - bancos locais com artigos já coletados.
*/
return {
topics,
maxResults,
articles: [
{
title: "Practical FreeRTOS Debugging Techniques",
source: "Example Embedded Blog",
summary: "Artigo sobre diagnóstico de tarefas, pilha e temporização em FreeRTOS.",
url: "https://example.com/freertos-debugging",
},
],
};
},
})
Esse tipo de Tool é especialmente útil porque transforma uma rotina manual em uma função chamável por agente. Em vez de eu pedir genericamente “procure artigos sobre IoT”, posso ter uma Tool com contrato bem definido, retorno padronizado e possibilidade de gerar uma página HTML, Markdown ou JSON com os resultados.
Retorno em texto ou JSON
O tutorial de referência destaca uma diferença importante: a Tool pode retornar uma string simples ou um objeto compatível com JSON.
Quando retorno uma string, o modelo recebe exatamente aquele texto:
execute: ({ input }) => input
Quando retorno um objeto, o OpenClaw pode preservar a estrutura do resultado:
execute: ({ input }) => ({
input,
length: input.length,
})
Para Tools mais sérias, eu prefiro JSON estruturado. Isso facilita testes, logs, integração com outros agentes e geração posterior de relatórios. Por exemplo, uma Tool que busca artigos não deve retornar apenas um texto corrido. Ela deve retornar título, fonte, resumo, URL, data de publicação quando disponível e talvez uma classificação por relevância.
Tools opcionais e execução condicional
O tutorial também mostra o uso de optional: true. Isso é útil quando uma Tool depende de um ambiente específico. Por exemplo, uma Tool que executa um script local pode não estar disponível dentro de uma sessão sandboxed.
tool({
name: "local_workflow",
description: "Executa um workflow local fora de ambientes sandboxed.",
parameters: Type.Object({
goal: Type.String({
description: "Objetivo do workflow local.",
}),
}),
optional: true,
factory({ api, toolContext }) {
if (toolContext.sandboxed) {
return null;
}
return {
async execute({ goal }) {
return {
queued: true,
goal,
};
},
};
},
})
Esse recurso é importante porque nem toda Tool deve estar disponível em qualquer contexto. Uma Tool que apenas organiza texto é segura em praticamente qualquer ambiente. Já uma Tool que acessa arquivos locais, executa processos, consulta credenciais ou aciona APIs externas precisa de controle mais rigoroso.
Build do plugin
Depois de escrever o código, o primeiro passo é compilar o TypeScript:
npm run build
Esse comando gera os arquivos JavaScript na pasta dist/.
Em seguida, gero os metadados do plugin:
npm run plugin:build
No tutorial de referência, esse script normalmente executa algo como:
npm run build && openclaw plugins build --entry ./dist/index.js
Esse comando atualiza o arquivo openclaw.plugin.json, que descreve o plugin, suas Tools, schemas e contratos.
Depois vem a validação:
npm run plugin:validate
Que pode ser implementada assim:
npm run build && openclaw plugins validate --entry ./dist/index.js
Esse passo é essencial porque valida se o plugin está corretamente estruturado e se pode ser carregado pelo OpenClaw.
Testando o plugin
Um plugin de Tool deve ter testes. Mesmo que o código inicial seja simples, eu gosto de testar pelo menos os metadados, porque isso confirma se as Tools esperadas foram registradas.
Um teste básico com Vitest seria:
import { describe, expect, it } from "vitest";
import entry from "./index.js";
import { getToolPluginMetadata } from "openclaw/plugin-sdk/tool-plugin";
describe("career-research-tools", () => {
it("declares tool metadata", () => {
expect(getToolPluginMetadata(entry)?.tools.map((tool) => tool.name))
.toEqual([
"search_jobs",
"search_embedded_articles",
]);
});
});
Para executar:
npm test
Com o tempo, eu também adicionaria testes para a lógica interna de cada Tool. Por exemplo, uma Tool de busca de artigos deve tratar ausência de resultados, erro de API, timeout, resposta malformada e cancelamento via context.signal.
Configurando o package.json
O package.json precisa declarar ESM, dependências e scripts. Um exemplo baseado no tutorial seria:
{
"name": "career-research-tools",
"version": "0.1.0",
"type": "module",
"files": [
"dist",
"openclaw.plugin.json",
"README.md"
],
"peerDependencies": {
"openclaw": ">=2026.5.17"
},
"dependencies": {
"typebox": "^1.1.38"
},
"devDependencies": {
"openclaw": "latest",
"typescript": "^5.9.0",
"vitest": "^3.2.0"
},
"openclaw": {
"extensions": [
"./dist/index.js"
]
},
"scripts": {
"build": "tsc -p tsconfig.json",
"plugin:build": "npm run build && openclaw plugins build --entry ./dist/index.js",
"plugin:validate": "npm run build && openclaw plugins validate --entry ./dist/index.js",
"test": "vitest run"
}
}
O campo mais importante para integração com o OpenClaw é:
"openclaw": {
"extensions": [
"./dist/index.js"
]
}
Ele informa onde está o ponto de entrada compilado do plugin.
O arquivo openclaw.plugin.json
O arquivo openclaw.plugin.json é gerado automaticamente pelo comando de build do plugin, mas pode ser versionado junto com o projeto. Ele descreve o plugin de forma estática.
Um exemplo simplificado seria:
{
"id": "career-research-tools",
"name": "Career Research Tools",
"description": "Tools para pesquisa de vagas e artigos técnicos.",
"version": "0.1.0",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
},
"activation": {
"onStartup": true
},
"contracts": {
"tools": [
"search_jobs",
"search_embedded_articles"
]
}
}
Esse arquivo funciona como uma espécie de manifesto. Ele permite que o OpenClaw saiba quais contratos o plugin oferece.
Workflow de desenvolvimento
Meu fluxo ideal para desenvolver um plugin seria simples e repetitivo. Primeiro eu altero o código TypeScript. Depois compilo com npm run build. Em seguida gero os metadados com npm run plugin:build. Depois valido com npm run plugin:validate. Por fim, rodo os testes com npm test.
Em forma de sequência, o ciclo fica assim:
npm run build
npm run plugin:build
npm run plugin:validate
npm test
Esse ciclo é importante porque plugins para agentes não devem ser tratados como scripts improvisados. Uma Tool mal descrita pode fazer o agente tomar decisões ruins. Uma Tool sem validação pode receber parâmetros errados. Uma Tool sem testes pode falhar silenciosamente. E uma Tool que acessa sistemas externos precisa tratar erro, timeout, credenciais e limites de uso.
Cuidados de projeto
Quando penso em desenvolver plugins para OpenClaw, considero três pontos fundamentais: contrato, segurança e observabilidade.
O contrato define exatamente o que a Tool faz. Uma Tool chamada search_jobs não deve também enviar e-mails, alterar arquivos locais ou iniciar outro fluxo oculto. Quanto mais específica for a Tool, mais previsível será o comportamento do agente.
A segurança aparece na validação dos parâmetros, no controle de credenciais e na limitação do que cada Tool pode executar. Plugins que acessam internet, arquivos locais ou comandos do sistema precisam ser tratados com mais cuidado. O agente pode ser inteligente, mas a Tool deve ser disciplinada.
A observabilidade é a capacidade de entender o que aconteceu. Em meus próprios plugins, eu manteria logs claros, retornos estruturados e mensagens de erro úteis. Isso é ainda mais importante quando o agente executa tarefas recorrentes, como consultar vagas diariamente ou gerar resumos de artigos.
Conclusão
Desenvolver um plugin de Tool para o OpenClaw é, essencialmente, transformar uma capacidade prática em uma função segura, descrita e reutilizável por agentes. O processo começa com uma estrutura simples em TypeScript, passa pela definição de schemas com Typebox, pela criação das Tools com defineToolPlugin, pela compilação, geração de metadados, validação e testes.
Ainda continuo usando Windsurf no meu desenvolvimento diário, principalmente com SWE para tarefas simples e Claude Opus para tarefas mais complexas. Porém, o modelo de plugins do OpenClaw abre uma possibilidade interessante: organizar minhas automações pessoais em módulos formais, testáveis e reutilizáveis. Meus agentes de busca de vagas e pesquisa de artigos sobre Embarcados e IoT são bons exemplos de funcionalidades que podem evoluir para plugins reais, com Tools bem definidas, retorno estruturado e integração futura com fluxos agentic mais robustos.



