DatabaseChat
Guides

Tools

Define what the LLM can query.

Tools describe the queries and actions your assistant can call. Each tool declares a name, description, JSON schema for parameters, and a handler string created with createFunctionHandle.

Tool shape

interface DatabaseChatTool {
  name: string;
  description: string;
  parameters: {
    type: "object";
    properties: Record<
      string,
      {
        type: "string" | "number" | "boolean" | "array" | "object";
        description?: string;
        enum?: string[];
        items?: { type: string };
      }
    >;
    required?: string[];
  };
  handlerType?: "query" | "mutation" | "action";
  handler: string;
  metadata?: {
    kind: "count" | "paginated_list" | "semantic_search" | "detail" | "unknown";
    resultContract?: "standard";
  };
}

Raw tools default handlerType to "query". Use "action" for tools that call ctx.vectorSearch or external APIs. The typed semantic builder defaults to "action" because semantic search commonly uses Convex actions.

Tool metadata

metadata tells the component what kind of result a tool returns. It is not a model-visible argument and it is not app domain data. For example, do not use kind for values like "product" or "candidate". Use it to describe the tool's result semantics.

Typed standard-result builders set metadata automatically. If you define raw tools by hand, add metadata when the component should be able to generate better tool guidance. If metadata is omitted, the tool is treated as "unknown".

FieldMeaning
metadata.kindThe result category the component uses for prompt guidance and tool reliability semantics.
metadata.resultContractSet to "standard" only when the handler returns { data, meta } as DatabaseChatToolResult.

Supported kind values:

KindUse for
"count"Exact total/count tools. The authoritative answer is usually meta.count.
"paginated_list"Deterministic list tools that may return one cursor page at a time.
"semantic_search"Top-K relevance tools. Results are sampled and should not be interpreted as exact totals.
"detail"One-record detail lookups. This is useful for classifying exact detail fetches.
"unknown"Raw or app-specific tools where the component should not assume count or pagination rules.

Use metadata.resultContract: "standard" only for handlers that return the standard result contract described below. Do not set it for legacy raw arrays, raw objects, or vector helpers that return a plain array.

Explicit tool example

import { createFunctionHandle } from "convex/server";
import { api } from "./_generated/api";
import type { DatabaseChatTool } from "./components/databaseChat/tools";

async function getTools(): Promise<DatabaseChatTool[]> {
  return [
    {
      name: "searchProducts",
      description: "Search products by name, category, or price range",
      parameters: {
        type: "object",
        properties: {
          query: { type: "string", description: "Search text" },
          category: {
            type: "string",
            enum: ["electronics", "clothing", "home", "sports"],
          },
          minPrice: { type: "number" },
          maxPrice: { type: "number" },
          limit: { type: "number" },
        },
      },
      handler: await createFunctionHandle(api.chatTools.searchProducts),
    },
  ];
}

createFunctionHandle is asynchronous and should be called inside a Convex function, usually from a small getTools() helper used by your chat action.

Built-in helpers

If you want generic tools, the component includes helpers:

  • createQueryTableTool(allowedTables, handler)
  • createCountTool(allowedTables, handler)
  • createAggregateTool(allowedTables, handler)
  • createSearchTool(allowedTables, handler)

Each helper returns a DatabaseChatTool and still requires a handler string.

Typed standard-result tools

For tools that return the standard result contract, prefer the split builders. They generate the same DatabaseChatTool shape, attach reliability metadata, and keep model-visible filters nested under filters.

import {
  defineCountTool,
  definePaginatedListTool,
  defineSemanticSearchTool,
  enumFilter,
  injectedString,
  numberFilter,
  stringFilter,
  type InferToolHandlerArgs,
  type InferToolResult,
} from "@dayhaysoos/convex-database-chat/tools";
import type { DatabaseChatToolResult } from "@dayhaysoos/convex-database-chat/resultContract";

