Databricks Agent Mesh for Small Business Workflow Automation
A multi-agent routing mesh powered by Databricks models that coordinates specialized agents for order processing, customer service, and analytics for small businesses.
Small businesses use multiple disconnected AI agents for different tasks—order tracking, support, and data analysis—but lack a unified orchestration layer to route requests and share context, leading to inconsistent experiences and manual handoffs.
A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
This recipe builds a multi-agent routing mesh using Next.js and the @reaatech/agent-mesh family of packages, backed by Databricks-hosted foundation models. You’ll create three specialist agents — an Order Management Agent, a Customer Support Agent, and an Analytics Agent — coordinated through a unified API that classifies incoming requests, maintains per-user session continuity, and enforces rate limits with observability. By the end, you’ll have a running endpoint that accepts natural-language requests like “Where is my order?” or “Show me this month’s sales” and routes them to the right agent, with session history, audit logging, and a circuit breaker.
Prerequisites
Node.js >= 22 and pnpm >= 10 installed
A Databricks workspace with a serving endpoint for a foundation model (e.g. databricks-meta-llama-3-3-70b-instruct) and a personal access token
A PostgreSQL database (local or remote) for session and workflow data
Basic familiarity with Next.js App Router, TypeScript, and REST APIs
Step 1: Scaffold the project and install dependencies
Create a Next.js project with the App Router, then add all the packages this recipe needs.
Install the runtime and dev dependencies. The @reaatech/* packages provide the mesh foundation, session management, routing, gateway primitives, and observability. Mastra gives you agent primitives, Drizzle ORM provides database access, and the Databricks SDK connects to your model endpoints.
Expected output: Your package.json now lists all dependencies with exact versions (no ^ or ~ prefixes). The create-next-app command already added next, react, and react-dom.
Step 2: Configure environment variables
Create a .env file at the project root. This recipe reads several variables for Databricks authentication, database connectivity, Langfuse observability, and runtime configuration.
Add the same entries (with placeholder values) to .env.example so other developers know what’s needed. The two mandatory variables — DATABRICKS_HOST and DATABRICKS_TOKEN — are checked at import time in the Databricks client.
Expected output: Both .env (with real values) and .env.example (with placeholders) exist at the project root.
Step 3: Define the database schema with Drizzle ORM
Create src/lib/db-schema.ts. This defines three PostgreSQL tables — orders, support_tickets, and analytics_events — that back the specialist agents’ tools.
Then create src/lib/drizzle.ts to export a configured Drizzle client connected to your Postgres pool:
ts
import { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres";import { Pool } from "pg";const databaseUrl = process.env.DATABASE_URL;if (!databaseUrl) { throw new Error("DATABASE_URL environment variable is required");}const pool = new Pool({ connectionString: databaseUrl });export const db = drizzle(pool);export type DbClient = NodePgDatabase & { $client: Pool };
Expected output: Both files compile without type errors. The DbClient type is used by every agent service to run queries.
Step 4: Create the agent mesh types and registry
The mesh needs shared Zod schemas and a registry that maps intent keywords to agent configurations.
Create src/mesh/types.ts — these extend the base IncomingRequestSchema from @reaatech/agent-mesh with domain-specific fields:
Create src/mesh/config.ts — the agent registry declares each specialist agent with its ID, display name, example phrases for intent classification, and confidence threshold:
ts
import { type AgentConfig, AgentConfigSchema } from "@reaatech/agent-mesh";export const AGENT_REGISTRY: AgentConfig[] = [ { agent_id: "order-agent", display_name: "Order Management Agent", description: "Handles order inquiries, creation, and status updates", endpoint: "http://localhost:3000/api/agents/order", type: "mcp", is_default: false, confidence_threshold: 0.80, clarification_required: false, examples: [ "Where is my order?", "I want to check my order status", "Can I cancel my order?", ], }, { agent_id: "support-agent", display_name: "Customer Support Agent", description: "Handles customer support tickets and inquiries", endpoint: "http://localhost:3000/api/agents/support", type: "mcp", is_default: false, confidence_threshold: 0.70, clarification_required: false, examples: [ "I need help with my account", "How do I reset my password?", "I have a billing question", ], }, { agent_id: "analytics-agent", display_name: "Analytics Agent", description: "Provides business analytics and insights", endpoint: "http://localhost:3000/api/agents/analytics", type: "mcp", is_default: false, confidence_threshold: 0.85, clarification_required: false, examples: [ "Show me sales summary for this month", "What are my top selling products?", "Generate a report on customer activity", ], },];export function lookupAgent(agentId: string): AgentConfig | undefined { return AGENT_REGISTRY.find((agent) => agent.agent_id === agentId);}export function validateAgentConfig(raw: unknown): AgentConfig { return AgentConfigSchema.parse(raw);}
Expected output:src/mesh/types.ts and src/mesh/config.ts compile cleanly. The registry has three agents, each with example phrases that the route handler’s classifyIntent function uses to determine routing.
Step 5: Build the Databricks SDK client
Create src/services/databricks.ts. This initializes a WorkspaceClient from the Databricks SDK and exports a submitInference function that calls a model serving endpoint with retry logic.
ts
import { WorkspaceClient, Config, ApiError } from "@databricks/sdk-experimental";import { logger } from "@reaatech/agent-mesh-observability";const rawHost: string = process.env.DATABRICKS_HOST ?? "";const rawToken: string = process.env.DATABRICKS_TOKEN ?? "";if (!rawHost || !rawToken) { throw new Error("DATABRICKS_HOST and DATABRICKS_TOKEN environment variables are required");}const config = new Config({ host: rawHost, token: rawToken,});export const databricksClient = new WorkspaceClient(config);export async function submitInference(model: string, prompt: string): Promise<string> { let lastError: Error | undefined; for (let attempt = 0; attempt < 2; attempt++) { try { const url = `${rawHost}/serving-endpoints/${model}/invocations`; const response = await globalThis.fetch( url, { method: "POST", headers: { Authorization: `Bearer ${rawToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: prompt }, ], }), }, ); if (!response.ok) { if ((response.status === 503 || response.status === 429) && attempt === 0) { await new Promise((resolve) => setTimeout(resolve, 1000)); continue; } throw new Error(`HTTP ${String(response.status)}: ${response.statusText}`); } const data = await response.json() as { choices?: Array<{ message?: { content?: string } }> }; const text = data.choices?.[0]?.message?.content ?? ""; return text; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (error instanceof ApiError && (error.statusCode === 503 || error.statusCode === 429) && attempt === 0) { await new Promise((resolve) => setTimeout(resolve, 1000)); continue; } logger.error("submitInference failed", { model, attempt, error: String(error) }); throw lastError; } } throw lastError ?? new Error("submitInference failed after all retries");}
Expected output: The module exports databricksClient (the SDK workspace client) and submitInference (a retry-aware function that posts to a Databricks model serving endpoint). The constructor checks fail fast if env vars are missing.
Step 6: Build the session manager
Create src/services/session-manager.ts. This wraps the @reaatech/agent-mesh-session package to provide find-or-create semantics, turn appending, workflow state updates, and session lifecycle management.
Expected output: Seven exported functions — getSession, findOrCreateSession, addTurn, updateSessionWorkflowState, completeSession, abandonSession, resumePriorSession. Each delegates to the corresponding @reaatech/agent-mesh-session function with error logging.
Step 7: Create the specialist agents
Each agent is a Mastra Agent equipped with Drizzle-backed tools and a databricksTool for Databricks model inference.
Order Agent — src/services/order-agent.ts
ts
import { eq } from "drizzle-orm";import type { DbClient } from "../lib/drizzle.js";import { orders } from "../lib/db-schema.js";import type { Order } from "../lib/db-schema.js";import { Agent } from "@mastra/core/agent";import { logger } from "@reaatech/agent-mesh-observability";import { submitInference } from "./databricks.js";import { OrderInquirySchema } from "../mesh/types.js";export const lookupOrderTool = { description: "Look up an order by its ID. Returns order details including customer, items, status, and total.", parameters: { type:
Support Agent — src/services/support-agent.ts
Follow the same pattern with createTicketTool, lookupTicketTool, updateTicketStatusTool, and getTicketsByCustomerTool. The Mastra agent is configured with instructions for the support domain:
ts
import { eq } from "drizzle-orm";import type { DbClient } from "../lib/drizzle.js";import { supportTickets } from "../lib/db-schema.js";import type { SupportTicket } from "../lib/db-schema.js";import { Agent } from "@mastra/core/agent";import { logger } from "@reaatech/agent-mesh-observability";import { submitInference } from "./databricks.js";import { SupportRequestSchema } from "../mesh/types.js";export const createTicketTool = { description: "Create a new support ticket for a customer.", parameters: { type: "object"
Analytics Agent — src/services/analytics-agent.ts
The analytics agent provides OLAP-style tools: getOrderSummaryTool, getTopProductsTool, getAgentMetricsTool, and getCustomerActivityTool.
ts
import { and, gte, lte, eq, sql } from "drizzle-orm";import type { DbClient } from "../lib/drizzle.js";import { orders, supportTickets, analyticsEvents } from "../lib/db-schema.js";import { Agent } from "@mastra/core/agent";import { logger } from "@reaatech/agent-mesh-observability";import { submitInference } from "./databricks.js";import { AnalyticsQuerySchema } from "../mesh/types.js";export const getOrderSummaryTool = { description: "Get order summary statistics within a date range, grouped by status.", parameters: { type: "object" as const, properties: {
Expected output: Three files under src/services/ — order-agent.ts, support-agent.ts, analytics-agent.ts. Each exports a mastraAgent instance, tool definitions, data-access functions, and a handle() entry point.
Step 8: Wire up instrumentation for observability
Create src/instrumentation.ts. Next.js runs this file at startup when register() is exported. It initializes OpenTelemetry, starts the logger, and cleans up MCP connections on shutdown.
import type { NextConfig } from "next";const nextConfig: NextConfig = {};export default nextConfig;
The NEXT_RUNTIME === "nodejs" guard ensures that initOtel() (which uses Node-only APIs) doesn’t execute in the Edge runtime.
Expected output:src/instrumentation.ts exports register(). Running pnpm typecheck produces no errors.
Step 9: Create the mesh API route handler
This is the central orchestration endpoint — app/api/mesh/route.ts. It handles POST requests by authenticating, rate-limiting, classifying intent, dispatching to the selected agent, and recording observability metrics.
ts
import { NextRequest, NextResponse } from "next/server";import { IncomingRequestSchema } from "@reaatech/agent-mesh";import { dispatchToAgent, shouldCloseSession, formatAgentResponse, getUpdatedWorkflowState } from "@reaatech/agent-mesh-router";import { createChildLogger, logAuditEvent, AUDIT_EVENTS, logAgentRouted, recordAgentDispatchDuration, recordAgentDispatchError } from "@reaatech/agent-mesh-observability";import { findOrCreateSession, addTurn, completeSession, updateSessionWorkflowState } from "../../../src/services/session-manager.js";import { AGENT_REGISTRY, lookupAgent } from "../../../src/mesh/config.js";
The route implements a token-bucket rate limiter per client IP. The checkRateLimit function is local to the file:
ts
const buckets = new Map<string, { tokens: number; lastRefill: number }>();function checkRateLimit(clientId: string): boolean { const now = Date.now(); const windowMs = Number(process.env.RATE_LIMIT_WINDOW_MS) || 900000; const maxRequests = Number(process.env.RATE_LIMIT_MAX_REQUESTS) || 100; let bucket = buckets.get(clientId); if (!bucket) { bucket = { tokens: maxRequests, lastRefill: now }; buckets.set(clientId, bucket); } const elapsed = now - bucket.lastRefill; const refill = Math.floor(elapsed / windowMs) * maxRequests; if (refill > 0) { bucket.tokens = Math.min(maxRequests, bucket.tokens + refill); bucket.lastRefill = now; } if (bucket.tokens <= 0) return false; bucket.tokens--; return true;}
The POST handler walks through the full request lifecycle:
Authenticate via x-api-key header (skipped in development)
Rate-limit by client IP
Parse and validate the JSON body with IncomingRequestSchema
Find or create a session for the user
Classify intent by matching input keywords against agent examples
Call dispatchToAgent from @reaatech/agent-mesh-router
Record dispatch duration and audit events
Append the agent’s turn to the session
Update workflow state and close the session if the workflow is complete
ts
export async function POST(request: NextRequest): Promise<NextResponse> { const requestId = crypto.randomUUID(); const childLogger = createChildLogger({ request_id: requestId }); const apiKey = request.headers.get("x-api-key"); if (process.env.NODE_ENV !== "development" && apiKey !== process.env.API_KEY) { childLogger.warn("Missing or invalid API key"); logAuditEvent({ event_type: "auth.failure", timestamp: new Date().toISOString(), request_id: requestId,
The classifyIntent function scores input text against each agent’s example phrases — at least two keyword matches triggers a routing decision:
ts
function classifyIntent(input: string): { agentId: string; summary: string; entities: Record<string, unknown>; confidence: number } { const lower = input.toLowerCase(); for (const agent of AGENT_REGISTRY) { for (const example of agent.examples) { const words = example.toLowerCase().split(/\s+/); const matchCount = words.filter((w) => lower.includes(w)).length; if (matchCount >= 2) { return { agentId: agent.agent_id, summary: agent.display_name, entities: {}, confidence: agent.confidence_threshold, }; } } } return { agentId: (AGENT_REGISTRY[0]?.agent_id) ?? "order-agent", summary: "general inquiry", entities: {}, confidence: 0.5, };}
Expected output:app/api/mesh/route.ts compiles. The handler covers authentication, rate limiting, validation, session management, intent classification, agent dispatch, observability instrumentation, and circuit-breaker-aware error handling.
Step 10: Run the tests
Create your test suite under tests/. Here’s the main route handler test at tests/api/mesh/route.test.ts — it mocks all external dependencies so no real network or database calls are needed:
Expected output: All tests pass. Coverage reports show at least 90% on runtime code (services, route handler, lib modules). The UI components (app/**/page.tsx, layout.tsx) are excluded from coverage targets.
Next steps
Add more specialist agents — extend the registry with a returns agent, a shipping agent, or a fraud-detection agent, each with its own tools and Databricks-backed inference
Deploy to production — containerize with Docker, set up a Postgres RDS instance, point DATABRICKS_HOST and DATABASE_URL at your production endpoints, and add a reverse proxy in front of the mesh API
Add a web dashboard — build a real-time admin panel that shows active sessions, agent dispatch metrics, and audit log events streamed from Langfuse
Upgrade intent classification — replace the keyword-matching classifyIntent function with an LLM-based classifier that calls submitInference to determine the best agent from a few-shot prompt
"object"
as
const
,
properties: { orderId: { type: "number" as const } },
instructions: "You are an order processing assistant for a small business. Use lookupOrderTool to find orders, createOrderTool to create orders, updateOrderStatusTool to update order status, cancelOrderTool to cancel orders, and databricksTool for general inquiries.",
instructions: "You are a customer support assistant for a small business. Use createTicketTool to create tickets, lookupTicketTool to find tickets, updateTicketStatusTool to update ticket status, getTicketsByCustomerTool to look up tickets by customer, and databricksTool for general inquiries.",
instructions: "You are a business analytics assistant for a small business. Use getOrderSummaryTool for order summaries, getTopProductsTool for top products, getAgentMetricsTool for agent metrics, getCustomerActivityTool for customer activity, and databricksTool for general inquiries.",