GOAT Network
AgentKit

Custom Tools

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).

Using customActionProvider

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 ready
const tools = myProvider.openAITools();
console.log(tools);

CustomActionInput Interface

Interface
interface CustomActionInput<TInput = unknown, TOutput = unknown> {
  name: string;             // Unique action name (e.g. 'my_plugin.action_name')
  description: string;      // Human-readable description for the LLM
  schema: ZodTypeAny;       // Zod schema for input validation
  invoke: (input: TInput) => Promise<TOutput>;  // The action implementation
  riskLevel?: RiskLevel;    // 'read' | 'low' | 'medium' | 'high' (default: 'low')
  networks?: string[];      // Restrict to specific networks (default: all)
  requiresConfirmation?: boolean;  // Require explicit confirmation (default: false)
}

Implementing ActionDefinition Directly

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 types
interface 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 schema
const inputSchema = z.object({
  contractAddress: evmAddress,
  tokenId: z.string().regex(/^\d+$/, 'tokenId must be a numeric string'),
});

// Create a factory function that accepts dependencies
export 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));

ActionDefinition Interface

The full ActionDefinition interface gives you access to all features:

Interface
interface ActionDefinition<TInput = unknown, TOutput = unknown> {
  name: string;                          // Unique action identifier
  description: string;                   // Human-readable description for LLMs
  riskLevel: 'read' | 'low' | 'medium' | 'high';
  requiresConfirmation: boolean;
  networks: string[];                    // Empty array = all networks
  inputSchema?: Record<string, unknown>; // JSON Schema (alternative to Zod)
  outputSchema?: Record<string, unknown>;
  zodInputSchema?: ZodTypeAny;           // Preferred: Zod schema
  zodOutputSchema?: ZodTypeAny;
  sensitiveOutputFields?: string[];      // Fields to redact in hook events
  execute: (ctx: ActionContext, input: TInput) => Promise<TOutput>;
}

ActionContext

Every action receives a context object with trace information and an abort signal:

Interface
interface ActionContext {
  traceId: string;           // Unique trace ID for logging
  network: string;           // Target network (e.g. 'goat-testnet')
  caller?: string;           // Optional caller identifier
  now: number;               // Timestamp (Date.now())
  signal?: AbortSignal;      // For cancellation support
  accessToken?: string;      // Per-request bearer token (not included in logs)
}

Combining Built-in and Custom Actions

You can mix built-in plugins with custom actions in the same ActionProvider:

src/index.ts
import { ActionProvider, customActionProvider } from '@goatnetwork/agentkit/providers';
import { walletBalanceAction, NoopWalletReadAdapter } from '@goatnetwork/agentkit/plugins';

// Start with a fresh provider
const provider = new ActionProvider();

// Register built-in actions
provider.register(walletBalanceAction(new NoopWalletReadAdapter()));

// Register custom actions individually
const customProvider = customActionProvider([
  {
    name: 'custom.greet',
    description: 'Say hello',
    schema: z.object({ name: z.string() }),
    invoke: async (input) => ({ greeting: `Hello ${input.name}` }),
  },
]);

// Transfer custom actions to the main provider
for (const action of customProvider.list()) {
  provider.register(action);
}

// All actions are now available
console.log(provider.openAITools());

Sensitive Output Fields

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:

Example
{
  name: 'auth.get_token',
  sensitiveOutputFields: ['access_token', 'refresh_token'],
  // ...
}

Input Validation

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:

Validation Error Result
{
  ok: false,
  errorCode: 'INVALID_INPUT',
  error: 'Schema validation failed: to: Invalid EVM address',
  traceId: 'trace_001',
  action: 'my_plugin.transfer',
  attempts: 0
}

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.

On this page