Skip to main content
Solution accelerator
Customer-facing AI agents

Instrument an AI agent with behavioral tracking

Add client-side, server-side, and agent self-tracking to an AI-powered chatbot using Snowplow.

Progress0%

Track server-side agent execution

Client-side tracking tells you what the user did. Now you'll add server-side tracking for the agent's orchestration loop.

Every request to the chat API triggers an invocation - a complete cycle of the agent doing its work. Within an invocation, the agent takes multiple steps that are LLM reasoning iterations. Some steps include tool executions. When the agent has a final response, the invocation reaches completion.

All events in a single lifecycle share an invocation_id for correlation. The client-side message_received events include this ID, allowing you to link them with the corresponding server-side events.

Code-along or Read-along

If you're coding along, continue from the previous stage, and create the files described below.

If you're reading along:

bash
git checkout v0.2-server-tracking
npm install

To see exactly what changed: git diff v0.1-client-tracking..v0.2-server-tracking

What you'll add

This stage introduces:

  • One new dependency: @snowplow/node-tracker
  • Four event schemas from Iglu Central: agent_invocation, agent_step, tool_execution, agent_completion
  • Two entity schemas from Iglu Central: agent_context, tool_context
  • Two local custom entity schemas: tool_params, tool_results
  • One new file: src/lib/tracking/server.ts - the server tracking module
  • Modifications to:
    • Adding tracking to the agent lifecycle in src/app/api/chat/route.ts
    • Adding tracking to tools in src/lib/tools/business-tools.ts

Install the Snowplow tracker

Install the Node.js tracker:

bash
npm install @snowplow/node-tracker

Create custom entities

Check out the Schema reference section below for details on the Iglu Central schemas used for the server-side tracking. They capture the agent lifecycle - that a tool was called, how long it took, and whether it succeeded. These questions are generic to any agentic application.

In this section, you'll create two custom entities for domain-specific business data:

  • tool_params: the parameters passed to each business tool, e.g. origin, destination, dates
  • tool_results: the results returned from each business tool, e.g. flights found, booking ID

Create the tool_params entity at snowplow/iglu-local/schemas/com.snowplow.demo.travel/tool_params/jsonschema/1-0-0:

jsonsnowplow/iglu-local/schemas/com.snowplow.demo.travel/tool_params/jsonschema/1-0-0
{
"$schema": "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#",
"description": "Parameters passed to business tools in the travel demo app. Consolidated entity covering search_flights, book_flight, and check_calendar.",
"self": {
"vendor": "com.snowplow.demo.travel",
"name": "tool_params",
"format": "jsonschema",
"version": "1-0-0"
},
"type": "object",
"properties": {
"origin": {
"type": ["string", "null"],
"description": "Origin city or airport code (search_flights)",
"maxLength": 200
},
"destination": {
"type": ["string", "null"],
"description": "Destination city or airport code (search_flights)",
"maxLength": 200
},
"date": {
"type": ["string", "null"],
"description": "Departure date in YYYY-MM-DD format (search_flights)",
"maxLength": 10
},
"return_date": {
"type": ["string", "null"],
"description": "Return date for round-trip (search_flights)",
"maxLength": 10
},
"passengers": {
"type": ["integer", "null"],
"description": "Number of passengers (search_flights)",
"minimum": 1
},
"cabin_class": {
"type": ["string", "null"],
"description": "Cabin class (search_flights)",
"maxLength": 50
},
"sort_by": {
"type": ["string", "null"],
"description": "Sort order for results (search_flights)",
"maxLength": 50
},
"max_results": {
"type": ["integer", "null"],
"description": "Maximum number of results to return (search_flights)",
"minimum": 1
},
"flight_id": {
"type": ["string", "null"],
"description": "Unique identifier of the flight to book (book_flight)",
"maxLength": 200
},
"airline": {
"type": ["string", "null"],
"description": "Airline name (book_flight)",
"maxLength": 200
},
"flight_number": {
"type": ["string", "null"],
"description": "Flight number (book_flight)",
"maxLength": 50
},
"passenger_name": {
"type": ["string", "null"],
"description": "Passenger full name (book_flight)",
"maxLength": 500
},
"payment_method": {
"type": ["string", "null"],
"description": "Payment method (book_flight)",
"maxLength": 50
},
"start_date": {
"type": ["string", "null"],
"description": "Start date in YYYY-MM-DD format (check_calendar)",
"maxLength": 10
},
"end_date": {
"type": ["string", "null"],
"description": "End date in YYYY-MM-DD format (check_calendar)",
"maxLength": 10
},
"user_id": {
"type": ["string", "null"],
"description": "User ID for calendar check (check_calendar)",
"maxLength": 200
}
},
"additionalProperties": false
}

