Skip to main content

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:

src/index.ts
import "reflect-metadata"; // Required for decorator metadata
Warning

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:

src/services/MyFunctions.ts
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

src/services/MyFunctions.ts
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.

src/services/examples.ts
@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.

src/services/examples.ts
// ✅ 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")
Tip

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" }

src/services/examples.ts
// 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

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

src/services/examples.ts
// 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:

src/types/User.ts
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;
}
}
Tip

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

PropertyTypeDescription
descriptionstringWhat this object represents
propertiesobjectMap of property definitions
properties[key].typestringThe property type (string, number, boolean, object, array)
properties[key].descriptionstringWhat this property represents

Complete Examples

Calculator Function

src/services/calculator.ts
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

src/services/crm.ts
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 };
}
}
Info

📦 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
// 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:

src/services/CRMFunctions.ts
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:

src/index.ts
const db = new Database(connectionString);
const crmFunctions = new CRMFunctions(db);

await daemo.registerService(crmFunctions);

Best Practices

1. Be Specific in Descriptions

src/services/examples.ts
// ✅ 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

src/services/examples.ts
@Input({
id: "string: Required - The contact's ID",
fields: "string[]: Optional - Specific fields to return"
})

3. Return Meaningful Data

src/services/examples.ts
// ✅ 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

src/services/contacts.ts
@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:

  1. The SDK extracts all metadata (names, descriptions, types)
  2. This metadata is sent to Daemo when your service starts
  3. The AI uses this information to understand and call your functions
  4. Your actual code runs locally when the AI invokes a tool
Note

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: