---
title: "Toolkit"
framework: react
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/react/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/react/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

```tsx
("use client");

import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgStudio, AgStudioRef } from "ag-studio-react";
import { openaiAdapter } from "./shared/openaiAdapter.tsx";
import {
  AgAiConversationItem,
  AgAiRequest,
  AgAiToolCall,
  AgBuiltInAiCommandRef,
  AgDataEngine,
  AgDataSourcesDefinition,
  AgReportState,
  AgStudioApi,
  AgStudioMode,
  AgStudioProperties,
  AgToolSchema,
} from "ag-studio";
import { salesData } from "./data.tsx";

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

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

export const AI_API_TOKEN = "";

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 });

const runDemo: (demo: CommandDemo) => Promise<void> = async (
  demo: CommandDemo,
) => {
  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);
};

const StudioExample = () => {
  const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
  const studioStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
  const [data, setData] = useState<AgDataSource>(salesData);
  const initialState = useMemo<AgReportState>(() => {
    return {
      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",
    };
  }, []);

  return (
    <div style={containerStyle}>
      <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
        <div className="example-controls">
          <div className="controls-row" id="toolkitBar"></div>
        </div>

        <AgStudio
          style={studioStyle}
          className="my-studio-container"
          data={data}
          initialState={initialState}
          mode={"edit"}
        />
      </div>
    </div>
  );
};

const root = createRoot(document.getElementById("root")!);
root.render(
  <StrictMode>
    <StudioExample />
  </StrictMode>,
);
```

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

## 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/react/ai-context/) - Describe the data and dashboard to your model.
- [LLM Adapter](https://www.ag-grid.com/studio/react/ai-adapter/) - The alternative: keep the built-in runtime and connect a provider.