Create the tool_results entity at snowplow/iglu-local/schemas/com.snowplow.demo.travel/tool_results/jsonschema/1-0-0:

jsonsnowplow/iglu-local/schemas/com.snowplow.demo.travel/tool_results/jsonschema/1-0-0
{
"$schema": "http://iglucentral.com/schemas/com.snowplowanalytics.self-desc/schema/jsonschema/1-0-0#",
"description": "Results returned from business tools in the travel demo app. Consolidated entity covering search_flights, book_flight, and check_calendar.",
"self": {
"vendor": "com.snowplow.demo.travel",
"name": "tool_results",
"format": "jsonschema",
"version": "1-0-0"
},
"type": "object",
"properties": {
"flights_found": {
"type": ["integer", "null"],
"description": "Number of flights matching the search criteria (search_flights)",
"minimum": 0
},
"price_min": {
"type": ["number", "null"],
"description": "Lowest price among matching flights (search_flights)"
},
"price_max": {
"type": ["number", "null"],
"description": "Highest price among matching flights (search_flights)"
},
"price_currency": {
"type": ["string", "null"],
"description": "Currency code for price fields (search_flights)",
"maxLength": 10
},
"booking_id": {
"type": ["string", "null"],
"description": "Unique booking identifier (book_flight)",
"maxLength": 200
},
"confirmation_code": {
"type": ["string", "null"],
"description": "Booking confirmation code (book_flight)",
"maxLength": 50
},
"booking_status": {
"type": ["string", "null"],
"description": "Status of the booking (book_flight)",
"maxLength": 50
},
"conflicts_found": {
"type": ["integer", "null"],
"description": "Number of calendar conflicts found (check_calendar)",
"minimum": 0
},
"available_dates_count": {
"type": ["integer", "null"],
"description": "Number of available dates in the range (check_calendar)",
"minimum": 0
}
},
"additionalProperties": false
}

These schemas have a different vendor from the Iglu Central schemas: com.snowplow.demo.travel instead of com.snowplow.agent.tracking. This is your application's namespace, not that of the shared registry.

Because of the Snowplow Micro configuration in start.sh, it will automatically pick up schemas from iglu-local alongside those it resolves from Iglu Central.

Schema design considerations

Each of these schemas is compatible with all three business tools: which fields are populated depends on tool_context.tool_name. This keeps the demo application simple.

In a production application, we highly advise creating separate schemas per tool. For example, instead of one tool_params schema with nullable fields for the different tools, you'd have search_flights_params, book_flight_params, and check_calendar_params schemas with only the relevant fields, and corresponding search_flights_results, book_flight_results, and check_calendar_results schemas (for the relevant tools' inputs and outputs).

A per-tool approach is more extendable and has better type safety. Analysis will also be easier as the data for each tool would be separated.

Check out our tracking design best practise guide for more on schema design patterns.

Create the server tracking module

The server tracking module follows the same singleton pattern as the client module, but uses the Node.js tracker and server-side environment variables.

Initialize the tracker

typescriptsrc/lib/tracking/server.ts
import {
newTracker,
buildSelfDescribingEvent,
type Tracker,
} from '@snowplow/node-tracker';

let serverTracker: Tracker | null = null;

