Reading time: 8 minutes and 56 seconds.

Create an insight agent

Build an insight agent that uses an AI model to suggest calculated columns for a visualization’s data table.

Insight agents are a new kind of action mod introduced in the 2.5 API (Spotfire 15.0). An insight agent runs on a background thread where it can analyze the analysis, perform calculations, and consult an AI model to produce a collection of insights. Each insight can carry an action that the user may apply to the document.

In this tutorial we will build a visual insight agent that inspects the data table behind a visualization, asks an AI model to suggest useful calculated columns for that data, and offers each suggestion as an insight. Selecting a suggestion adds the calculated column to the table.

How insight agents work

An insight agent runs in two phases:

  • Insight phase – runs on a background thread against a read-only snapshot of the document. The agent can read the document, query data, run transformations, and call an AI model, but it cannot modify the document. When it is done it reports a result – a title, a summary, and any insights it found.
  • Action phase – runs on the application thread against the live document when the user applies an insight. This is an ordinary action mod script, so it can modify the document.

There are two kinds of agents, declared by the "type" field in the manifest’s "agents" array:

  • A "marking" agent is triggered after the user has marked data, and receives the marked rows.
  • A "visual" agent can be triggered from any visualization, and doesn’t require any marked data.

We will build a visual agent.

1. Set up the project

Developing mods requires the latest LTS version of Node.js. The recommended editor is Visual Studio Code.

Open a terminal in the folder where you want the project to live and create a new agent project from the SDK template:

npx @spotfire/mods-sdk@latest new agent -o calculated-column

This creates a calculated-column folder containing an agent project. Change into it, install the dependencies, and build it:

cd calculated-column
npm install
npm run build

Open the folder in Visual Studio Code and start the default build task with Ctrl+Shift+B. This launches the development server, the TypeScript typechecker, and the build watcher, so the mod is rebuilt whenever you change a file. You can now connect to the mod from the Mods development tool in Spotfire.

The template comes with a ready-made marking agent so that the project builds out of the box. Since we are building a visual agent, we will replace it in the next step.

2. Remove the default marking agent

The template defines two things we will not need:

  • a marking agent (marking-agent) in the "agents" array, and
  • the action it triggers (marking-insight) in the "scripts" array.

Delete the two source files:

rm src/scripts/marking-agent.ts src/scripts/marking-insight.ts

Then remove the marking-agent and marking-insight entries from mod-manifest.json so that both arrays are empty. The manifest should now look like this:

{
    "apiVersion": "2.5",
    "version": "1.0",
    "type": "action",
    "name": "Calculated Column",
    "id": "calculated-column",
    "description": "Suggests useful calculated columns for a visualization's data table.",
    "capabilities": ["AI"],
    "icon": "icon.svg",
    "scripts": [],
    "agents": []
}

The "AI" capability is required for any mod that uses the AI APIs. After editing the manifest, click Reload manifest in the Mods development tool.

3. Create the visual agent and its action

Use the SDK to add a visual agent and the script that will run when the user applies a suggestion:

npx @spotfire/mods-sdk add-agent calculated-column-agent --name "Calculated column agent" --type visual
npx @spotfire/mods-sdk add-script add-calculated-column --name "Add a calculated column"

The add-calculated-column action requires four parameters – the column name, the expression, the data table to add the column to, and an explanation to store as the column description. Add them with the add-parameter command:

npx @spotfire/mods-sdk add-parameter add-calculated-column name String
npx @spotfire/mods-sdk add-parameter add-calculated-column expression String
npx @spotfire/mods-sdk add-parameter add-calculated-column table DataTable
npx @spotfire/mods-sdk add-parameter add-calculated-column explanation String

The manifest now declares one agent and one script:

{
    "apiVersion": "2.5",
    "version": "1.0",
    "type": "action",
    "name": "Calculated Column",
    "id": "calculated-column",
    "description": "Suggests useful calculated columns for a visualization's data table.",
    "capabilities": ["AI"],
    "icon": "icon.svg",
    "scripts": [
        {
            "id": "add-calculated-column",
            "name": "Add a calculated column",
            "entryPoint": "addCalculatedColumn",
            "file": "build/add-calculated-column.js",
            "parameters": [
                { "name": "name", "type": "String" },
                { "name": "expression", "type": "String" },
                { "name": "table", "type": "DataTable" },
                { "name": "explanation", "type": "String" }
            ]
        }
    ],
    "agents": [
        {
            "id": "calculated-column-agent",
            "name": "Calculated column agent",
            "entryPoint": "calculatedColumnAgent",
            "file": "build/calculated-column-agent.js",
            "type": "visual"
        }
    ]
}

