---
title: "Toolkit"
framework: javascript
version: "2.1.2"
---

# Toolkit

The Toolkit exposes Studio's built-in actions as standalone, framework-agnostic units, so you can drive the AI loop in your own runtime instead of the built-in one. A custom runtime needs no agents - you decide what the model can do by choosing which commands to expose as tools.

## Commands and Tools

The two are easy to conflate, so it is worth being precise:

- A **command** is the action itself. It validates its input, mutates state or runs a query, and returns a success or failure result. It has no name or description of its own.
- A **tool** is a command wrapped with a name, description, and status text - the form an LLM can discover and call.

Studio's [Built-in Tools](https://www.ag-grid.com/studio/javascript/ai-ax#tool-reference) are commands wrapped this way for the built-in runtime. The Toolkit hands you the command; you supply the wrapper your runtime expects.

## Retrieving a Command

Pass an `AgBuiltInAiCommandRef` to `api.defineAiCommand`:

```ts
const command = api.defineAiCommand({ type: 'AgExecuteQueryCommand' });

const schema = command.toJSONSchema();     // hand to your runtime as a tool definition
const result = await command.apply(args);  // { success: true, value } | { success: false, error }
```

Commands that carry configuration take a `params` field. `AgConfigureWidgetCommand` narrows its schema by widget type:

```ts
const configureBar = api.defineAiCommand({
    type: 'AgConfigureWidgetCommand',
    params: { widgetType: 'bar-chart-grouped' },
});
```

## Wrapping a Command as a Tool

You own the name, description, and status text - and any extra work around execution, such as validating input or trimming the result before it returns to the model.

```ts
import { tool } from 'your-ai-runtime';

const executeQueryCommand = api.defineAiCommand({ type: 'AgExecuteQueryCommand' });

const executeQueryTool = tool({
    name: 'execute_query',
    description: 'Query the full dataset available',
    schema: executeQueryCommand.toJSONSchema(),
    execute: async (params) => {
        const result = await executeQueryCommand.apply(params);

        if (!result.success) {
            return result;
        }

        // Trim to 50 rows to avoid overwhelming the context
        return result.value.slice(0, 50);
    },
});
```