const initServerTracker = (): Tracker | null => {
if (serverTracker) return serverTracker;

const collectorUrl = process.env.SNOWPLOW_COLLECTOR_URL;
const appId = process.env.SNOWPLOW_APP_ID;

if (!collectorUrl || !appId) {
console.warn(
'Snowplow server tracker not initialized: missing SNOWPLOW_COLLECTOR_URL or SNOWPLOW_APP_ID',
);
return null;
}

serverTracker = newTracker(
{
namespace: 'travel-agent-server',
appId: appId,
encodeBase64: false,
},
{
endpoint: collectorUrl,
protocol: 'http',
eventMethod: 'post',
bufferSize: 1,
},
);

return serverTracker;
};

The setting bufferSize: 1 flushes events to the Collector immediately after each one is tracked. In production you'd use a larger buffer for efficiency, but for development this ensures events appear in Micro instantly.

The environment variables don't have the NEXT_PUBLIC_ prefix. They're server-only, and never included in the browser bundle.

Build the context entity helpers

The generic entity builders reference Iglu Central schemas:

typescriptsrc/lib/tracking/server.ts (continued)
export interface AgentContextData {
invocation_id: string;
session_id: string;
user_id?: string | null;
agent_type: string;
model_name: string;
model_provider: string;
conversation_messages_count?: number | null;
current_step_number?: number | null;
}

const buildAgentContext = (data: AgentContextData) => ({
schema: 'iglu:com.snowplow.agent.tracking/agent_context/jsonschema/1-0-0' as const,
data: data as unknown as Record<string, unknown>,
});

export interface ToolContextData {
tool_name: string;
tool_category: 'business' | 'self_tracking';
tool_call_id: string;
tool_description?: string | null;
}

const buildToolContext = (data: ToolContextData) => ({
schema: 'iglu:com.snowplow.agent.tracking/tool_context/jsonschema/1-0-0' as const,
data: data as unknown as Record<string, unknown>,
});

The custom entity builders reference the local schemas you created in iglu-local:

typescriptsrc/lib/tracking/server.ts (continued)
export interface ToolParamsData {
origin?: string | null;
destination?: string | null;
date?: string | null;
return_date?: string | null;
passengers?: number | null;
cabin_class?: string | null;
sort_by?: string | null;
max_results?: number | null;
flight_id?: string | null;
airline?: string | null;
flight_number?: string | null;
passenger_name?: string | null;
payment_method?: string | null;
start_date?: string | null;
end_date?: string | null;
user_id?: string | null;
}

const buildToolParams = (data: ToolParamsData) => ({
schema: 'iglu:com.snowplow.demo.travel/tool_params/jsonschema/1-0-0' as const,
data: data as unknown as Record<string, unknown>,
});

export interface ToolResultsData {
flights_found?: number | null;
price_min?: number | null;
price_max?: number | null;
price_currency?: string | null;
booking_id?: string | null;
confirmation_code?: string | null;
booking_status?: string | null;
conflicts_found?: number | null;
available_dates_count?: number | null;
}

const buildToolResults = (data: ToolResultsData) => ({
schema: 'iglu:com.snowplow.demo.travel/tool_results/jsonschema/1-0-0' as const,
data: data as unknown as Record<string, unknown>,
});

Add the tracking functions

Each lifecycle event gets its own function. All four tracking functions: lazy-initialize the tracker, return early if it can't initialize, build the event, and attach the relevant entities.

Here's trackAgentInvocation:

typescriptsrc/lib/tracking/server.ts (continued)
export const trackAgentInvocation = (params: AgentInvocationParams) => {
const t = initServerTracker();
if (!t) return;

t.track(
buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplow.agent.tracking/agent_invocation/jsonschema/1-0-0',
data: {
invocation_id: params.invocationId,
session_id: params.sessionId,
user_message_preview: params.userMessagePreview ?? null,
invoked_at: new Date().toISOString(),
},
},
}),
[
buildAgentContext({
invocation_id: params.invocationId,
session_id: params.sessionId,
agent_type: params.agentType || 'travel_assistant',
model_name: params.modelName,
model_provider: params.modelProvider,
conversation_messages_count: params.conversationMessagesCount ?? null,
}),
],
);
};

