Skip to content

Audit routing (lifecycle hooks)

Use audit routing when compliance or support needs a per-decision log: which tool ran (explore, execute, compile_metric), with what args summary, plus the SQL and row count after execution. Arivie does not store audit records internally — RFC-002 routes them through lifecycle hooks to your pipeline (Axiom, Datadog, S3, etc.).

MCP and HTTP share one agent — audit hooks sit below both transports

Base example: examples/with-nextjs/.

Add hooks to defineArivie in arivie.config.ts. onToolCall receives tool (the tool id string) for every agent step; onAfterQuery fires after execute / compile_metric runs SQL:

defineArivie({
// ... owner, model, db, semantic, resolveUser
hooks: {
async onToolCall(ctx) {
await auditSink.write({
event: "tool.call",
tool: ctx.tool,
ownerId: ctx.ownerId,
userId: ctx.userId,
durationMs: ctx.durationMs,
});
},
async onAfterQuery(ctx) {
await auditSink.write({
event: "sql.executed",
sql: ctx.sql,
rowCount: ctx.rows.length,
ownerId: ctx.ownerId,
userId: ctx.userId,
durationMs: ctx.durationMs,
});
},
},
});

The reference config shape (without hooks) is:

arivie.config.ts (base)
/* SPDX-License-Identifier: Apache-2.0 */
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { defineArivie, type ArivieInstance } from "@arivie/core";
import { postgresAdapter } from "@arivie/db-postgres";
import { resolveMixpanelSource } from "./lib/mixpanel-source.js";
import { makeMcpServer } from "@arivie/mcp";
import type { MCPServer } from "@mastra/mcp";
import { loadSemanticLayerSync, type SemanticLayer } from "@arivie/semantic";
import { anthropic } from "@ai-sdk/anthropic";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createOpenAI } from "@ai-sdk/openai";
import type { LanguageModel } from "ai";
import { MockLanguageModelV3 } from "ai/test";
const __dirname = dirname(fileURLToPath(import.meta.url));
const semanticPath = join(__dirname, "semantic");
function requireDatabaseUrl(): string {
const url = process.env.DATABASE_URL;
if (url == null || url.length === 0) {
throw new Error(
"DATABASE_URL is required — copy .env.example to .env and set your Postgres URL",
);
}
return url;
}
function resolveModel(): LanguageModel {
// Preference order: Gemini (cheapest) → Anthropic → OpenAI → mock.
// Override the Gemini default via GOOGLE_MODEL (e.g. "gemini-2.5-flash").
const googleKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
if (googleKey != null && googleKey.length > 0) {
const google = createGoogleGenerativeAI({ apiKey: googleKey });
const modelId = process.env.GOOGLE_MODEL ?? "gemini-2.5-flash";
return google(modelId) as LanguageModel;
}
const anthropicKey = process.env.ANTHROPIC_API_KEY;
if (anthropicKey != null && anthropicKey.length > 0) {
return anthropic("claude-sonnet-4-20250514");
}
const openaiKey = process.env.OPENAI_API_KEY;
if (openaiKey != null && openaiKey.length > 0) {
const openai = createOpenAI({ apiKey: openaiKey });
return openai(process.env.OPENAI_MODEL ?? "gpt-5") as LanguageModel;
}
console.warn(
"[with-nextjs] No model key set (GOOGLE_GENERATIVE_AI_API_KEY|ANTHROPIC_API_KEY|OPENAI_API_KEY) — using deterministic mock model.",
);
return new MockLanguageModelV3({
provider: "mock",
modelId: "mock",
doGenerate: {
content: [
{
type: "text",
text: "Example mock response (set GOOGLE_GENERATIVE_AI_API_KEY for a live Gemini run).",
},
],
finishReason: "stop",
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
warnings: [],
},
} as unknown as ConstructorParameters<typeof MockLanguageModelV3>[0]) as LanguageModel;
}
let cached: {
arivie: ArivieInstance;
mcp: MCPServer;
semantic: SemanticLayer;
} | undefined;
export async function getArivieRuntime(): Promise<{
arivie: ArivieInstance;
mcp: MCPServer;
}> {
if (cached == null) {
const postgres = postgresAdapter({
url: requireDatabaseUrl(),
readOnlyRole: "arivie_reader",
});
const { adapter: mixpanel, label: mixpanelLabel } = resolveMixpanelSource();
if (mixpanelLabel.includes("mock-plan-b")) {
console.warn(`[with-nextjs] ${mixpanelLabel}`);
}
const semantic = loadSemanticLayerSync(semanticPath);
const arivie = await defineArivie({
owner: {
id: process.env.ARIVIE_OWNER_ID ?? "with-nextjs-owner",
name: "With Next.js Example",
},
model: resolveModel(),
workspace: { rootDir: semanticPath },
sources: { postgres, mixpanel },
semantic: { path: semanticPath, mode: "preload" },
compileMetric: true, // JSON-IR routing for declared measures (arxiv 2502.00032)
resolveUser: async () => ({
userId: "demo-user",
permissions: ["analytics:read"],
dbRole: "arivie_reader",
}),
});
const mcp = makeMcpServer({
agent: arivie.agent,
semantic,
db: postgres,
ownerId: process.env.ARIVIE_OWNER_ID ?? "with-nextjs-owner",
ownerName: "With Next.js Example",
});
cached = { arivie, mcp, semantic };
}
return { arivie: cached.arivie, mcp: cached.mcp };
}
Terminal window
cd arivie && pnpm install
pnpm --filter with-nextjs dev
curl -X POST http://localhost:3000/api/arivie -H 'Content-Type: application/json' -d '{"prompt":"How many customers?"}'

Verify your audit sink receives tool.call and sql.executed events for the request.