Each add-* command also creates a starter source file (src/scripts/calculated-column-agent.ts and src/scripts/add-calculated-column.ts). We will fill these in next.

4. Implement the agent

Define the structured output

We want the AI model to return a list of column suggestions, each with a name, a Spotfire expression, and an explanation. The template uses Zod to describe the expected shape and to turn it into a JSON schema we can hand to the model. Replace the contents of src/utils/schema.ts with:

import { z } from "zod";

/** The structured response we ask the AI model to produce. */
export const columnSuggestionsSchema = z.object({
    columns: z.array(
        z.object({
            name: z.string(),
            expression: z.string(),
            explanation: z.string()
        })
    )
});

export type ColumnSuggestions = z.infer<typeof columnSuggestionsSchema>;

/** The JSON schema string passed to the model as the structured output format. */
export const columnSuggestionsSchemaStr = JSON.stringify(z.toJSONSchema(columnSuggestionsSchema), null, 2);

Write the agent

The agent entry point receives a generated VisualContext object (the { context, resources, utils } wrapper). Its context property is a VisualInsightAgentContext that exposes the triggering visual, a read-only document snapshot, the AI service, and user interaction helpers for reporting progress.

The agent performs the following steps:

  1. Resolves the data table from the visual, bailing out early via ReportResult if there is no data.
  2. Asks the default AI model for column suggestions, requesting structured output via CompletionSettings.
  3. Turns each suggestion into an ActionModInsight whose action is an invocation of the add-calculated-column script.
  4. Reports the insights.

Replace the contents of src/scripts/calculated-column-agent.ts with the following:

import { columnSuggestionsSchema, columnSuggestionsSchemaStr } from "../utils/schema";

const { Visualization } = Spotfire.Dxp.Application.Visuals;
const { AiChatHistory, CompletionSettings } = Spotfire.Dxp.Framework.Ai;
const { ActionModInsight } = Spotfire.Dxp.Application.Insights;
const { ActionModScriptArgumentLiteral, ActionModScriptArgumentNode } = Spotfire.Dxp.Application.Mods;
const { List } = System.Collections.Generic;

const maxSuggestions = 5;

export function calculatedColumnAgent({ context, resources, utils }: VisualContext) {
    // The insight phase runs on a background thread against a read-only snapshot of the document.
    const visualization = context.Visual.As(Visualization);
    const table = visualization?.Data.DataTableReference;
    if (visualization == null || table == null || table.RowCount == 0 || table.Columns.Count == 0) {
        context.ReportResult("No data available", "This agent needs a visualization backed by a non-empty data table.");
        return;
    }

    context.UserInteraction.ReportProgress("Analyzing the data table...");

    const suggestions = getColumnSuggestions(context, table);
    if (suggestions == null || suggestions.columns.length == 0) {
        context.ReportResult(
            "No suggestions",
            "The AI model did not produce any calculated column suggestions for this data."
        );
        return;
    }

    // Turn each suggestion into an insight. Selecting it later runs the "add-calculated-column" script,
    // which performs the actual document modification on the application thread (the action phase).
    const insights = new List(ActionModInsight);
    for (const suggestion of suggestions.columns) {
        if (insights.Count >= maxSuggestions) {
            break;
        }

        if (suggestion.name === "" || suggestion.expression === "") {
            continue;
        }

        const columnName = `🤖 ${suggestion.name}`;
        insights.Add(
            new ActionModInsight(
                `Add column: ${columnName}`,
                suggestion.explanation,
                utils.CreateScriptInvocation("add-calculated-column", {
                    name: new ActionModScriptArgumentLiteral(columnName),
                    expression: new ActionModScriptArgumentLiteral(suggestion.expression),
                    table: new ActionModScriptArgumentNode(table),
                    explanation: new ActionModScriptArgumentLiteral(suggestion.explanation)
                })
            )
        );
    }

    context.ReportResult(
        "Calculated column suggestions",
        "AI-generated calculated columns that may add analytical value to your data.",
        insights
    );
}