trackAgentStep tracks each loop iteration and captures token usage, step type, and finish reason:

typescriptsrc/lib/tracking/server.ts (continued)
export const trackAgentStep = (params: AgentStepParams) => {
const t = initServerTracker();
if (!t) return;

t.track(
buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplow.agent.tracking/agent_step/jsonschema/1-0-0',
data: {
invocation_id: params.invocationId,
step_number: params.stepNumber,
step_type: params.stepType,
prompt_tokens: params.promptTokens,
completion_tokens: params.completionTokens,
finish_reason: params.finishReason ?? null,
tool_calls_count: params.toolCallsCount,
text_length: params.textLength ?? null,
step_duration_ms: params.stepDurationMs ?? null,
stepped_at: new Date().toISOString(),
},
},
}),
[
buildAgentContext({
invocation_id: params.invocationId,
session_id: params.sessionId,
agent_type: 'travel_assistant',
model_name: params.modelName,
model_provider: params.modelProvider,
conversation_messages_count: params.conversationMessagesCount ?? null,
current_step_number: params.stepNumber,
}),
],
);
};

trackToolExecution always attaches tool_context and agent_context entities, carrying the tool name, category, and invocation metadata. When the caller supplies toolParams and toolResults, those are pushed as additional custom entities under com.snowplow.demo.travel so the domain-specific input and output fields are recorded without bloating the generic schemas:

typescriptsrc/lib/tracking/server.ts (continued)
export const trackToolExecution = (params: ToolExecutionParams) => {
const t = initServerTracker();
if (!t) return;

const contexts: Array<{ schema: string; data: Record<string, unknown> }> = [
buildToolContext({
tool_name: params.toolName,
tool_category: params.toolCategory,
tool_call_id: params.toolCallId,
tool_description: params.toolDescription ?? null,
}),
buildAgentContext({
invocation_id: params.invocationId,
session_id: params.sessionId,
agent_type: 'travel_assistant',
model_name: params.modelName,
model_provider: params.modelProvider,
current_step_number: params.currentStepNumber ?? null,
}),
];

if (params.toolParams) {
contexts.push(buildToolParams(params.toolParams));
}

if (params.toolResults) {
contexts.push(buildToolResults(params.toolResults));
}

t.track(
buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplow.agent.tracking/tool_execution/jsonschema/1-0-0',
data: {
invocation_id: params.invocationId,
step_number: params.stepNumber ?? null,
execution_duration_ms: params.executionDurationMs,
success: params.success,
error_type: params.errorType ?? null,
error_message: params.errorMessage ?? null,
executed_at: new Date().toISOString(),
},
},
}),
contexts,
);
};

trackAgentCompletion fires when the loop ends and records aggregate metrics across the whole invocation:

typescriptsrc/lib/tracking/server.ts (continued)
export const trackAgentCompletion = (params: AgentCompletionParams) => {
const t = initServerTracker();
if (!t) return;

t.track(
buildSelfDescribingEvent({
event: {
schema: 'iglu:com.snowplow.agent.tracking/agent_completion/jsonschema/1-0-0',
data: {
invocation_id: params.invocationId,
total_steps: params.totalSteps,
total_duration_ms: params.totalDurationMs,
total_tokens: params.totalTokens,
tools_called: params.toolsCalled,
business_tools_called: params.businessToolsCalled ?? null,
self_tracking_tools_called: params.selfTrackingToolsCalled ?? null,
finish_reason: params.finishReason,
success: params.success,
final_response_length: params.finalResponseLength ?? null,
completed_at: new Date().toISOString(),
},
},
}),
[
buildAgentContext({
invocation_id: params.invocationId,
session_id: params.sessionId,
agent_type: 'travel_assistant',
model_name: params.modelName,
model_provider: params.modelProvider,
}),
],
);
};

Wire tracking into the chat route

The chat route needs a request-scoped context object to track state across the entire invocation, and hooks into the Vercel AI SDK's lifecycle callbacks.

Add the request context