Commands never throw - `apply()` returns a discriminated result, so handle the failure branch rather than wrapping calls in `try`/`catch`. To describe the data to the model or validate arguments first, pull the current schema and vocabulary from [Context](https://www.ag-grid.com/studio/javascript/ai-context/).

## Built-In Commands

| Ref `type` | What it does |
| --- | --- |
| `AgExecuteQueryCommand` | Run a query against the data source. Supports aggregation (group-by with measures) and projection (raw row selection). |
| `AgAddPageFilterCommand` | Append a filter to the page-level filter list. |
| `AgRemovePageFilterCommand` | Remove a page-level filter, matching by its current state. |
| `AgAddWidgetFilterCommand` | Add a filter scoped to a single widget. |
| `AgRemoveWidgetFilterCommand` | Remove a widget-level filter, matching by its current state. |
| `AgAddWidgetCommand` | Add a widget to the canvas. |
| `AgPositionWidgetCommand` | Move or resize a widget. Omitted position fields preserve current values. |
| `AgRemoveWidgetCommand` | Permanently remove a widget from the page. |
| `AgConfigureWidgetCommand` | Configure a widget's data mapping, title, and formatting. Schema narrows by `params.widgetType`. |

Each button in the example sends a hardcoded prompt to OpenAI along with the matching command's JSON Schema as a single forced tool. The LLM returns tool arguments, which are handed straight to `command.apply()`. The console shows the message, schema, returned arguments, and command result for each.

#### Toolkit

```ts
import { openaiAdapter } from "./shared/openaiAdapter.ts";
import {
  AgAiConversationItem,
  AgAiRequest,
  AgAiToolCall,
  AgBuiltInAiCommandRef,
  AgReportState,
  AgStudioApi,
  AgStudioProperties,
  AgToolSchema,
  createStudio,
} from "ag-studio";
import { salesData } from "./data.ts";

export const AI_API_URL = "https://ai-api.ag-grid.com/api/openai/v1";
export const AI_API_TOKEN = "";

const initialState: AgReportState = {
  pages: [
    {
      id: "main",
      widgets: {
        "revenue-by-region": {
          type: "bar-chart-grouped",
          dataMapping: {
            categoryKey: [{ id: "sales.region" }],
            valueKey: [{ id: "sales.revenue", aggregation: "sum" }],
          },
          format: { caption: { enabled: true, text: "Revenue by Region" } },
        },
      },
      widgetLayout: {
        "revenue-by-region": { xTrack: 0, yTrack: 0, xSpan: 24, ySpan: 18 },
      },
    },
  ],
  selectedPageId: "main",
};

const studioProperties: AgStudioProperties = {
  mode: "edit",
  initialState,
  data: salesData,
};

interface CommandDemo {
  label: string;
  ref: AgBuiltInAiCommandRef;
  prompt: string;
}

const SYSTEM_INSTRUCTIONS = [
  "You are operating an AG Studio dashboard via a single tool call.",
  'The page id is "main". It contains one widget: id "revenue-by-region", type "bar-chart-grouped".',
  'The data source "sales" exposes fields: region (text), product (text), revenue (currency).',
  "Call the provided tool exactly once with arguments that satisfy the user request and the tool schema.",
].join(" ");

const COMMANDS: CommandDemo[] = [
  {
    label: "Execute Query",
    ref: { type: "AgExecuteQueryCommand" },
    prompt: "Run a query that returns average revenue per product.",
  },
  {
    label: "Add Page Filter",
    ref: { type: "AgAddPageFilterCommand" },
    prompt: "Filter the page so only the EMEA region is included.",
  },
  {
    label: "Remove Page Filter",
    ref: { type: "AgRemovePageFilterCommand" },
    prompt: "Remove the page filter that is currently restricting the region.",
  },
  {
    label: "Add Widget Filter",
    ref: { type: "AgAddWidgetFilterCommand" },
    prompt:
      "On the revenue-by-region widget, filter so only the Technology product is shown.",
  },
  {
    label: "Remove Widget Filter",
    ref: { type: "AgRemoveWidgetFilterCommand" },
    prompt: "Remove the product filter from the revenue-by-region widget.",
  },
  {
    label: "Add Widget",
    ref: { type: "AgAddWidgetCommand" },
    prompt:
      "Add a new KPI (value) widget showing total revenue. Place it at xTrack 0, yTrack 18, spanning 8 columns and 6 rows.",
  },
  {
    label: "Position Widget",
    ref: { type: "AgPositionWidgetCommand" },
    prompt:
      "Move the revenue-by-region widget to the right half of the page (xTrack 12, yTrack 0, xSpan 12, ySpan 18).",
  },
  {
    label: "Remove Widget",
    ref: { type: "AgRemoveWidgetCommand" },
    prompt: "Delete the revenue-by-region widget.",
  },
  {
    label: "Configure Widget",
    ref: {
      type: "AgConfigureWidgetCommand",
      params: { widgetType: "bar-chart-grouped" },
    },
    prompt:
      'Re-caption the revenue-by-region widget to "Total Revenue by Region".',
  },
];

const assistant = openaiAdapter({ endpoint: AI_API_URL, key: AI_API_TOKEN });

let studioApi: AgStudioApi;

const studioDiv = document.querySelector<HTMLElement>("#myStudio")!;
studioApi = createStudio(studioDiv, studioProperties);

const bar = document.querySelector<HTMLElement>("#toolkitBar")!;
COMMANDS.forEach((demo) => {
  const button = document.createElement("button");
  button.type = "button";
  button.textContent = demo.label;
  button.addEventListener("click", async () => {
    button.disabled = true;
    try {
      await runDemo(demo);
    } catch (err) {
      console.error("[ai-toolkit] failed", demo.ref.type, err);
    } finally {
      button.disabled = false;
    }
  });
  bar.appendChild(button);
});

async function runDemo(demo: CommandDemo): Promise<void> {
  const command = studioApi.defineAiCommand(demo.ref);
  const schema = command.toJSONSchema() as Record<string, unknown>;
  // OpenAI's Responses API requires the tool `parameters` root to be an
  // object schema - `anyOf`/`oneOf` at the root (which several of our
  // lens-derived command schemas produce) is rejected. Wrap every schema
  // in a single-property envelope and unwrap before `apply()`. Hoist
  // `$defs` to the wrapper root so `#/$defs/...` refs still resolve.
  const { $defs, ...inner } = schema;
  const wrappedSchema: Record<string, unknown> = {
    type: "object",
    properties: { command: inner },
    required: ["command"],
    additionalProperties: false,
  };
  if ($defs !== undefined) wrappedSchema.$defs = $defs;
  const tool: AgToolSchema = {
    name: demo.ref.type,
    description: `Built-in AG Studio command: ${demo.ref.type}`,
    parameters: wrappedSchema as AgToolSchema["parameters"],
  };

  console.log(`[ai-toolkit] ${demo.ref.type} - message sent:`, demo.prompt);
  console.log(`[ai-toolkit] ${demo.ref.type} - tool schema:`, tool);

  const userMessage: AgAiConversationItem = {
    id: `msg-${Date.now()}`,
    kind: "input",
    type: "message",
    role: "user",
    content: [{ type: "text", text: demo.prompt }],
    status: "completed",
  };

  const request: AgAiRequest = {
    input: [userMessage],
    instructions: SYSTEM_INSTRUCTIONS,
    tools: [tool],
    toolChoice: { name: tool.name },
    responseFormat: { type: "text" },
  };

  const handler = assistant.executeTurn(request);
  // The adapter's `complete` only resolves once the stream is drained;
  // the underlying fetch is initiated lazily by the async iterator.
  for await (const _event of handler.stream) {
    // Drain - we only care about the final response, not intermediate events.
  }
  const response = await handler.complete;
  const toolCall = response.output.find(
    (item): item is AgAiToolCall => item.type === "function_call",
  );
  if (!toolCall) {
    console.warn(
      `[ai-toolkit] ${demo.ref.type} - LLM returned no tool call`,
      response,
    );
    return;
  }

  const wrappedArgs = JSON.parse(toolCall.arguments);
  const args = wrappedArgs?.command;
  console.log(`[ai-toolkit] ${demo.ref.type} - tool args from LLM:`, args);

  const result = await command.apply(args);
  console.log(`[ai-toolkit] ${demo.ref.type} - command result:`, result);
}
```

[Live example: Toolkit](https://www.ag-grid.com/studio/examples/ai-toolkit/ai-toolkit/typescript/)

## JSON Schema Support

AG Studio generates [JSON Schema Draft 2020-12](https://json-schema.org/draft/2020-12). Some LLM providers and runtimes do not support every feature of this version, so you may need to adapt the schema:

- **Optional parameters** - some LLMs require every parameter in the `required` array. Model an optional field as a union with `null` (or a sentinel), then decode the result.
- **Nesting depth** - some LLMs limit schema nesting depth. Studio uses `$defs` and `$ref` to keep schemas shallow, but you may still need to break a schema up further.
- **Root schema** - some commands produce a union at the schema root, while some providers accept only an object there. Wrap the schema and unwrap the returned parameters before applying.

## Next Steps

- [Context](https://www.ag-grid.com/studio/javascript/ai-context/) - Describe the data and dashboard to your model.
- [LLM Adapter](https://www.ag-grid.com/studio/javascript/ai-adapter/) - The alternative: keep the built-in runtime and connect a provider.
