AgentKit's plugin system is extensible. You can create custom actions using either the customActionProvider helper (the easy way) or by implementing ActionDefinition directly (the advanced way).
The customActionProvider function is the simplest way to add custom tools. You provide a name, description, Zod schema, and an invoke function -- AgentKit handles the rest.
src/custom-tools.ts
import { z } from 'zod';import { customActionProvider } from '@goatnetwork/agentkit/providers';const myProvider = customActionProvider([ { name: 'my_tool.hello', description: 'Returns a greeting for the given name', schema: z.object({ name: z.string().min(1), }), invoke: async (input) => { return { message: `Hello, ${input.name}!` }; }, riskLevel: 'read', // optional, defaults to 'low' networks: [], // optional, empty = all networks requiresConfirmation: false, // optional, defaults to false }, { name: 'my_tool.fetch_price', description: 'Fetch the current price of a token from an external API', schema: z.object({ symbol: z.string().min(1), }), invoke: async (input) => { const resp = await fetch(`https://api.example.com/price/${input.symbol}`); const data = await resp.json(); return { symbol: input.symbol, price: data.price }; }, riskLevel: 'read', },]);// The returned provider has all actions registered and readyconst tools = myProvider.openAITools();console.log(tools);
For full control, implement the ActionDefinition interface. This is how the built-in plugins are written.
src/actions/my-action.ts
import { z } from 'zod';import type { ActionDefinition, WalletProvider } from '@goatnetwork/agentkit/core';// Define input/output typesinterface MyActionInput { contractAddress: string; tokenId: string;}interface MyActionOutput { owner: string; metadata: string;}const evmAddress = z .string() .regex(/^0x[0-9a-fA-F]{40}$/, 'Invalid EVM address');// Define the Zod input schemaconst inputSchema = z.object({ contractAddress: evmAddress, tokenId: z.string().regex(/^\d+$/, 'tokenId must be a numeric string'),});// Create a factory function that accepts dependenciesexport function myAction(wallet: WalletProvider): ActionDefinition<MyActionInput, MyActionOutput> { return { name: 'my_plugin.get_nft_info', description: 'Query NFT owner and metadata URI from any ERC-721 contract', riskLevel: 'read', requiresConfirmation: false, networks: ['goat-mainnet', 'goat-testnet'], zodInputSchema: inputSchema, async execute(ctx, input) { const abi = [ 'function ownerOf(uint256 tokenId) view returns (address)', 'function tokenURI(uint256 tokenId) view returns (string)', ]; const owner = await wallet.callContract( input.contractAddress, abi, 'ownerOf', [BigInt(input.tokenId)] ); const metadata = await wallet.callContract( input.contractAddress, abi, 'tokenURI', [BigInt(input.tokenId)] ); return { owner: owner as string, metadata: metadata as string, }; }, };}
Then register it:
src/index.ts
import { ActionProvider } from '@goatnetwork/agentkit/providers';import { myAction } from './actions/my-action';const provider = new ActionProvider();provider.register(myAction(wallet));
If your action returns sensitive data (tokens, secrets, keys), declare them in sensitiveOutputFields. These values will be replaced with [REDACTED] in execution hook events and logs, but the original values are still returned in the ExecutionResult.output:
AgentKit validates inputs automatically. If you provide a zodInputSchema, Zod validation runs before the action executes. If validation fails, the runtime returns an error result without calling execute:
The published @goatnetwork/agentkit package does not expose the internal evmAddress validator as a public import. For copy-paste-safe examples, define a local z.string().regex(...) helper inside your app.