typescriptsrc/app/api/chat/route.ts
export interface RequestContext {
invocationId: string;
sessionId: string;
stepNumber: number;
invocationStartTime: number;
totalToolsCalled: number;
businessToolsCalled: number;
selfTrackingToolsCalled: number;
modelName: string;
modelProvider: ModelProvider;
}

This mutable context is created at the start of each request and passed to all tools and callbacks. It accumulates counters (steps taken, tools called) as the invocation progresses.

Track the invocation at request entry

At the top of the POST handler, create the context and fire the invocation event:

typescriptsrc/app/api/chat/route.ts
const requestContext: RequestContext = {
invocationId: crypto.randomUUID(),
sessionId: sessionId || crypto.randomUUID(),
stepNumber: 1,
invocationStartTime: Date.now(),
totalToolsCalled: 0,
businessToolsCalled: 0,
selfTrackingToolsCalled: 0,
modelName: modelConfig.id,
modelProvider: modelConfig.provider,
};

trackAgentInvocation({
invocationId: requestContext.invocationId,
sessionId: requestContext.sessionId,
userMessagePreview: userMessagePreview.substring(0, 500),
agentType: 'travel_assistant',
modelName: requestContext.modelName,
modelProvider: requestContext.modelProvider,
conversationMessagesCount: messages.length,
});

Track steps and completion via callbacks

The Vercel AI SDK provides onStepFinish and onFinish callbacks:

typescriptsrc/app/api/chat/route.ts
const result = streamText({
model: model,
messages: modelMessages,
stopWhen: stepCountIs(10),
system: `...`,
tools: {
search_flights: createSearchFlightsTool(requestContext),
book_flight: createBookFlightTool(requestContext),
check_calendar: createCheckCalendarTool(requestContext),
},
onStepFinish: async ({ text, toolCalls, usage, finishReason }) => {
const stepType =
requestContext.stepNumber === 1
? 'initial'
: toolCalls.length > 0
? 'continue'
: 'tool-result';

trackAgentStep({
invocationId: requestContext.invocationId,
sessionId: requestContext.sessionId,
stepNumber: requestContext.stepNumber,
stepType,
inputTokens: usage.inputTokens ?? 0,
outputTokens: usage.outputTokens ?? 0,
finishReason: mapFinishReasonForStep(finishReason),
toolCallsCount: toolCalls.length,
textLength: text.length,
modelName: requestContext.modelName,
modelProvider: requestContext.modelProvider,
conversationMessagesCount: messages.length,
});

requestContext.stepNumber++;
},
onFinish: async ({ text, finishReason, totalUsage }) => {
const totalDuration = Date.now() - requestContext.invocationStartTime;
const totalTokens =
totalUsage.totalTokens ??
(totalUsage.inputTokens ?? 0) + (totalUsage.outputTokens ?? 0);

trackAgentCompletion({
invocationId: requestContext.invocationId,
sessionId: requestContext.sessionId,
totalSteps: requestContext.stepNumber,
totalDurationMs: totalDuration,
totalTokens,
toolsCalled: requestContext.totalToolsCalled,
businessToolsCalled: requestContext.businessToolsCalled,
selfTrackingToolsCalled: requestContext.selfTrackingToolsCalled,
finishReason: finishReason === 'error' ? 'error' : 'stop',
success: finishReason !== 'error',
finalResponseLength: text.length,
modelName: requestContext.modelName,
modelProvider: requestContext.modelProvider,
});
},
});

The tool factories receive requestContext as a parameter - createSearchFlightsTool(requestContext) - so they can access the shared context for tracking.

Instrument the business tools

Each tool factory takes a RequestContext parameter and wraps its execution with timing and tracking. The domain-specific data - what was searched for, what came back - is attached as custom entities:

