Skip to main content
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:
  1. Discovers functions by scanning configured paths
  2. Transforms exports into normalized tool definitions
  3. Exposes them via GET /navai/functions endpoint
  4. 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):
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
exclude
string[]
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.

Export transformation rules

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 nameNormalized name
getWeatherget_weather
SendEmailsend_email
api-clientapi_client
get__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

  1. Explicit args: Use payload.args or payload.arguments if present
  2. Single value: Use payload.value if no args
  3. Whole payload: Use entire payload object if no args or value
  4. Context: Append context if function arity expects more arguments

Examples

{
  "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:
{
  "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

Performance

  • 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

Build docs developers (and LLMs) love