Skip to main content

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

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

1

Module Loading

Each loader function is called to import the module.
2

Export Scanning

All named exports and default export are scanned.
3

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
4

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'
5

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:

Direct Tool Registration

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)

Fallback Tool Registration

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' };
}

Shopping Cart

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

1

Use Descriptive Function Names

Choose names that clearly indicate what the function does.
// Good
export function getUserProfile() { ... }

// Avoid
export function get() { ... }
2

Accept Payload Objects

For functions with multiple parameters, use a payload object.
export function search(payload: {
  query: string;
  limit?: number;
  sortBy?: string;
}) { ... }
3

Return Structured Data

Return objects with clear success/error indicators.
return {
  ok: true,
  data: results,
  count: results.length
};
4

Handle Errors Gracefully

Throw errors with clear messages for better debugging.
if (!userId) {
  throw new Error('User ID is required');
}
5

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:
  1. Call execute_app_function or direct function tools when user requests an action
  2. Always include a payload (use null if no arguments needed)
  3. Use payload.args array for function arguments
  4. Use payload.constructorArgs and payload.methodArgs for class methods
  5. 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.

Build docs developers (and LLMs) love