typescriptsrc/lib/tools/business-tools.ts
export function createSearchFlightsTool(ctx: RequestContext) {
return tool({
description: 'Search for flights between two cities on a specific date',
inputSchema: searchFlightsSchema,
execute: async (params) => {
const startTime = Date.now();
const toolCallId = crypto.randomUUID();
ctx.totalToolsCalled++;
ctx.businessToolsCalled++;

try {
const results = await searchFlights(params);
const duration = Date.now() - startTime;

trackToolExecution({
invocationId: ctx.invocationId,
sessionId: ctx.sessionId,
stepNumber: ctx.stepNumber,
toolCallId,
toolName: 'search_flights',
toolCategory: 'business',
executionDurationMs: duration,
success: true,
toolParams: {
origin: params.origin,
destination: params.destination,
date: params.date,
return_date: params.return_date ?? null,
passengers: params.passengers,
cabin_class: params.cabin_class,
sort_by: params.sort_by,
max_results: params.max_results,
},
toolResults: {
flights_found: results.flights.length,
price_min:
results.flights.length > 0
? Math.min(...results.flights.map((f) => f.price.amount))
: null,
price_max:
results.flights.length > 0
? Math.max(...results.flights.map((f) => f.price.amount))
: null,
price_currency:
results.flights.length > 0
? results.flights[0].price.currency
: null,
},
modelName: ctx.modelName,
modelProvider: ctx.modelProvider,
currentStepNumber: ctx.stepNumber,
});

return results;
} catch (error) {
const duration = Date.now() - startTime;

trackToolExecution({
invocationId: ctx.invocationId,
sessionId: ctx.sessionId,
stepNumber: ctx.stepNumber,
toolCallId,
toolName: 'search_flights',
toolCategory: 'business',
executionDurationMs: duration,
success: false,
errorType: 'execution_error',
errorMessage: error instanceof Error ? error.message : 'Unknown error',
modelName: ctx.modelName,
modelProvider: ctx.modelProvider,
currentStepNumber: ctx.stepNumber,
});

throw error;
}
},
});
}

A few things to note in the code above:

  • startTime is captured before execution, duration calculated after
  • ctx.totalToolsCalled++ and ctx.businessToolsCalled++ keep the counters accurate for the completion event
  • toolParams and toolResults are passed as structured objects that trackToolExecution attaches as custom entities alongside tool_context and agent_context
  • Both success and failure paths are tracked - failure records errorType and errorMessage instead of custom entities
  • search_flights populates origin, destination, date in params and flights_found, price_min, price_max in results; book_flight and check_calendar populate their respective fields in the same consolidated schemas

Try it out

Valid API key required

Check that .env.local has a valid API key for the model you plan to use. Placeholder values like sk-ant-... cause silent failures (the app may appear to run but the model will not respond correctly).

bash
git checkout v0.2-server-tracking # (or verify your code-along)
npm run start:dev
  1. Send "Find flights from London to Paris tomorrow"
  2. Open Snowplow Micro UI at http://localhost:9090/micro/ui - press Refresh to see both client and server events arriving
  3. Find the agent_invocation event - note the invocation_id that links all events in this lifecycle
  4. Find the agent_step events - observe step_number incrementing, token counts, and finish_reason ("tool_calls" when the agent wants to call a tool, "stop" when it has a final response)
  5. Find the tool_execution for search_flights - note execution_duration_ms and the success: true flag. Drill into the event's entities and find the tool_params entity (with origin, destination, date) and tool_results entity (with flights_found, price_min, price_max) alongside the generic tool_context and agent_context
  6. Find the agent_completion - note total_steps, total_tokens, total_duration_ms, and the aggregate tool counts
  7. Trace the invocation_id across all events - use the Micro UI to drill into each event's entities and see how they form a complete lifecycle linked by this ID

You now have visibility into both the user's actions and the agent's execution.

Schema reference

The six schemas used in this stage are published on Iglu Central under vendor com.snowplow.agent.tracking, just like the client-side schemas from the previous stage.

All the events have the invocation_id.

agent_context entity

The agent_context entity is attached to every server-side event. It identifies the invocation, session, model, and current state.

agent_context

