This skill should be used when the user asks to "create a hedera plugin", "build a hedera agent kit plugin", "extend hedera agent kit", "create custom hedera tools", "add hedera functionality",...
This skill provides guidance for creating custom plugins that extend the Hedera Agent Kit. Plugins allow adding new tools for Hedera network interactionsβtoken operations, account management, consensus service, smart contracts, and custom integrations.
To create a Hedera plugin in 5 steps:
@hashgraph/hedera-agent-kit and @hiero-ledger/sdkindex.ts with the plugin definition and a tools/ directoryBaseTool β this opts the tool into the hooks/policies lifecycleconfiguration.plugins arrayEvery Hedera plugin implements this interface from @hashgraph/hedera-agent-kit:
import { Plugin } from '@hashgraph/hedera-agent-kit';
export interface Plugin {
name: string; // Unique kebab-case identifier
version?: string; // Semantic version (e.g., "1.0.0")
description?: string; // Brief explanation of plugin purpose
tools: (context: Context) => Tool[]; // Factory returning tools
}
The tools function receives a Context object containing network configuration and returns an array of Tool objects. BaseTool itself implements Tool, so a BaseTool subclass instance is a valid return value.
BaseToolTools are written as classes extending BaseTool, which implements Tool and runs a 7-stage lifecycle:
[1] preToolExecutionHook
[2] normalizeParams β your logic
[3] postParamsNormalizationHook
[4] coreAction β your logic
[5] postCoreActionHook
[6] secondaryAction β your logic (skipped via shouldSecondaryAction for queries)
[7] postToolExecutionHook
Hooks and policies registered on the agent kit fire at stages 1, 3, 5, and 7 automatically β you never call them manually.
A subclass must provide:
| Member | Type | Notes |
|---|---|---|
method |
string |
Snake-case identifier with _tool suffix |
name |
string |
Human-readable display name |
description |
string |
LLM-facing description (typically built in the constructor from a prompt function) |
parameters |
z.ZodObject |
Zod schema for input validation |
normalizeParams(params, context, client) |
async | Stage 2 β resolve defaults, validate format |
coreAction(normalised, context, client) |
async | Stage 4 β build the transaction or perform the query |
secondaryAction(request, client, context) |
async | Stage 6 β sign/submit the transaction (no-op for queries) |
Optional overrides: outputParser, shouldSecondaryAction (return false for query-only tools), handleError, and the four hook methods.
Mutation tools β state-changing operations (token create/mint/transfer, account update, topic submit). coreAction builds the Transaction, secondaryAction calls handleTransaction(). Use transactionToolOutputParser.
Query tools β read-only operations (token info, balances, mirror-node fetches). coreAction returns the result directly. Override shouldSecondaryAction to return false. Use untypedQueryOutputParser.
Follow this structure for all Hedera plugins:
my-hedera-plugin/
βββ index.ts # Plugin definition and exports
βββ tools/
βββ category/ # Group related tools
βββ create-something.ts
βββ get-something.ts
export const MY_TOOL_NAME = 'my_tool_name_tool';
Use UPPER_SNAKE_CASE with _TOOL suffix for the constant. The value should be lowercase snake_case.
const myToolPrompt = (context: Context = {}) => {
return `This tool does X on Hedera.
Parameters:
- param1 (str, required): Description of param1
- param2 (int, optional): Description of param2, defaults to 0`;
};
import { z } from 'zod';
const myToolParameters = (_context: Context = {}) => {
return z.object({
param1: z.string().describe('Description of param1'),
param2: z.number().optional().describe('Description of param2'),
});
};
type MyToolParams = z.infer<ReturnType<typeof myToolParameters>>;
See references/zod-schema-patterns.md for common Hedera parameter patterns.
import { Client, Status } from '@hiero-ledger/sdk';
import {
BaseTool,
Context,
handleTransaction,
RawTransactionResponse,
transactionToolOutputParser,
} from '@hashgraph/hedera-agent-kit';
const postProcess = (response: RawTransactionResponse) =>
`Operation completed.
Transaction ID: ${response.transactionId}`;
export class MyTool extends BaseTool<MyToolParams, MyToolParams> {
method = MY_TOOL_NAME;
name = 'My Tool Display Name';
description: string;
parameters: ReturnType<typeof myToolParameters>;
outputParser = transactionToolOutputParser;
constructor(context: Context) {
super();
this.description = myToolPrompt(context);
this.parameters = myToolParameters(context);
}
// Stage 2 β resolve defaults
async normalizeParams(params: MyToolParams) {
return params;
}
// Stage 4 β build the transaction (do not sign here)
async coreAction(params: MyToolParams, _context: Context, _client: Client) {
// return new SomeTransaction()...;
return undefined as any;
}
// Stage 6 β sign and submit
async secondaryAction(transaction: any, client: Client, context: Context) {
return handleTransaction(transaction, client, context, postProcess);
}
async handleError(error: unknown) {
const message = 'Failed to execute' + (error instanceof Error ? `: ${error.message}` : '');
console.error(`[${MY_TOOL_NAME}]`, message);
return { raw: { status: Status.InvalidTransaction, error: message }, humanMessage: message };
}
}
const tool = (context: Context): BaseTool => new MyTool(context);
export default tool;
The factory pattern lets the plugin's tools(context) function instantiate a fresh tool per request with the right context bound.
For read-only tools, override shouldSecondaryAction so the lifecycle stops after coreAction, and have secondaryAction pass the result through unchanged:
export class MyQueryTool extends BaseTool<MyToolParams, MyToolParams> {
// ... method/name/description/parameters as above ...
outputParser = untypedQueryOutputParser;
async normalizeParams(params: MyToolParams) {
return params;
}
async coreAction(params: MyToolParams, _context: Context, client: Client) {
return await someHederaQuery(params, client);
}
async shouldSecondaryAction() {
return false;
}
async secondaryAction(result: any) {
return result;
}
}
import { Context, Plugin } from '@hashgraph/hedera-agent-kit';
import myTool, { MY_TOOL_NAME } from './tools/category/my-tool';
export const myPlugin: Plugin = {
name: 'my-hedera-plugin',
version: '1.0.0',
description: 'A plugin for custom Hedera operations',
tools: (context: Context) => {
return [
myTool(context),
];
},
};
export const myPluginToolNames = {
MY_TOOL_NAME,
} as const;
export default { myPlugin, myPluginToolNames };
secondaryAction typically delegates to handleTransaction, which calls a postProcess function with the raw transaction response:
const postProcess = (response: RawTransactionResponse) => {
if (response.scheduleId) {
return `Scheduled transaction created.
Transaction ID: ${response.transactionId}
Schedule ID: ${response.scheduleId.toString()}`;
}
return `Operation completed.
Transaction ID: ${response.transactionId}`;
};
RETURN_BYTES mode β raw.bytes is a Uint8ArrayWhen a tool runs in RETURN_BYTES mode, raw.bytes returned by ResponseParserService is always a plain Uint8Array in both Node.js and browser environments. Pass it directly to Transaction.fromBytes() β no manual conversion is needed.
const bytes = toolCall.parsedData.raw.bytes;
const tx = Transaction.fromBytes(bytes);
| Element | Convention | Example |
|---|---|---|
| Plugin name | kebab-case | my-token-plugin |
| Plugin variable | camelCase | myTokenPlugin |
| Tool class | PascalCase + Tool |
CreateTokenTool |
| Tool constant | UPPER_SNAKE_CASE + _TOOL |
CREATE_TOKEN_TOOL |
| Tool method value | snake_case + _tool |
create_token_tool |
| Tool file | kebab-case | create-token.ts |
| Tool names export | camelCase + ToolNames |
myTokenPluginToolNames |
// From @hashgraph/hedera-agent-kit (core)
import {
Context,
Plugin,
BaseTool,
handleTransaction,
RawTransactionResponse,
transactionToolOutputParser,
untypedQueryOutputParser,
} from '@hashgraph/hedera-agent-kit';
// Built-in plugins live in the /plugins subpath
import { coreTokenPlugin, coreAccountPlugin } from '@hashgraph/hedera-agent-kit/plugins';
// Hedera SDK is a peer dependency
import { Client, Status } from '@hiero-ledger/sdk';
// For parameter validation
import { z } from 'zod';
For detailed patterns and techniques, consult:
references/plugin-interface.md - Plugin interface, BaseTool lifecycle in depthreferences/zod-schema-patterns.md - Common Zod schemas for Hedera parametersreferences/prompt-patterns.md - Prompt generation patterns for tool descriptionsreferences/error-handling.md - Error handling and output parsing patternsWorking examples in examples/:
examples/simple-plugin/ - Minimal BaseTool query (starter template)examples/token-plugin/ - Mutation (CreateTokenTool) and query (GetTokenInfoTool) BaseTool subclassestools/<DoesWhat>Tool (e.g., CreateTokenTool)normalizeParams, not in coreAction β keeps the build step purecoreAction β submission belongs in secondaryAction where hooks can reacthandleError so logs are tagged with the tool method.describe() β every field; descriptions guide the agentPlugins are passed explicitly to the toolkit you choose. Toolkits live in dedicated packages β pick the one that matches your framework:
| Framework | Toolkit package |
|---|---|
| LangChain | @hashgraph/hedera-agent-kit-langchain |
| Vercel AI SDK | @hashgraph/hedera-agent-kit-ai-sdk |
| ElizaOS | @hashgraph/hedera-agent-kit-elizaos |
| MCP | @hashgraph/hedera-agent-kit-mcp |
Empty
pluginsarray means zero tools. List every plugin you need explicitly, or useallCorePluginsfrom the/pluginssubpath to load every built-in core plugin at once.
import { Client, PrivateKey } from '@hiero-ledger/sdk';
import { AgentMode } from '@hashgraph/hedera-agent-kit';
import { coreTokenPlugin, coreAccountPlugin } from '@hashgraph/hedera-agent-kit/plugins';
import { HederaLangchainToolkit } from '@hashgraph/hedera-agent-kit-langchain';
import { myPlugin } from './my-hedera-plugin';
const client = Client.forTestnet().setOperator(
process.env.ACCOUNT_ID!,
PrivateKey.fromStringECDSA(process.env.PRIVATE_KEY!),
);
const toolkit = new HederaLangchainToolkit({
client,
configuration: {
plugins: [coreTokenPlugin, coreAccountPlugin, myPlugin],
context: { mode: AgentMode.AUTONOMOUS },
},
});
const tools = toolkit.getTools();
To load every built-in core plugin at once, import allCorePlugins:
import { allCorePlugins } from '@hashgraph/hedera-agent-kit/plugins';
// ...
plugins: [...allCorePlugins, myPlugin],
index.ts and tools/ subdirectoryBaseTool subclass and a factory function in its own fileindex.ts along with tool name constantsconfiguration.plugins arrayFor complete working examples, see the examples/ directory.