Frontend Functions Overview
Frontend functions are JavaScript/TypeScript functions that can be invoked by the voice agent. They run in the browser and have access to the application’s context.
Function Definition
NavaiFunctionDefinition Type
type NavaiFunctionDefinition = {
name: string;
description: string;
source: string;
run: (payload: NavaiFunctionPayload, context: NavaiFunctionContext) => Promise<unknown> | unknown;
};
type NavaiFunctionPayload = Record<string, unknown>;
type NavaiFunctionContext = {
navigate: (path: string) => void;
};
Creating Functions
Simple Function Export
// src/ai/functions/greet.ts
export async function greet(name: string, context: NavaiFunctionContext): Promise<string> {
return `Hello, ${name}! Welcome to the app.`;
}
Function with Payload Object
// src/ai/functions/search.ts
export async function search(
payload: { query: string; limit?: number },
context: NavaiFunctionContext
) {
const { query, limit = 10 } = payload;
const results = await searchAPI(query, limit);
return { results, count: results.length };
}
Default Export
// src/ai/functions/notify.ts
export default function notify(message: string) {
alert(message);
return { ok: true };
}
Class Methods
// src/ai/functions/Calculator.ts
export class Calculator {
add(a: number, b: number): number {
return a + b;
}
multiply(a: number, b: number): number {
return a * b;
}
divide(a: number, b: number): number {
if (b === 0) throw new Error('Division by zero');
return a / b;
}
}
The agent can call:
calculator_add → calls new Calculator().add(...)
calculator_multiply → calls new Calculator().multiply(...)
calculator_divide → calls new Calculator().divide(...)
Object with Multiple Functions
// src/ai/functions/utils.ts
export const utilities = {
formatCurrency(amount: number): string {
return `$${amount.toFixed(2)}`;
},
getCurrentUser(): { id: number; name: string } {
return { id: 1, name: 'John Doe' };
},
async fetchData(endpoint: string) {
const response = await fetch(endpoint);
return response.json();
}
};
The agent can call:
utilities_formatCurrency
utilities_getCurrentUser
utilities_fetchData
Function Discovery
loadNavaiFunctions
Loads and registers functions from module loaders.
import { loadNavaiFunctions } from '@navai/voice-frontend';
const moduleLoaders = {
'src/ai/functions/greet.ts': () => import('./ai/functions/greet'),
'src/ai/functions/search.ts': () => import('./ai/functions/search')
};
const registry = await loadNavaiFunctions(moduleLoaders);
// registry.byName - Map<string, NavaiFunctionDefinition>
// registry.ordered - NavaiFunctionDefinition[]
// registry.warnings - string[]
Type Signature
type NavaiFunctionModuleLoaders = Record<string, () => Promise<unknown>>;
type NavaiFunctionsRegistry = {
byName: Map<string, NavaiFunctionDefinition>;
ordered: NavaiFunctionDefinition[];
warnings: string[];
};
function loadNavaiFunctions(
functionModuleLoaders: NavaiFunctionModuleLoaders
): Promise<NavaiFunctionsRegistry>;
Discovery Rules
Module Loading
Each loader function is called to import the module.
Export Scanning
All named exports and default export are scanned.
Type Detection
Each export is classified as:
- Function → creates function definition
- Class → creates method definitions for all instance methods
- Object → creates function definitions for all callable properties
Name Normalization
Function names are normalized:
- Convert camelCase to snake_case
- Replace non-alphanumeric chars with underscores
- Convert to lowercase
'getUserData' → 'get_user_data'
'Calculator.add' → 'calculator_add'
Deduplication
If duplicate names exist, suffixes are added (_2, _3, etc.).
Function Execution
Payload Structure
The voice agent sends payloads in different formats:
Direct Arguments
// Voice: "call greet with arguments John"
// Payload: { args: ['John'] }
export function greet(name: string) {
return `Hello, ${name}!`;
}
Object Payload
// Voice: "search for laptops with limit 5"
// Payload: { query: 'laptops', limit: 5 }
export function search(payload: { query: string; limit: number }) {
const { query, limit } = payload;
// ...
}
Class Constructor + Method
// Voice: "use calculator to add 5 and 3"
// Payload: {
// constructorArgs: [],
// methodArgs: [5, 3]
// }
export class Calculator {
add(a: number, b: number) {
return a + b;
}
}
Single Value
// Voice: "notify the user with message hello"
// Payload: { value: 'hello' }
export function notify(message: string) {
alert(message);
}
Context Parameter
Functions can optionally receive a context parameter as the last argument:
type NavaiFunctionContext = {
navigate: (path: string) => void;
};
Example:
export async function searchAndNavigate(
query: string,
context: NavaiFunctionContext
) {
const results = await search(query);
if (results.length === 1) {
// Navigate to the single result
context.navigate(`/items/${results[0].id}`);
}
return results;
}
Error Handling
If a function throws an error, the agent receives an error response:
export async function riskyOperation(input: string) {
if (!input) {
throw new Error('Input is required');
}
// ...
}
// Voice agent receives:
// {
// ok: false,
// function_name: 'risky_operation',
// error: 'Function execution failed.',
// details: 'Input is required'
// }
Function Registration
Functions are registered in two ways:
Functions with valid tool names (alphanumeric, hyphens, underscores, 1-64 chars) are registered as direct tools:
// Function: getUserData
// Registered as direct tool: get_user_data
// Voice agent can call directly:
// - get_user_data(payload)
Functions with invalid tool names are only available via execute_app_function:
// Function: "My Function" (has spaces)
// Not valid tool name
// Warning: "Function 'My Function' is available only via execute_app_function"
// Voice agent must call:
// - execute_app_function({ function_name: 'my_function', payload: {...} })
Real-World Examples
User Management
// src/ai/functions/user.ts
import { getUserProfile, updateUserSettings } from '@/lib/api';
export async function getUserInfo() {
const profile = await getUserProfile();
return {
name: profile.name,
email: profile.email,
plan: profile.subscription.plan
};
}
export async function updateSettings(
payload: { theme?: string; language?: string; notifications?: boolean }
) {
await updateUserSettings(payload);
return { ok: true, message: 'Settings updated successfully' };
}
// src/ai/functions/cart.ts
import { cartStore } from '@/store/cart';
export function addToCart(payload: { productId: number; quantity: number }) {
const { productId, quantity } = payload;
cartStore.add(productId, quantity);
return {
ok: true,
cartSize: cartStore.getItemCount(),
total: cartStore.getTotal()
};
}
export function getCartSummary() {
return {
items: cartStore.getItems(),
total: cartStore.getTotal(),
itemCount: cartStore.getItemCount()
};
}
export function clearCart() {
cartStore.clear();
return { ok: true, message: 'Cart cleared' };
}
Data Filtering
// src/ai/functions/filter.ts
import { dataStore } from '@/store/data';
export class DataFilter {
filterByCategory(category: string) {
return dataStore.items.filter(item => item.category === category);
}
filterByPriceRange(minPrice: number, maxPrice: number) {
return dataStore.items.filter(
item => item.price >= minPrice && item.price <= maxPrice
);
}
sortBy(field: string, order: 'asc' | 'desc' = 'asc') {
const sorted = [...dataStore.items].sort((a, b) => {
const aVal = a[field];
const bVal = b[field];
return order === 'asc' ? aVal - bVal : bVal - aVal;
});
return sorted;
}
}
Voice commands:
- “Filter by category electronics” →
data_filter_filterByCategory
- “Filter by price range 10 to 50” →
data_filter_filterByPriceRange
- “Sort by price descending” →
data_filter_sortBy
Navigation Helper
// src/ai/functions/navigation.ts
import { NavaiFunctionContext } from '@navai/voice-frontend';
export async function openLatestOrder(context: NavaiFunctionContext) {
const orders = await fetch('/api/orders').then(r => r.json());
if (orders.length === 0) {
return { ok: false, message: 'No orders found' };
}
const latestOrder = orders[0];
context.navigate(`/orders/${latestOrder.id}`);
return { ok: true, orderId: latestOrder.id };
}
export async function searchAndOpen(
payload: { query: string },
context: NavaiFunctionContext
) {
const results = await fetch(`/api/search?q=${payload.query}`).then(r => r.json());
if (results.length === 1) {
// Single result, navigate directly
context.navigate(`/items/${results[0].id}`);
return { ok: true, navigated: true, item: results[0] };
}
// Multiple results, show search page
context.navigate(`/search?q=${payload.query}`);
return { ok: true, navigated: false, resultCount: results.length };
}
Best Practices
Use Descriptive Function Names
Choose names that clearly indicate what the function does.// Good
export function getUserProfile() { ... }
// Avoid
export function get() { ... }
Accept Payload Objects
For functions with multiple parameters, use a payload object.export function search(payload: {
query: string;
limit?: number;
sortBy?: string;
}) { ... }
Return Structured Data
Return objects with clear success/error indicators.return {
ok: true,
data: results,
count: results.length
};
Handle Errors Gracefully
Throw errors with clear messages for better debugging.if (!userId) {
throw new Error('User ID is required');
}
Use Context for Navigation
When functions need to navigate, use the context parameter.export function openProfile(
userId: number,
context: NavaiFunctionContext
) {
context.navigate(`/users/${userId}`);
}
Agent Instructions
The voice agent is instructed to:
- Call
execute_app_function or direct function tools when user requests an action
- Always include a payload (use
null if no arguments needed)
- Use
payload.args array for function arguments
- Use
payload.constructorArgs and payload.methodArgs for class methods
- Never invent function names not in the allowed list
// Internal agent instructions (from agent.ts)
const instructions = [
// ...
'Allowed app functions:',
'- get_user_info: Fetch current user information',
'- search: Search for items',
'- add_to_cart: Add item to shopping cart',
'Rules:',
'- If user asks to run an internal action, call execute_app_function or the matching direct function tool.',
'- Always include payload in execute_app_function. Use null when no arguments are needed.',
'- For execute_app_function, pass arguments using payload.args (array).',
'- For class methods, pass payload.constructorArgs and payload.methodArgs.',
'- Never invent function names that are not listed.'
].join('\n');
Functions are automatically discovered and registered. You only need to export them from modules in your configured functions folder.