Entity
Context entity describing the agent, its configuration, and current state when performing actions.
Schema URIiglu:com.snowplow.agent.tracking/agent_context/jsonschema/1-0-0
Example
json
{
"invocation_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": null,
"agent_type": "travel_assistant",
"model_name": "claude-sonnet-4-20250514",
"model_provider": "anthropic",
"application_version": "1.0.0",
"conversation_messages_count": 3,
"current_step_number": 2
}
Properties and schema
PropertyDescription
invocation_id
string
Required. Unique identifier for current agent invocation
session_id
string
Required. User session identifier
user_id
string
Optional. User identifier if authenticated
agent_type
string
Required. Type/name of agent
model_name
string
Required. LLM model identifier (e.g., claude-sonnet-4-20250514)
model_provider
string
Required. LLM provider (e.g., anthropic, openai)
application_version
string
Optional. Application version
conversation_messages_count
integer
Optional. Number of messages in conversation history at this point
current_step_number
integer
Optional. Current step number within the invocation

tool_context entity

The tool_context entity is attached to tool-related events. It identifies the tool and its category.

tool_context

Entity
Context entity describing a tool (function) being invoked by the agent, including its purpose, category, and parameters.
Schema URIiglu:com.snowplow.agent.tracking/tool_context/jsonschema/1-0-0
Example
json
{
"tool_name": "search_flights",
"tool_category": "business",
"tool_call_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"tool_description": "Search for flights between two cities on a specific date"
}
Properties and schema
PropertyDescription
tool_name
string
Required. Name of the tool being executed
tool_category
string
Required. Category of tool (application-defined)
tool_call_id
string
Required. Unique identifier for this specific tool invocation
tool_description
string
Optional. Brief description of what this tool does

agent_invocation event

The agent_invocation event fires when the chat API receives a request. It starts the lifecycle — every subsequent event in this invocation shares the same invocation_id. The user_message_preview field captures the first 500 characters of the user's message for context; full message content is never stored.

agent_invocation

Event
Marks the start of an agent invocation. Agent details and state are in the attached agent_context entity.
Schema URIiglu:com.snowplow.agent.tracking/agent_invocation/jsonschema/1-0-0
Example
json
{
"invocation_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"user_message_preview": "Find flights from London to Paris tomorrow",
"invoked_at": "2024-01-15T10:30:00.000Z"
}
Properties and schema
PropertyDescription
invocation_id
string
Required. Unique identifier for this agent invocation
session_id
string
Required. User session identifier
user_message_preview
string
Optional. Truncated/sanitized user message that triggered invocation
invoked_at
string
Required. Timestamp when invocation started
Warehouse query
sql
select
unstruct_event_com_snowplow_agent_tracking_agent_invocation_1_0_0
from
PIPELINE_NAME.events events
where
events.collector_tstamp > timestamp_sub(current_timestamp(), interval 1 hour)
and events.event = 'unstruct'
and events.event_name = 'agent_invocation'
and events.event_vendor = 'com.snowplow.agent.tracking'

agent_step event

The agent_step event fires at the end of each LLM iteration. step_type indicates where in the reasoning loop this step falls: initial for the first LLM call, tool-result when the model is processing tool output, and continue for intermediate steps. finish_reason tells you why the model stopped: tool_calls means it wants to invoke a tool next, stop means it has a final answer.

agent_step

Event
Records a single iteration step in the agent reasoning loop. Agent context provides invocation details.
Schema URIiglu:com.snowplow.agent.tracking/agent_step/jsonschema/1-0-0
Example
json
{
"invocation_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"step_number": 2,
"step_type": "tool-result",
"input_tokens": 1250,
"output_tokens": 87,
"finish_reason": "tool_calls",
"tool_calls_count": 1,
"text_length": null,
"step_duration_ms": 820,
"stepped_at": "2024-01-15T10:30:01.820Z"
}
Properties and schema
PropertyDescription
invocation_id
string
Required. Parent invocation identifier
step_number
integer
Required. Sequential step number in this invocation
step_type
string
Required. Type of step in the agent loop
Must be one of: initial, continue, tool-result
input_tokens
integer
Required. Input tokens for this step
output_tokens
integer
Required. Output tokens for this step
finish_reason
string
Optional. Why the model stopped generating
Must be one of: stop, length, tool_calls, content_filter,
tool_calls_count
integer
Required. Number of tool calls made in this step
text_length
integer
Optional. Length of text generated in this step
step_duration_ms
integer
Optional. Duration of this step
stepped_at
string
Required. Timestamp when step completed
Warehouse query
sql
select
unstruct_event_com_snowplow_agent_tracking_agent_step_1_0_0
from
PIPELINE_NAME.events events
where
events.collector_tstamp > timestamp_sub(current_timestamp(), interval 1 hour)
and events.event = 'unstruct'
and events.event_name = 'agent_step'
and events.event_vendor = 'com.snowplow.agent.tracking'