type ProductRow = {
  id: string;
  name: string;
  price: number;
  viewUrl: string;
};

export const listProductsTool = definePaginatedListTool<ProductRow>({
  name: "listProducts",
  description: "List products matching deterministic filters.",
  handler: "handler_string",
  filters: {
    category: enumFilter({
      values: ["electronics", "clothing", "home", "sports"] as const,
      description: "Product category.",
    }),
    minPrice: numberFilter({ min: 0 }),
    searchQuery: stringFilter(),
  },
  injectedArgs: {
    tenantId: injectedString(),
  },
  pagination: {
    defaultLimit: 20,
    maxLimit: 100,
  },
});

type HandlerArgs = InferToolHandlerArgs<typeof listProductsTool>;
type HandlerResult = InferToolResult<typeof listProductsTool>;
// HandlerResult is DatabaseChatToolResult<ProductRow>.

The LLM sees filters, limit, and cursor. It does not see injected args such as tenantId; those still appear in handler arg inference and can be merged from toolContext.

Builder defaults:

  • defineCountTool: exact totals, no limit.
  • definePaginatedListTool: deterministic cursor pages, default limit 20, max 100.
  • defineSemanticSearchTool: sampled top-K relevance results, default limit 10, max 50, and handlerType: "action" unless explicitly overridden.

Standard result contract

Handlers for standard-result tools should return:

const result: DatabaseChatToolResult<ProductRow> = {
  data: rows,
  meta: {
    scope: { type: "workspace", id: workspaceId },
    appliedFilters: normalizedFilters,
    count: exactCount,
    returned: rows.length,
    exhaustive: false,
    truncated: true,
    truncationReason: "row_limit",
    sampled: false,
    pagination: {
      cursor,
      hasMore: nextCursor !== null,
      nextCursor,
      pageSize: limit,
    },
  },
};

Use validateToolResultContract(result) in tests or development checks when you want structured errors for contract invariants such as meta.returned === data.length.

Pagination semantics

For deterministic lists, meta.count is the total number of matching rows and meta.returned is the number returned in this page. If meta.pagination.hasMore is true, pass meta.pagination.nextCursor back as the next call's cursor.

For example, a first page can return:

{
  "data": [{ "id": "1", "name": "USB-C Hub" }],
  "meta": {
    "count": 30,
    "returned": 1,
    "exhaustive": false,
    "truncated": true,
    "sampled": false,
    "pagination": {
      "cursor": null,
      "hasMore": true,
      "nextCursor": "1",
      "pageSize": 1
    }
  }
}

An immediate "show more" follow-up should call the same list tool with the same filters and cursor: "1". The final page should return hasMore: false.

Semantic search semantics

Semantic search tools should set meta.sampled: true, meta.exhaustive: false, and a sampleMethod such as "semantic_top_k". A semantic result's data.length is the number of top-K results returned, not an exact count of all possible matches. Use a count tool for factual totals.

Auto-generated tools

You can generate tools from schema-like definitions with generateToolsFromSchema.

import { createFunctionHandle } from "convex/server";
import {
  defineTable,
  generateToolsFromSchema,
} from "./components/databaseChat/schemaTools";
import { api } from "./_generated/api";

const tables = [
  defineTable("products", [
    { name: "name", type: "string" },
    { name: "category", type: "string" },
    { name: "price", type: "number" },
  ]),
];

async function getSchemaTools() {
  return generateToolsFromSchema({
    tables,
    allowedTables: ["products"],
    handlers: {
      query: await createFunctionHandle(api.chatTools.queryTable),
      count: await createFunctionHandle(api.chatTools.countRecords),
      aggregate: await createFunctionHandle(api.chatTools.aggregate),
    },
  });
}

AutoToolsConfig lets you restrict tables and hide fields:

  • allowedTables: tables to expose
  • excludeFields: per-table fields to omit
  • tableDescriptions: descriptions for table names
  • fieldDescriptions: descriptions for individual fields