AI functions
Extend AI agents with custom backend logic they can call during conversations.
Why Use AI Functions
Your AI agent needs to do more than answer questions. It needs to look up order status, update a database, run a domain-specific calculation, or call an external API. The model doesn't know how to do any of that on its own.
Without AI functions, the agent is limited to what the model already knows. With AI functions, the agent can call your backend code whenever the conversation requires it:
// Backend: define the function
@aiFunction<{ shipName: string }>(
'Returns the list of pirates on a given ship. Call when the user asks about a ship crew.',
[{ name: 'shipName', description: 'The name of the ship', type: 'string', required: true }],
)
async listPiratesOnShip(params: { shipName: string }): Promise<string> {
// Your custom logic: query a database, call an API, run a calculation, etc.
const { shipName } = params;
const crew = await this.lookupCrew(shipName);
return crew.join(', ');
}
// Frontend: pass the function to the agent and ask a question
const response = await squid
.ai()
.agent('pirate-agent')
.ask('Who is on the Black Pearl?', {
functions: ['listPiratesOnShip'],
});
Functions can also be added permanently to an agent via the Agent Studio or setAgentOptionInPath, instead of passing them per request.
The agent reads the function's description, decides when to call it, and incorporates the result into its response.
Overview
AI functions are methods in a Squid Service decorated with @aiFunction. When you attach them to an AI agent, the agent can invoke them during a conversation based on the function's description and the user's prompt.
When to use AI functions
| Use Case | Recommendation |
|---|---|
| Agent needs to call custom server-side logic | AI function |
| Agent needs to query or write to a connected database | Use a database connector, or an AI function for custom query logic |
| Agent needs to access a connected service (e.g. CRM, calendar, API) | Use a connected integration |
| Agent needs to connect to external tools via MCP | Use MCP |
| Agent needs to search uploaded documents | Use Knowledge Bases |
How it works
- You decorate a method with
@aiFunctionin a class that extendsSquidService - You attach the function to an agent by name (via the SDK or the Agent Studio in the console)
- When a user sends a message, the agent evaluates the function's description against the prompt
- If the agent determines the function is relevant, it calls the function with AI-generated parameter values
- Your function runs on the backend and returns a string result
- The agent incorporates the result into its response
Quick Start
Prerequisites
- A Squid backend project initialized with
squid init-backend - The
@squidcloud/backendpackage installed - An AI agent created
Step 1: Create an AI function
Add a method with the @aiFunction decorator in a Squid Service:
import { SquidService, aiFunction } from '@squidcloud/backend';
export class AiService extends SquidService {
@aiFunction<{ city: string }>('Returns the current weather for a given city. Call when the user asks about weather.', [{ name: 'city', description: 'The city name', type: 'string', required: true }])
async getWeather(params: { city: string }): Promise<string> {
const { city } = params;
// Replace with your actual weather API call
return `The weather in ${city} is 72°F and sunny.`;
}
}
Step 2: Deploy the backend
For local development, run:
squid start
To deploy to the cloud, see deploying your backend.
After deployment, the function appears under Abilities in the Agent Studio in the console.
Step 3: Call the agent with the function
const response = await squid
.ai()
.agent('my-agent')
.ask("What's the weather in Tokyo?", {
functions: ['getWeather'],
});
console.log(response);
The agent sees the user asked about weather, calls getWeather with { city: "Tokyo" }, and includes the result in its response.
You can also add functions permanently to the agent using setAgentOptionInPath, so you don't need to pass them on every request:
await squid.ai().agent('my-agent').setAgentOptionInPath('functions', ['getWeather']);
// Now the agent always has access to getWeather
const response = await squid.ai().agent('my-agent').ask("What's the weather in Tokyo?");
Core Concepts
The @aiFunction decorator
The decorator accepts two required arguments:
description(string): Tells the agent when to call this function. Write it as a clear instruction, not a vague label.params(array): Defines the parameters the agent should provide when calling the function.
@aiFunction<{ productId: string; quantity: number }>(
'Updates the stock quantity for a product. Call when the user wants to adjust inventory.',
[
{ name: 'productId', description: 'The product ID', type: 'string', required: true },
{ name: 'quantity', description: 'Amount to add (negative to subtract)', type: 'number', required: true },
],
)
async updateStock(params: { productId: string; quantity: number }): Promise<string> {
// ...
}
Alternatively, you can pass the decorator a single options object:
@aiFunction({
description: 'Updates the stock quantity for a product.',
params: [
{ name: 'productId', description: 'The product ID', type: 'string', required: true },
{ name: 'quantity', description: 'Amount to add (negative to subtract)', type: 'number', required: true },
],
})
async updateStock(params: { productId: string; quantity: number }): Promise<string> {
// ...
}
Parameter definitions
Each parameter in the params array supports these fields:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Parameter name, matching the key in your function's params object |
description | string | Yes | Tells the agent what value to provide |
type | string | Yes | Data type: 'string', 'number', 'boolean', etc. |
required | boolean | Yes | Whether the agent must provide this value |
enum | any[] | No | Restricts the value to a set of allowed options |
Use enum to constrain values when only specific options are valid:
@aiFunction('Saves a section in a document', [
{
name: 'sectionId',
type: 'string',
description: 'Which section to update in the document',
required: true,
enum: ['introduction', 'background', 'methodology', 'results', 'conclusion'],
},
{
name: 'content',
type: 'string',
description: 'The content of the section',
required: true,
},
])
async saveSection(params: { sectionId: string; content: string }): Promise<string> {
const { sectionId, content } = params;
const docRef = this.squid.collection('documents').doc('my-doc');
await docRef.update({ [sectionId]: content });
return 'Section saved: ' + sectionId;
}
Return values
AI functions must return a Promise<string>. The agent receives this string and uses it to formulate its response. Return a clear, concise result:
// Good: returns useful information the agent can relay
return 'Order #1234 shipped on March 5. Tracking number: ABC123';
// Bad: returns raw JSON the agent has to interpret
return JSON.stringify(orderObject);
Attributes
Attributes let you bind an AI function to a specific connector type, extending that connector's built-in capabilities with your own custom logic. When you add a connector of that type to an agent, the function is automatically included alongside the connector's default behavior.
For example, a database connector already lets the agent query data with AI. But if you want the agent to consistently query in a specific way, you can write an AI function attributed to that connector type:
@aiFunction({
description: 'Retrieves recent orders for a customer from the PostgreSQL database',
params: [
{
name: 'customerEmail',
description: 'The email address of the customer',
type: 'string',
required: true,
},
],
attributes: {
integrationType: ['postgres'],
},
})
async getCustomerOrders(
{ customerEmail }: { customerEmail: string },
{ integrationId }: AiFunctionCallContextWithIntegration,
): Promise<string> {
// Custom query logic.
//
// Squid provides "Query with AI" where the agent can write the query and execute it to
// accomplish a task, but if you want it to consistently query in a certain way, you can write
// an AI Function that you can instruct it to call instead.
}
When you add your PostgreSQL connector to an agent, this function is automatically included.
Functions attributed to a connector type do not appear in the AI Functions list in the Agent Studio. Their inclusion with the agent is handled by the connector.
Categories
Categories let you group AI functions in the Agent Studio for easier organization:
@aiFunction({
description: 'Get all new user reviews from the product listing on Amazon.',
params: [
{
name: 'productId',
description: 'The ID of the listing on Amazon, e.g. "B094D3JGLT" for the URL "https://www.amazon.com/dp/B094D3JGLT"',
type: 'string',
required: true,
},
{
name: 'cutoffDate',
description: 'The cutoff date. Only reviews newer than this date should be returned. Use ISO8601 format. Defaults to returning all reviews.',
type: 'string',
required: false,
},
],
categories: ['Data Gathering'],
})
async getProductReviews(
{ productId, cutoffDate }: { productId: string; cutoffDate?: string },
{ integrationId }: AiFunctionCallContextWithIntegration,
): Promise<string> {
// Your logic to gather the user reviews.
}
This causes getProductReviews to appear under a "Data Gathering" category in the AI Functions list. You can provide more than one category, and the function appears under each.
Overriding parameter values
For some agents, you may want to fix certain parameter values rather than letting the AI decide. You can override parameters using predefinedParameters. The AI will not know that overridden parameters exist and cannot set their values.
For example, using the saveSection function from earlier, you can create an agent that only updates the introduction section:
Via the chat widget:
<squid-chat-widget
...
squid-ai-agent-chat-options={{
functions: [{
name: 'saveSection',
predefinedParameters: { sectionId: 'introduction' }
}]
}}
...
>
</squid-chat-widget>
Via the SDK:
const result = await squid
.ai()
.agent('my-agent')
.ask('Save that content.', {
functions: [
{
name: 'saveSection',
predefinedParameters: { sectionId: 'introduction' },
},
],
});
The agent can only provide the content parameter. The sectionId is always 'introduction'.
Passing context to AI functions
In addition to the AI-generated params, you can pass your own values into AI functions using the context object. This is useful for data the AI should not control, such as document IDs, user preferences, or security-related values.
The context has two scopes:
agentContext: Passed to every AI function call made by the agent. Access it viactx.agentContext.functionContext: Passed to a specific function only. Access it viactx.functionContext.
Example
Building on the saveSection function, suppose you want the document ID to be set at the agent level and you want to redact internal codenames from content before saving:
import { SquidService, aiFunction, AiFunctionCallContext } from '@squidcloud/backend';
const SECTION_IDS = ['introduction', 'background', 'methodology', 'results', 'conclusion'] as const;
type SectionId = (typeof SECTION_IDS)[number];
interface DocumentIdAgentContext {
documentId: string;
}
interface RedactionFunctionContext {
codenameList: string[];
}
class DocumentService extends SquidService {
@aiFunction('Saves a section in a document', [
{
name: 'sectionId',
type: 'string',
description: 'Which section to update in the document',
required: true,
enum: [...SECTION_IDS],
},
{
name: 'content',
type: 'string',
description: 'The content of the section',
required: true,
},
])
async saveSection({ sectionId, content }: { sectionId: SectionId; content: string }, ctx: AiFunctionCallContext<RedactionFunctionContext, DocumentIdAgentContext>): Promise<string> {
const docRef = this.squid.collection('documents').doc(ctx.agentContext.documentId);
let censoredContent = content;
for (const censorWord of ctx.functionContext.codenameList) {
censoredContent = censoredContent.replaceAll(censorWord, 'REDACTED');
}
await docRef.update({ [sectionId]: censoredContent });
return 'Section saved: ' + sectionId;
}
}
Pass these context values from the client via the chat widget:
<squid-chat-widget
...
squid-ai-agent-chat-options={{
agentContext: { documentId: "document_controlled_by_this_agent" },
functions: [{
name: 'saveSection',
context: { codenameList: ['LITANIA', 'LEOPARD'] }
}]
}}
...
>
</squid-chat-widget>
Or via the SDK:
const result = await squid
.ai()
.agent('my-agent')
.ask('I want the "results" section to be "LITANIA has determined the answer to be 42".', {
agentContext: { documentId: 'document_controlled_by_this_agent' },
functions: [
{
name: 'saveSection',
context: { codenameList: ['LITANIA', 'LEOPARD'] },
},
],
});
In this case, the saved content would contain "REDACTED has determined the answer to be 42" because "LITANIA" is in the codename list.
Error Handling
When an AI function throws an error, the agent receives the error message and can relay it to the user or decide how to proceed. Throw clear, descriptive errors:
@aiFunction<{ orderId: string }>(
'Looks up the status of an order.',
[{ name: 'orderId', description: 'The order ID', type: 'string', required: true }],
)
async getOrderStatus(params: { orderId: string }): Promise<string> {
const { orderId } = params;
const order = await this.squid.collection('orders').doc(orderId).snapshot();
if (!order) {
throw new Error(`Order ${orderId} not found`);
}
return `Order ${orderId}: ${order.status}, shipped ${order.shippedDate}`;
}
Common issues
| Issue | Cause | Solution |
|---|---|---|
| Function never gets called | Description does not match user prompts | Rewrite the description to clearly state when to call the function |
| Wrong parameter values | Parameter descriptions are vague | Add specific descriptions and use enum to constrain values |
| Function not found by agent | Function name not passed in functions option | Pass the function name in the ask call or add it in Agent Studio |
| Function not appearing in Agent Studio | Function not deployed to the cloud | Deploy the backend with squid deploy |
Best Practices
Write clear descriptions
The description is the single most important factor in whether the agent calls your function correctly. Write it as an instruction that tells the agent exactly when to use it:
// Good: specific and instructive
'Returns the shipping status and tracking number for an order. Call when the user asks about order status, delivery, or tracking.';
// Bad: vague
'Gets order info';
Design parameters carefully
- Use
required: trueonly for values the function cannot work without - Set
enumwhen the valid values are a known set - Write parameter descriptions that tell the agent what format to use (e.g., "ISO8601 date", "email address")
Keep return values informative
The agent uses your return string to formulate its response. Return human-readable information, not raw data structures.
Validate inputs
Even though the AI generates parameter values, validate them in your function. The AI may produce unexpected values:
async updateQuantity(params: { quantity: number }): Promise<string> {
const { quantity } = params;
if (!Number.isFinite(quantity) || quantity < 0) {
throw new Error('Quantity must be a non-negative number');
}
// ...
}
Next Steps
- AI agent documentation for configuring agents and attaching functions
- MCP for connecting agents to external tool servers
- AI home maintenance tutorial for a complete example using AI functions