tool_execution event

The tool_execution event fires when a tool call completes. The event captures lifecycle data — timing and success or failure — while the domain-specific data lives in the custom tool_params and tool_results entities attached alongside tool_context and agent_context. On a failed call, error_type and error_message are populated instead.

tool_execution

Event
Records execution of an agent tool. Tool details are in the attached tool_context entity. Agent state is in agent_context entity.
Schema URIiglu:com.snowplow.agent.tracking/tool_execution/jsonschema/1-0-0
Example
json
{
"invocation_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"step_number": 2,
"execution_duration_ms": 340,
"success": true,
"error_type": null,
"error_message": null,
"executed_at": "2024-01-15T10:30:01.480Z"
}
Properties and schema
PropertyDescription
invocation_id
string
Required. Parent invocation identifier
step_number
integer
Optional. Step number within invocation
execution_duration_ms
integer
Required. Time taken to execute the tool
success
boolean
Required. Whether tool execution succeeded
error_type
string
Optional. Type of error if execution failed
error_message
string
Optional. Error message if execution failed
executed_at
string
Required. Timestamp when tool execution completed
Warehouse query
sql
select
unstruct_event_com_snowplow_agent_tracking_tool_execution_1_0_0
from
PIPELINE_NAME.events events
where
events.collector_tstamp > timestamp_sub(current_timestamp(), interval 1 hour)
and events.event = 'unstruct'
and events.event_name = 'tool_execution'
and events.event_vendor = 'com.snowplow.agent.tracking'

agent_completion event

The agent_completion event fires when the invocation ends and provides aggregate metrics across the entire lifecycle. finish_reason: max_steps means the agent hit the stopWhen: stepCountIs(10) limit without reaching a final answer — a useful signal for identifying queries that are hitting complexity limits.

agent_completion

Event
Records completion of an agent invocation with summary metrics. Agent context provides invocation details.
Schema URIiglu:com.snowplow.agent.tracking/agent_completion/jsonschema/1-0-0
Example
json
{
"invocation_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"total_steps": 3,
"total_duration_ms": 2850,
"total_tokens": 3420,
"tools_called": 1,
"business_tools_called": 1,
"self_tracking_tools_called": 0,
"finish_reason": "stop",
"success": true,
"final_response_length": 312,
"completed_at": "2024-01-15T10:30:02.850Z"
}
Properties and schema
PropertyDescription
invocation_id
string
Required. Invocation identifier
total_steps
integer
Required. Total number of reasoning steps
total_duration_ms
integer
Required. Total time from invocation to completion
total_tokens
integer
Required. Total tokens used (prompt + completion)
tools_called
integer
Required. Total number of tool calls made
business_tools_called
integer
Optional. Number of business tools called
self_tracking_tools_called
integer
Optional. Number of self-tracking tools called
finish_reason
string
Required. Why the agent stopped
Must be one of: stop, length, error, max_steps
success
boolean
Required. Whether invocation completed successfully
final_response_length
integer
Optional. Length of final response to user
completed_at
string
Required. Timestamp when invocation completed
Warehouse query
sql
select
unstruct_event_com_snowplow_agent_tracking_agent_completion_1_0_0
from
PIPELINE_NAME.events events
where
events.collector_tstamp > timestamp_sub(current_timestamp(), interval 1 hour)
and events.event = 'unstruct'
and events.event_name = 'agent_completion'
and events.event_vendor = 'com.snowplow.agent.tracking'