The NAVAI backend function system automatically discovers callable functions in your codebase and exposes them as tools that AI agents can invoke during voice sessions.
What are backend functions?
Backend functions are TypeScript/JavaScript functions that you write in designated directories. The NAVAI runtime:
Discovers functions by scanning configured paths
Transforms exports into normalized tool definitions
Exposes them via GET /navai/functions endpoint
Executes them when called via POST /navai/functions/execute
Functions are loaded lazily on first request and cached in memory. Changes to function files require a backend restart to take effect.
Creating functions
Place callable exports in your functions directory (default: src/ai/functions-modules):
Simple function (weather.ts)
Function with context (email.ts)
Class with methods (Calculator.ts)
Object with callable members (math.ts)
export async function getWeather ( location : string ) {
// Call weather API
const response = await fetch (
`https://api.weather.com/data?location= ${ location } `
);
return response . json ();
}
Function discovery
The loadNavaiFunctions API loads functions from module loaders:
import { loadNavaiFunctions } from "@navai/voice-backend" ;
const registry = await loadNavaiFunctions ({
"src/ai/functions-modules/weather.ts" : () => import ( "./weather.js" ),
"src/ai/functions-modules/email.ts" : () => import ( "./email.js" )
});
console . log ( registry );
// {
// byName: Map { "get_weather" => {...}, "send_email" => {...} },
// ordered: [...],
// warnings: []
// }
Function signature
From functions.ts:278-320:
export async function loadNavaiFunctions (
functionModuleLoaders : NavaiFunctionModuleLoaders
) : Promise < NavaiFunctionsRegistry >
functionModuleLoaders
NavaiFunctionModuleLoaders
required
Record mapping file paths to dynamic import functions type NavaiFunctionModuleLoaders = Record < string , () => Promise < unknown >>;
Return type
export type NavaiFunctionsRegistry = {
byName : Map < string , NavaiFunctionDefinition >; // Fast lookup by name
ordered : NavaiFunctionDefinition []; // Ordered list of functions
warnings : string []; // Load/parse warnings
};
export type NavaiFunctionDefinition = {
name : string ; // Normalized function name (snake_case)
description : string ; // Auto-generated description
source : string ; // File path and export name
run : ( payload : NavaiFunctionPayload , context : NavaiFunctionContext ) => Promise < unknown > | unknown ;
};
Module loaders
Instead of manually creating module loaders, use the runtime to automatically scan directories:
import { resolveNavaiBackendRuntimeConfig , loadNavaiFunctions } from "@navai/voice-backend" ;
const config = await resolveNavaiBackendRuntimeConfig ({
baseDir: process . cwd (),
functionsFolders: "src/ai/functions-modules" ,
includeExtensions: [ "ts" , "js" , "mjs" ],
exclude: [ "**/node_modules/**" , "**/*.test.ts" ]
});
const registry = await loadNavaiFunctions ( config . functionModuleLoaders );
Configuration options
From runtime.ts:9-16:
export type ResolveNavaiBackendRuntimeConfigOptions = {
env ?: NavaiBackendEnv ;
functionsFolders ?: string ;
defaultFunctionsFolder ?: string ;
baseDir ?: string ;
includeExtensions ?: string [];
exclude ?: string [];
};
functionsFolders
string
default: "src/ai/functions-modules"
Comma-separated paths to scan. Supports:
Folder: src/ai/tools
Recursive: src/ai/tools/...
Wildcard: src/features/*/tools
Explicit file: src/ai/tools/weather.ts
CSV: src/ai/tools,src/features/*/functions
baseDir
string
default: "process.cwd()"
Base directory for resolving relative paths
includeExtensions
string[]
default: "[\"ts\",\"js\",\"mjs\",\"cjs\",\"mts\",\"cts\"]"
File extensions to include in scan
Glob patterns to exclude from scan
Environment variable
Set NAVAI_FUNCTIONS_FOLDERS to configure paths from environment:
NAVAI_FUNCTIONS_FOLDERS=src/ai/functions-modules,...
The runtime reads this automatically when you call resolveNavaiBackendRuntimeConfig() without options.
The function loader transforms different export shapes into normalized tool definitions.
Exported function
From functions.ts:245-266:
// Input: weather.ts
export async function getWeather ( location : string ) {
return { temp: 72 , conditions: "sunny" };
}
// Output: Tool definition
{
name : "get_weather" ,
description : "Call exported function getWeather." ,
source : "src/ai/functions-modules/weather.ts#getWeather" ,
run : async ( payload , context ) => {
const args = buildInvocationArgs ( payload , context , 1 );
return await getWeather ( ... args );
}
}
Exported class
From functions.ts:146-190:
// Input: Calculator.ts
export class Calculator {
add ( a : number , b : number ) {
return a + b ;
}
multiply ( a : number , b : number ) {
return a * b ;
}
}
// Output: One tool per method
[
{
name: "calculator_add" ,
description: "Call class method Calculator.add()." ,
source: "src/ai/functions-modules/Calculator.ts#Calculator.add" ,
run : async ( payload , context ) => {
const constructorArgs = payload . constructorArgs ?? [];
const instance = new Calculator ( ... constructorArgs );
const args = buildInvocationArgs ( payload , context , 2 );
return await instance . add ( ... args );
}
},
{
name: "calculator_multiply" ,
description: "Call class method Calculator.multiply()." ,
source: "src/ai/functions-modules/Calculator.ts#Calculator.multiply" ,
run : async ( payload , context ) => { /* ... */ }
}
]
Exported object
From functions.ts:192-234:
// Input: math.ts
export const MathOps = {
square ( n : number ) {
return n * n ;
},
cube ( n : number ) {
return n * n * n ;
}
};
// Output: One tool per callable member
[
{
name: "math_ops_square" ,
description: "Call exported object member MathOps.square()." ,
source: "src/ai/functions-modules/math.ts#MathOps.square" ,
run : async ( payload , context ) => { /* ... */ }
},
{
name: "math_ops_cube" ,
description: "Call exported object member MathOps.cube()." ,
source: "src/ai/functions-modules/math.ts#MathOps.cube" ,
run : async ( payload , context ) => { /* ... */ }
}
]
Name normalization
Function names are normalized to snake_case lowercase.
From functions.ts:28-34:
function normalizeName ( value : string ) : string {
return value
. replace ( / ( [ a-z0-9 ] )( [ A-Z ] ) / g , "$1_$2" )
. replace ( / [ ^ a-zA-Z0-9 ] + / g , "_" )
. replace ( / ^ _ + | _ + $ / g , "" )
. toLowerCase ();
}
Examples
Original name Normalized name getWeatherget_weatherSendEmailsend_emailapi-clientapi_clientget__dataget_data_privateprivate
Collision handling
If multiple exports normalize to the same name, suffixes are appended:
// weather.ts exports: getWeather
// weather2.ts exports: get_weather
// Result:
// - get_weather (from weather.ts)
// - get_weather_2 (from weather2.ts)
A warning is emitted:
[navai] Renamed duplicated function "get_weather" to "get_weather_2".
Argument resolution
The function loader intelligently resolves arguments from the payload.
From functions.ts:64-79:
function buildInvocationArgs (
payload : NavaiFunctionPayload ,
context : NavaiFunctionContext ,
targetArity : number
) : unknown [] {
const directArgs = readArray ( payload . args ?? payload . arguments );
const args = directArgs . length > 0 ? [ ... directArgs ] : [];
if ( args . length === 0 && "value" in payload ) {
args . push ( payload . value );
} else if ( args . length === 0 && Object . keys ( payload ). length > 0 ) {
args . push ( payload );
}
if ( targetArity > args . length ) {
args . push ( context );
}
return args ;
}
Resolution priority
Explicit args : Use payload.args or payload.arguments if present
Single value : Use payload.value if no args
Whole payload : Use entire payload object if no args or value
Context : Append context if function arity expects more arguments
Examples
Explicit args
Single value
Whole payload
With context
{
"function_name" : "get_weather" ,
"payload" : {
"args" : [ "San Francisco" , "fahrenheit" ]
}
}
// Invokes: getWeather("San Francisco", "fahrenheit")
Function context
Functions can access the Express request object via context:
export async function authenticatedFunction (
payload : { data : string },
context : { req : Request }
) {
// Access headers
const userId = context . req . headers [ 'x-user-id' ];
const token = context . req . headers [ 'authorization' ];
// Access body, query, params
console . log ( context . req . body );
console . log ( context . req . query );
// Implement auth logic
if ( ! token ) {
throw new Error ( "Unauthorized" );
}
return { userId , data: payload . data };
}
From functions.ts:1-9:
export type NavaiFunctionPayload = Record < string , unknown >;
export type NavaiFunctionContext = Record < string , unknown >;
export type NavaiFunctionDefinition = {
name : string ;
description : string ;
source : string ;
run : ( payload : NavaiFunctionPayload , context : NavaiFunctionContext ) => Promise < unknown > | unknown ;
};
Context is automatically injected by the Express handler at index.ts:319. Currently it only includes {req}, but you can extend it for custom middleware.
HTTP endpoints
Functions are exposed via two endpoints:
GET /navai/functions
Lists all discovered functions.
Response:
{
"items" : [
{
"name" : "get_weather" ,
"description" : "Call exported function getWeather." ,
"source" : "src/ai/functions-modules/weather.ts#getWeather"
},
{
"name" : "send_email" ,
"description" : "Call exported function sendEmail." ,
"source" : "src/ai/functions-modules/email.ts#sendEmail"
}
],
"warnings" : [
"[navai] Renamed duplicated function \" get_data \" to \" get_data_2 \" ."
]
}
POST /navai/functions/execute
Executes a function by name.
Request:
{
"function_name" : "get_weather" ,
"payload" : {
"args" : [ "San Francisco" ]
}
}
Success response:
{
"ok" : true ,
"function_name" : "get_weather" ,
"source" : "src/ai/functions-modules/weather.ts#getWeather" ,
"result" : {
"temperature" : 72 ,
"conditions" : "sunny"
}
}
Error responses:
Missing function_name (400)
Unknown function (404)
Runtime error (500)
{
"error" : "function_name is required."
}
Warnings
The runtime emits warnings for common issues:
Module warnings
[navai] Ignored src/ai/functions-modules/empty.ts: module has no exports.
[navai] Ignored src/ai/functions-modules/data.ts: module has no callable exports.
[navai] Failed to load src/ai/functions-modules/broken.ts: Cannot find module
Name collision warnings
[navai] Renamed duplicated function "get_weather" to "get_weather_2".
Class warnings
[navai] Ignored src/ai/functions-modules/Empty.ts#EmptyClass: class has no callable instance methods.
Configuration warnings
[navai] NAVAI_FUNCTIONS_FOLDERS did not match any module: "src/wrong/path". Falling back to "src/ai/functions-modules".
Always monitor warnings in your logs. They indicate potential issues with function discovery or naming conflicts.
Best practices
Function design
Keep functions focused on a single task
Use descriptive function names (they become tool names)
Document parameters with JSDoc comments
Return structured data (objects) rather than primitive values
Handle errors gracefully with meaningful messages
Organization
src/ai/functions-modules/
├── weather.ts # Weather-related functions
├── navigation.ts # Navigation functions
├── email.ts # Email functions
└── database/
├── users.ts # User database operations
└── orders.ts # Order database operations
Security
Validate all input parameters
Implement authentication checks using context.req
Don’t expose sensitive functions (exclude them from scan)
Use environment variables for API keys and secrets
Log function invocations for audit trails
Keep functions fast (< 5 seconds)
Use async/await for I/O operations
Cache expensive operations when possible
Implement timeouts for external API calls
Monitor function execution times
Advanced usage
Custom function validation
import { loadNavaiFunctions } from "@navai/voice-backend" ;
const registry = await loadNavaiFunctions ( moduleLoaders );
// Filter out functions you don't want to expose
const allowedNames = [ "get_weather" , "send_email" ];
const filtered = registry . ordered . filter ( fn =>
allowedNames . includes ( fn . name )
);
const filteredRegistry = {
byName: new Map ( filtered . map ( fn => [ fn . name , fn ])),
ordered: filtered ,
warnings: registry . warnings
};
Custom module loaders
import { loadNavaiFunctions } from "@navai/voice-backend" ;
const customLoaders = {
"virtual://weather" : async () => ({
default : async ( location : string ) => {
return { temp: 72 , conditions: "sunny" };
}
}),
"virtual://calculator" : async () => ({
add : ( a : number , b : number ) => a + b ,
multiply : ( a : number , b : number ) => a * b
})
};
const registry = await loadNavaiFunctions ( customLoaders );
Reloading functions
let registryPromise : Promise < NavaiFunctionsRegistry > | null = null ;
function clearRegistry () {
registryPromise = null ;
}
async function getRegistry () {
if ( ! registryPromise ) {
const config = await resolveNavaiBackendRuntimeConfig ();
registryPromise = loadNavaiFunctions ( config . functionModuleLoaders );
}
return registryPromise ;
}
// Expose reload endpoint (development only)
app . post ( "/dev/reload-functions" , ( req , res ) => {
clearRegistry ();
res . json ({ ok: true , message: "Registry will reload on next request" });
});
The built-in Express routes cache the registry. Manual reload is only useful if you implement custom endpoints.
Next steps
Other frameworks Learn how to implement NAVAI backend in Laravel, Django, Rails, or custom frameworks