🛠️ Creating Custom Tools
Custom tools are reusable components that encapsulate specific functionality in your Loopstack workflows. They can accept arguments, use dependency injection, and integrate seamlessly with the workflow execution.
Basic Tool Structure
A custom tool extends the Tool base class and implements the execute() method:
import { BlockConfig, HandlerCallResult } from '@loopstack/shared';
import { z } from 'zod';
import { Tool } from '@loopstack/core';
const propertiesSchema = z.object({
a: z.number(),
b: z.number()
});
@BlockConfig({
config: {
description: 'Add two numbers together',
},
properties: propertiesSchema,
})
export class MathSumTool extends Tool {
async execute(): Promise<HandlerCallResult> {
const sum = this.args.a + this.args.b;
return {
data: sum
};
}
}Key Components
1. BlockConfig Decorator
The @BlockConfig decorator defines the tool’s metadata and validation:
config: Basic configuration including descriptionproperties: Zod schema for validating runtime argumentsconfigSchema: Zod schema for validating workflow configuration (optional)
2. Properties Schema
Define the expected arguments using Zod:
const propertiesSchema = z.object({
a: z.number(),
b: z.number()
});3. Config Schema (Optional)
Allow template expressions in workflow configuration:
import { TemplateExpression } from '@loopstack/shared';
const NumberOrTemplateExpression = z.union([
TemplateExpression, // Allow template expressions like ${ args.a }
z.number(), // Allow literal values
]);
const configSchema = z.object({
a: NumberOrTemplateExpression,
b: NumberOrTemplateExpression,
});4. Execute Method
The execute() method contains your tool’s logic. Access arguments via this.args:
async execute(): Promise<HandlerCallResult> {
const result = this.args.a + this.args.b;
return {
data: result
};
}Dependency Injection
Tools support constructor-based dependency injection. Loopstack Tools are NestJs Services with transient scope by default:
import { MathService } from '../services/math.service';
@BlockConfig({
// ... configuration
})
export class MathSumTool extends Tool {
constructor(private mathService: MathService) {
super(); // Required: call parent constructor
}
async execute(): Promise<HandlerCallResult> {
const sum = this.mathService.sum(this.args.a, this.args.b);
return {
data: sum
};
}
}Using Tools in Workflows
1. Import the Tool
Add your tool to the workflow’s imports:
@BlockConfig({
imports: [
MathSumTool,
CreateChatMessage,
],
// ...
})
export class CustomToolExampleWorkflow extends Workflow {
// ...
}2. Register as Provider
Add the tool to your module’s providers:
@Module({
providers: [
CustomToolExampleWorkflow,
MathSumTool,
MathService, // Don't forget service dependencies
// ...
],
})
export class ExampleModule {}3. Call in Workflow YAML
Use the tool in your workflow transitions:
transitions:
- id: calculate
from: start
to: end
call:
- id: calculation
tool: MathSumTool
args:
a: ${ args.a }
b: ${ args.b }
# Access the result
- tool: CreateChatMessage
args:
role: 'assistant'
content: |
Result: {{ state.currentTransitionResults.calculation.data }}