/** Asks the default AI model to suggest calculated columns for the given table. */
function getColumnSuggestions(
    context: Spotfire.Dxp.Application.Insights.VisualInsightAgentContext,
    table: Spotfire.Dxp.Data.DataTable
) {
    const systemPrompt = `You are a data analyst assistant. The user has a data table and wants you to suggest
useful calculated columns that would provide additional analytical value.

Analyze the table name and its columns to infer the domain (e.g., finance, manufacturing,
oil & gas, healthcare, retail). Then suggest up to ${maxSuggestions} calculated columns that
would be valuable for someone working in that domain.

Rules:
- Each suggestion must include a column name, a valid Spotfire expression, and a brief explanation.
- Spotfire expressions can use arithmetic operators (+, -, *, /), functions like Abs(), Log(),
  Power(), Sqrt(), If(), Case(), Concatenate(), Year(), Month(), DateDiff(), and column
  references in square brackets like [ColumnName].
- Only reference columns that exist in the table. Do not invent column names.
- Suggest columns that derive meaningful new information: ratios, flags, categorizations,
  time-based derivations, normalized values, or domain-specific KPIs.
- Keep column names concise and descriptive.
- Return suggestions in order of usefulness.`;

    const userPrompt: string[] = [];
    userPrompt.push(`Table: "${table.Name}" (${table.RowCount} rows)`);
    userPrompt.push("Columns:");
    for (const column of table.Columns) {
        userPrompt.push(`- [${column.Name}] (${column.DataType.ToString()})`);
    }

    const history = new AiChatHistory();
    history.AddSystemMessage(systemPrompt);
    history.AddUserMessage(userPrompt.join("\n"));

    // Request a structured response by passing the JSON schema as the completion settings.
    const model = context.AiService.GetDefaultModel();
    const response = model.ChatCompletion(history, null, new CompletionSettings(columnSuggestionsSchemaStr));

    // The model may not honor the schema, so parse defensively.
    const parsed = columnSuggestionsSchema.safeParse(JSON.parse(response));
    return parsed.success ? parsed.data : null;
}

RegisterEntryPoint(calculatedColumnAgent);

A few things worth noting:

  • The structured response is requested by passing the JSON schema to CompletionSettings. The model returns a JSON string, which we parse with Zod – always defensively, since the model may not honor the schema.
  • The action carried by each insight is built with utils.CreateScriptInvocation. Literal values are wrapped in ActionModScriptArgumentLiteral and document nodes (like the table) in ActionModScriptArgumentNode. These argument types are checked against the parameters declared in the manifest.
  • Nothing in the agent modifies the document – that only happens in the action, which runs later.

Write the action

The add-calculated-column script runs on the application thread when the user applies a suggestion. It receives the parameters declared in the manifest and adds the calculated column. Replace the contents of src/scripts/add-calculated-column.ts with the following:

export function addCalculatedColumn({ table, name, expression, explanation }: AddCalculatedColumnParameters) {
    const column = table.Columns.AddCalculatedColumn(name, expression);
    column.Properties.Description = explanation;
}

RegisterEntryPoint(addCalculatedColumn);

5. Run, debug, and share

With the build watcher running, connect the mod from the Mods development tool (remember to Reload manifest after the manifest changes above). In Spotfire, create a visualization backed by a data table, then trigger the agent from the floating visualization toolbar or from the AI insights panel. The agent runs, the suggestions appear as insights, and applying one adds the calculated column to the data table.

To debug the agent, select your mod in the Mods development tool and click Start debugging. This shows a port (9222 by default) that you can connect to from Visual Studio Code by pressing F5. You can then set breakpoints in your script that will be hit the next time the agent runs. For more on debugging, see Developing action mods.

When you have finished developing the agent, disconnect it from the development server in the Mods development tool and save it to the library. To make it available in every analysis, pin it from the Spotfire library section of the Files and data flyout or the Actions flyout.

Running calculations during the insight phase

This agent only calls the AI model, but the insight phase runs on a background thread where you can do much more before reporting a result. Because the agent works against a read-only snapshot of the document, you can query data, build data flows, and even run data functions to compute the values your insights are based on — none of which blocks the user or changes the document. If you want to run calculations during the agent phase, start from these snippets:

Where to go next

Last modified July 1, 2026