Defining Tools
Tools are functions that AI can call. You define them using TypeScript decorators that describe what each function does.
Required Setup
Before using decorators, you must import reflect-metadata once at the top of your entry file:
import "reflect-metadata"; // Required for decorator metadata
Without this import, decorators won't work correctly. Add it at the very top of your index.ts or app.ts.
The Basics
The SDK provides two decorator styles. Use whichever you prefer:
Style 1: @DaemoFunction (Recommended)
import { DaemoFunction } from 'daemo-engine';
import "reflect-metadata";
export class MyFunctions {
@DaemoFunction({
description: "What this function does",
returnType: MyReturnClass // Optional: for complex return types
})
async myFunction(id: string): Promise<Result> {
return { result: "something" };
}
}
Style 2: @DaemoTool with separate decorators
import { DaemoTool, Description, Input, Output } from 'daemo-engine';
import "reflect-metadata";
export class MyFunctions {
@DaemoTool() // Mark as a tool
@Description("What this function does") // Human-readable description
@Input({ param: "type: description" }) // Input parameters
@Output({ result: "type: description" }) // Return type
async myFunction(args: { param: string }) {
return { result: "something" };
}
}
Let's break down each approach.
@DaemoTool()
Marks a method as a Daemo tool. Without this, the function won't be registered.
@DaemoTool() // Required - makes this function available to AI
async myFunction(args: { ... }) { ... }
@Description()
Tells the AI what your function does. This is critical — the AI uses this to decide when to call your function.
// ✅ Good - specific and clear
@Description("Calculates the power of a number (base raised to exponent)")
// ❌ Bad - too vague
@Description("Does math")
// ✅ Good - explains the use case
@Description("Searches for contacts by email address, supports partial matching")
// ❌ Bad - doesn't explain what it does
@Description("Contact search")
Write descriptions for the AI. Imagine you're explaining to a smart assistant what this function does and when they should use it.
@Input()
Defines the parameters your function accepts. Format: { paramName: "type: description" }
// Single parameter
@Input({ email: "string: The email address to search for" })
// Multiple parameters
@Input({
a: "number: The base number",
b: "number: The exponent"
})
// Complex example
@Input({
contactId: "string: The unique ID of the contact",
fields: "string[]: Optional list of fields to return",
includeDeleted: "boolean: Whether to include soft-deleted contacts"
})
Supported Types
| Type | Example |
|---|---|
string | "string: User's email" |
number | "number: Amount in cents" |
boolean | "boolean: Is active" |
string[] | "string[]: List of tags" |
number[] | "number[]: Array of IDs" |
object | "object: User preferences" |
@Output()
Defines what your function returns. Same format as @Input.
// Simple output
@Output({ result: "number: The calculated result" })
// Multiple return values
@Output({
success: "boolean: Whether the operation succeeded",
data: "object: The created record",
id: "string: The new record's ID"
})
// Array output
@Output({
contacts: "object[]: List of matching contacts",
total: "number: Total count of matches"
})
@DaemoSchema for Complex Types
When returning complex objects, use @DaemoSchema to give the AI rich, detailed type information:
import { DaemoFunction, DaemoSchema } from "daemo-engine";
import "reflect-metadata";
@DaemoSchema({
description: "Represents a user in the system.",
properties: {
id: { type: "string", description: "The user's unique ID." },
name: { type: "string", description: "The user's full name." },
email: { type: "string", description: "The user's email address." },
createdAt: { type: "string", description: "ISO date when user was created." },
},
})
class User {
id: string = "";
name: string = "";
email: string = "";
createdAt: string = "";
}
class UserService {
private users: User[] = [
{ id: "1", name: "Alice", email: "alice@example.com", createdAt: "2024-01-15" },
{ id: "2", name: "Bob", email: "bob@example.com", createdAt: "2024-02-20" },
];
@DaemoFunction({
description: "Gets a user by their unique ID.",
returnType: User, // Reference the schema-decorated class
})
async getUserById(id: string): Promise<User | null> {
return this.users.find((u) => u.id === id) || null;
}
@DaemoFunction({
description: "Gets all users in the system.",
returnType: [User], // Array of User
})
async getAllUsers(): Promise<User[]> {
return this.users;
}
}
Use @DaemoSchema for DTOs. When you have complex data structures that get passed around, decorating them with @DaemoSchema gives the AI precise context about each field — making it much better at understanding and using your data.
Schema Properties
| Property | Type | Description |
|---|---|---|
description | string | What this object represents |
properties | object | Map of property definitions |
properties[key].type | string | The property type (string, number, boolean, object, array) |
properties[key].description | string | What this property represents |
Complete Examples
Calculator Function
import { DaemoTool, Description, Input, Output } from 'daemo-engine';
export class CalculatorFunctions {
@DaemoTool()
@Description("Computes one number raised to the power of another (a^b)")
@Input({
a: "number: The base number",
b: "number: The exponent"
})
@Output({ result: "number: The computed result" })
async power(args: { a: number; b: number }) {
return { result: Math.pow(args.a, args.b) };
}
@DaemoTool()
@Description("Adds two numbers together")
@Input({
a: "number: First number",
b: "number: Second number"
})
@Output({ sum: "number: The sum of a and b" })
async add(args: { a: number; b: number }) {
return { sum: args.a + args.b };
}
}
CRM Functions
import { DaemoTool, Description, Input, Output } from 'daemo-engine';
export class CRMFunctions {
@DaemoTool()
@Description("Get all contacts from the CRM, optionally filtered by owner")
@Input({
ownerId: "string: Optional - filter by owner ID"
})
@Output({
contacts: "object[]: Array of contact records",
count: "number: Total number of contacts"
})
async getAllContacts(args: { ownerId?: string }) {
// Your database query here
const contacts = await this.db.contacts.find(
args.ownerId ? { ownerId: args.ownerId } : {}
);
return { contacts, count: contacts.length };
}
@DaemoTool()
@Description("Search for contacts by email address (supports partial matching)")
@Input({
email: "string: The email address or partial email to search for"
})
@Output({
contacts: "object[]: Matching contacts",
count: "number: Number of matches"
})
async searchContactsByEmail(args: { email: string }) {
const contacts = await this.db.contacts.find({
email: { $regex: args.email, $options: 'i' }
});
return { contacts, count: contacts.length };
}
@DaemoTool()
@Description("Create a new contact in the CRM")
@Input({
firstName: "string: Contact's first name",
lastName: "string: Contact's last name",
email: "string: Contact's email address (must be unique)",
company: "string: Optional - Company name",
phone: "string: Optional - Phone number"
})
@Output({
success: "boolean: Whether the contact was created",
contact: "object: The created contact record",
id: "string: The new contact's ID"
})
async createContact(args: {
firstName: string;
lastName: string;
email: string;
company?: string;
phone?: string;
}) {
const contact = await this.db.contacts.create(args);
return { success: true, contact, id: contact.id };
}
}
📦 Real-World Example: See how the SF 311 Agent defines 4 coordinated tools that query 8M+ rows of city data. The sf311Functions.ts file shows production patterns including Zod schemas and complex query building.
Organizing Functions
By Domain
Group related functions into classes:
// src/services/contacts.ts
export class ContactFunctions {
@DaemoTool() async getContact(...) { }
@DaemoTool() async createContact(...) { }
@DaemoTool() async updateContact(...) { }
}
// src/services/deals.ts
export class DealFunctions {
@DaemoTool() async getDeal(...) { }
@DaemoTool() async createDeal(...) { }
@DaemoTool() async updateDealStage(...) { }
}
Class Dependencies
If your functions need database connections or other dependencies:
export class CRMFunctions {
private db: Database;
constructor(db: Database) {
this.db = db;
}
@DaemoTool()
@Description("Get a contact by ID")
@Input({ id: "string: The contact's unique ID" })
@Output({ contact: "object: The contact record" })
async getContact(args: { id: string }) {
const contact = await this.db.contacts.findById(args.id);
return { contact };
}
}
Then instantiate with dependencies:
const db = new Database(connectionString);
const crmFunctions = new CRMFunctions(db);
await daemo.registerService(crmFunctions);
Best Practices
1. Be Specific in Descriptions
// ✅ Good
@Description("Get all deals at a specific stage in the sales pipeline (Lead Identified, Meeting Scheduled, Demo Completed, Proposal Sent, Follow-Up, Contract Sent, Closed Won, Closed Lost)")
// ❌ Bad
@Description("Get deals")
2. Document Required vs Optional
@Input({
id: "string: Required - The contact's ID",
fields: "string[]: Optional - Specific fields to return"
})
3. Return Meaningful Data
// ✅ Good - returns useful info
@Output({
success: "boolean: Whether the operation succeeded",
contact: "object: The updated contact",
changes: "object: What fields were modified"
})
// ❌ Less helpful
@Output({ ok: "boolean: Success" })
4. Handle Errors Gracefully
@DaemoTool()
@Description("Delete a contact by ID")
@Input({ id: "string: The contact's ID" })
@Output({
success: "boolean: Whether deletion succeeded",
error: "string: Error message if failed"
})
async deleteContact(args: { id: string }) {
try {
await this.db.contacts.delete(args.id);
return { success: true };
} catch (err) {
return { success: false, error: err.message };
}
}
What Happens Next
When you define tools with these decorators:
- The SDK extracts all metadata (names, descriptions, types)
- This metadata is sent to Daemo when your service starts
- The AI uses this information to understand and call your functions
- Your actual code runs locally when the AI invokes a tool
Your code stays with you. Only the metadata (function signatures and descriptions) is sent to Daemo. Your business logic never leaves your environment.
Next Steps
Now that you have tools defined, let's register them:
- Registering Services — Connect your functions to Daemo