Skip to main content

ObjectScript SDK

The AI Hub ObjectScript SDK lets you connect your InterSystems IRIS instance to your AI provider's LLMs. You can then equip these models with tools (ObjectScript methods) and let them interact with your system in a dynamic, controlled way; tool calls are carefully controlled and audited by user-defined policies, giving strict safety guarantees about how and in what context LLMs are allowed to reach out into the outside world.

The InterSystems IRIS AI Hub is a new capability of InterSystems IRIS, and an Early Access Program is available to assist early adopters. After signing up to the EAP, you'll get advance notice of updates to the software and a channel to report feedback and issues directly to the product team.

SDK Overview

At a high level, developing agentic applications with the AI Hub ObjectScript SDK involves the following workflow. To follow along, refer to the Quickstart:

  1. Create a connection to an LLM provider (e.g. Anthropic) and instantiate one of its models (e.g. Sonnet 4.5) as an agent in your system. This basically creates a chat bot.

  2. To let your agent interact with your system, give it a tool. A tool in this context can be any ObjectScript method. For example, if you want your agent to summarize documents, you can give it a method that reads a file; your agent will call the tool automatically.

You can extend this basic workflow by restricting or modifying tool calls (authorization policies), logging tool execution (audit policies), delegating parts of a task to other agents (sub-agents), and even creating tools at run-time (discovery policies).

Quickstart

Create an %AI.Provider using the configuration options required by your LLM provider. These requirements vary between providers; providers like OpenAI and Anthropic only require an API key, while Amazon requires some combination of a region and credentials set as environment variables. For details on each provider's requirements and examples on how to configure them, see Providers.

For demonstration purposes, this example uses OpenAI, which only requires an API key. The API key is retrieved from an environment variable, but you can also pass the API key in directly as a string for testing purposes:

set config = ##class(%DynamicObject).%New()
set key = $SYSTEM.Util.GetEnviron("OPENAI_API_KEY")
do config.%Set("api_key", key)
set provider = ##class(%AI.Provider).Create("openai", config)
Note:

To set your API key as an environment variable in Linux and macOS:

export OPENAI_API_KEY="sk-..."

In Windows, $SYSTEM.Util.GetEnviron() can only retrieve system-level environment variables. To set system-level variables, open an elevated PowerShell session and run the following:

[Environment]::SetEnvironmentVariable('OPENAI_API_KEY', 'sk-...', 'Machine')

You can then use the provider to create an %AI.Agent and then change settings like its model, prompt, and temperature, among others:

Set agent = ##class(%AI.Agent).%New(provider)
Set agent.Model = "gpt-4"
Set agent.SystemPrompt = "You are a helpful assistant."

To interact with the agent, create a chat session and ask it a question:

set session = agent.CreateSession()
set response = agent.Chat(session, "Is zero considered an even number?")
w response.Content

Yes, zero is considered an even number. In mathematics, an even number is any integer that can be divided by 2 without leaving a remainder. Since zero divided by 2 equals zero with no remainder, it fits this definition.

To create a tool, create a subclass of %AI.Tool. This example tool returns the InterSystems IRIS server time:

Class Sample.GetTime Extends %AI.Tool
{

Property Name As %String [ InitialExpression = "GetTime" ];

Property Description As %String(MAXLEN = "") [ InitialExpression = "Gets the current system time." ];

/// Get the current server time.
ClassMethod GetTime() As %String
{
    Return $ZTIME($PIECE($HOROLOG,",",2),1)
}
}

Verify that you're in the same namespace in which your tool is defined and register the tool with your agent using %AI.Agent.UseToolSet():

SET sc = agent.UseToolSet("Sample.GetServerTime")
SET response = agent.Chat(session, "What time is it?")

WRITE response.Content
The current server time is 17:30:14.

Providers

An instance of %AI.Provider represents a connection to an LLM provider. The ObjectScript SDK currently supports the following providers; support for providers whose names are marked with an asterisk (*) is experimental:

  • OpenAI

  • Anthropic

  • Google Gemini

  • AWS Bedrock

  • Google Vertex*

  • Meta Llama*

  • NIM*

  • xAI*

  • DeepSeek*

  • Kimi Moonshot AI*

  • OpenRouter*

  • Ollama*

Note:

This list is unrelated to the MCP clients supported by the AI Hub's iris-mcp-server; any client that supports the Model Context Protocol is supported.

To create a %AI.Provider instance, you need to specify the provider's identifier and the relevant configuration options (for example, an API key), which varies by provider. The following sections list the provider identifier for and configuration options required by each provider and an example of how to configure them.

In cases where configuration options can be set in an environment variable, the option description will express this with env: environment_variable.

Creating a provider uses the pattern, where:

  • id — String, the provider ID.

  • config — A %DynamicObject, a set of key-value pairs that represent the configuration options for the specified provider. This typically includes an API key, but can also include other things such as a base URL if your API endpoint differs from the default (usually from self-hosting) or if the provider does not have a default endpoint.

Set provider = ##class(%AI.Provider).Create(id, config)

To validate a configuration, use %AI.Provider.ValidateConfig():

// Returns a Boolean; 1 is valid, 0 invalid
WRITE provider.ValidateConfig()
1

OpenAI

  • Provider ID: "openai"

  • Configuration options:

    • api_key — The API Key.

    • base_url (Optional) — The API base URL; set this if you self-host (default: https://api.openai.com/v1/).

    • org_id (Optional) — Organization ID.

Set provider = ##class(%AI.Provider).Create("openai", {
    "api_key": "..."
})

Anthropic

  • Provider ID: "anthropic"

  • Configuration options:

    • api_key — The API Key.

    • base_url (Optional) — The API base URL; set this if you self-host (default: https://api.anthropic.com/v1/).

    • version (Optional) — The Anthropic API version.

Set provider = ##class(%AI.Provider).Create("anthropic", {
    "api_key": "..."
})

Google Gemini

  • Provider ID: "gemini"

  • Configuration options:

    • api_key (env: GOOGLE_API_KEY) — The API Key. You can also set this with the GOOGLE_API_KEY environment variable.

Set provider = ##class(%AI.Provider).Create("gemini", {
    "api_key": "..."
})

Google Vertex

  • Provider ID: "gemini"

  • Configuration options:

    • project_id (env: GOOGLE_CLOUD_PROJECT) — GCP Project ID.

    • region (env: GOOGLE_CLOUD_LOCATION) (Optional) — Vertex AI region (default: "global"). Use "global" for the global endpoint or a specific region like "us-east5".

    • service_account_path (env: GOOGLE_APPLICATION_CREDENTIALS) (Optional) — Path to service account JSON file.

    • service_account_json (Optional) — Service account JSON content (alternative to service_account_path).

Set provider = ##class(%AI.Provider).Create("vertex", {
    "project_id": "your-project-id"
    "region": "us-east5",
    "service_account_path": "/path/to/service_account.json"
})

Amazon Bedrock

  • Provider ID: "bedrock"

  • Configuration options:

    • region (Required for AWS Signature V4 (SigV4)Opens in a new tab, env: AWS_REGION) — AWS region (e.g. us-east-1). If you use AWS Signature V4 (SigV4), then

    • api_key or bearer_token (Required if you don't use SigV4, env: AWS_BEARER_TOKEN_BEDROCK) — Long-lived Bedrock API key for bearer token authentication. If set, bypasses SigV4 signing. Also accepted as bearer_token.

To use SigV4, provide your credentials with the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables and then specify the region:

Set provider = ##class(%AI.Provider).Create("bedrock", {
    "region": "us-east-1"
})

Otherwise, provide the region and the bearer_token:

Set provider = ##class(%AI.Provider).Create("bedrock", {
    "region": "us-east-1",
    "bearer_token": "..."
})

You can also provide the bearer_token in the AWS_BEARER_TOKEN_BEDROCK environment variable. In this case, you only need to provide the region:

Set provider = ##class(%AI.Provider).Create("bedrock", {
    "region": "us-east-1"
})

Meta Llama

  • Provider ID: "meta"

  • Configuration options:

    • api_key (env: LLAMA_API_KEY) — The API Key.

Set provider = ##class(%AI.Provider).Create("meta", {
    "api_key": "..."
})

xAI

  • Provider ID: "xai" (alias: "grok")

  • Configuration options:

    • api_key (env: XAI_API_KEY) — The API Key.

Set provider = ##class(%AI.Provider).Create("xai", {
    "api_key": "..."
})

NVIDIA NIM

  • Provider ID: "nim"

  • Configuration options:

    • base_url — The API base URL.

    • api_key (Optional) — The API Key.

Set provider = ##class(%AI.Provider).Create("nim", {
    "base_url": "http://localhost:8000/v1"
})

DeepSeek

  • Provider ID: "deepseek"

  • Configuration options:

    • api_key (env: DEEPSEEK_API_KEY) — The API Key.

    • base_url (Optional) — The API base URL (default: https://api.deepseek.com/v1).

Set provider = ##class(%AI.Provider).Create("deepseek", {
    "api_key": "..."
})

Kimi (Moonshot AI)

  • Provider ID: "kimi" or its alias "moonshot"

  • Configuration options:

    • api_key (env: MOONSHOT_API_KEY) — The API Key.

    • base_url (Optional) — The API base URL (default: https://api.moonshot.ai/v1/).

Set provider = ##class(%AI.Provider).Create("kimi", {
    "api_key": "...",
})

OpenRouter

  • Provider ID: "openrouter"

  • Configuration options:

    • api_key (env: OPENROUTER_API_KEY) — OpenRouter API key.

    • site_url (optional, env: OPENROUTER_SITE_URL) — Application URL sent as HTTP-Referer (shown in OpenRouter dashboard).

    • site_name (optional, env: OPENROUTER_SITE_NAME) — Application name sent as X-Title (shown in OpenRouter dashboard).

    • base_url (Optional) — The API base URL (default: https://openrouter.ai/api/v1).

Set provider = ##class(%AI.Provider).Create("openrouter", {
    "api_key": "...",
})

Agents

An agent (%AI.Agent) is the execution engine for all interactions between your program and the LLM. After creating a chat session %AI.Agent.Session, your agent can use tools (defined as ObjectScript methods) to interact with your InterSystems IRIS instance and any of its connected resources; any method you can define in InterSystems IRIS, you can hand off to the agent.

To create an agent, you must first create a %AI.Provider, which requires an API key. This section of the documentation assumes that you have created both the provider and agent. For example:

// Create provider using the API key in the OPENAI_API_KEY environment variable
set config = ##class(%DynamicObject).%New()
set key = $SYSTEM.Util.GetEnviron("OPENAI_API_KEY")
do config.%Set("api_key", key)
set provider = ##class(%AI.Provider).Create("openai", config)

// Create agent
Set agent = ##class(%AI.Agent).%New(provider)

Most of the examples below cover imperative agent and model configuration (that is, manually setting properties of instances), but information about these properties still apply to declaratively created agents.

Agent Properties

After you create an agent, you can set its properties. The following is a list of the most important properties that you can optionally set during agent creation; other properties are set automatically in other contexts:

  • Model — String, the model to use. If unspecified, the agent uses the provider's default model. For a list of available models, use %AI.Provider.ListModels.

  • SystemPrompt — String, the agent's prompt.

  • Temperature — Float, the creativity of the LLM. Greater values mean more creative responses. This parameter is the same as your model's temperature session property. If unset, this uses the provider's default.

    If you want to set the property, set it based on your use-case:

    • 0.0-0.3 — Factual responses and reading data

    • 0.4-0.7 — General purpose

    • 0.8-1.2 — Creative activities (writing, brainstorming, etc.)

    • 1.3-2.0 — Experimental; responses can be incoherent

  • MaxIterations — Integer, the maximum number of "turns" (that is, iterations of %AI.Agent.Run), where each turn consists of sending one prompt to and receiving one response from the provider's model (default: 10).

    Note:

    This is not the same as the max_iterations session property, which controls the number of tool calls per turn.

  • AutoCompactOnTokenLimit — Boolean, whether to summarize the conversation if you exceed the token limit. If false, the agent raises a $$AIErrorTokenLimitExceeded error instead (default: 0).

The following examples show how to set the various properties on some agent instance:

Set agent.Model = "gpt-4"
Set agent.SystemPrompt = "You are a helpful assistant."
Set agent.Temperature = 0.7
Set agent.MaxIterations = 15

Declarative Agent Configuration

Rather than instantiating agents and setting their properties manually, you can create agents in a declarative way by creating a subclass of %AI.Agent; the main difference is that, to properly instantiate the agent, you need to call %Init() manually after %New().

The following example:

  • Specifies the PROVIDER directly as part of the agent.

    This is equivalent to instantiating and passing in the %AI.Provider to the constructor of %AI.Agent.

  • Uses a macro to specify the API key with an environment variable.

  • Specifies the tools available to the agent as a comma-delimited string of %AI.Tool or %AI.ToolSet class names. If you want to create an agent without any tools, you can comment out this property.

    Class Sample.FileSystemTools Extends %AI.ToolSet
    {
    
    XData Definition [ MimeType = application/xml ]
    {
    <ToolSet Name="FileSystemTools">
        <Tool Name="GetFileText" Method="GetFileText"/>
    </ToolSet>
    }
    
    /// Read from a file
    ClassMethod GetFileText(filePath As %String) As %String
    {
        Set text = ""
        
        Try {
            Set file = ##class(%File).%New(filePath)
            
            If 'file.Open("R") {
                $$$ThrowStatus($$$ERROR($$$GeneralError, "Could not open file: " _ filePath))
            }
            
            While 'file.AtEnd {
                Set text = text _ file.ReadLine() _ $Char(10)
            }
            
            Do file.Close()
            
        } Catch ex {
            Return "ERROR: " _ ex.DisplayString()
        }
        
        Return text
    }
    
    }

    This is equivalent to adding the tool to the agent manually with %AI.Agent.UseToolSet.

  • Specifies a system prompt for the model in the XData INSTRUCTIONS block.

    This is equivalent to prompting the agent by manually setting the %AI.Agent.SystemPrompt property.

  1. Create a subclass of %AI.Agent:

    Class Sample.DeclarativeAgent Extends %AI.Agent
    {
      /// LLM provider
      Parameter PROVIDER = "openai";
    
      /// Model to use
      Parameter MODEL = "gpt-4";
    
      /// API key is retrieved from the OPENAI_API_KEY environment variable at run-time
      Parameter APIKEY = "@{env:OPENAI_API_KEY}";
    
      /// Comma-delimited string of ToolSets
      Parameter TOOLSETS = "Sample.FileSystemTools";
    
      /// System Instructions (Markdown)
      XData INSTRUCTIONS [ MimeType = text/markdown ]
      {
    # File System Assistant
    
    You are a helpful AI assistant specialized in file system operations.
    
    ## Available Tools
    - File System Operations
      }
    
      /// Custom initialization hook (optional)
      Method %OnInit() As %Status
      {
        // Configure additional properties if needed
        Return $$$OK
      }
    }
  2. Create and instance of the agent:

    SET agent = ##class(Sample.DeclarativeAgent).%New()
    
  3. Initialize the agent:

    SET sc = agent.%Init()
    
  4. Interact with the agent like you would with any other %AI.Agent instance. For example, to create a chat session:

    SET session = agent.CreateSession()
    SET response = agent.Chat(session, "Read the contents of /lorem.txt")
    WRITE response.Content
    
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    

Sessions

All interaction with a given agent is in the context of a session (%AI.Agent.Session), which maintains conversation state. To create a session, you must first create a provider and agent:

// Create provider
set config = ##class(%DynamicObject).%New()
set key = $SYSTEM.Util.GetEnviron("OPENAI_API_KEY")
do config.%Set("api_key", key)
set provider = ##class(%AI.Provider).Create("openai", config)

// Create agent
Set agent = ##class(%AI.Agent).%New(provider)
Set agent.Model = "gpt-4"
Set agent.SystemPrompt = "You are a helpful assistant."

// Create session from agent
Set session = agent.CreateSession()

You can also create a session directly from a provider, but you still need an agent to use the various Chat() methods:

Set session = ##class(%AI.Agent.Session).Create(
    provider,                   // %AI.Provider instance
    "gpt-4",                    // model
    "You are helpful.",         // system prompt
    toolsJson,                  // (optional) tool schemas from agent.ToolManager.%Discover()
    config                      // (optional) config object with session properties
)

You can then interact with your chosen model. The session acts as the context for your chat and lets the agent remember and build upon earlier parts of the discussion in its %AI.LLM.Response:

set response = agent.Chat(session, "Is zero even?")
w response.Content

Yes, zero is considered an even number.

set response = agent.Chat(session, "What about positive or negative?")
w response.Content

Zero is neither positive nor negative.

Session Properties

You can provide various properties in a JSON object when you create a session to control how the model behaves:

  • max_iterations — The maximum number of tool calls an agent can make per iteration of %AI.Agent.Run().

  • temperature — A value in the range [0.0, 2.0]. This is the same property as the agent-level temperature property. If unset, it uses the provider's default.

  • max_tokens — The response length.

  • top_p — Controls how varied the responses are; the higher the value, the more varied the responses. As a general guideline, InterSystems recommends a value in the range [0.1-0.7].

  • presence_penalty — A value in the range [-2.0, 2.0], how much to encourage new topics; positive values encourage new topics, while negative values penalize them.

  • frequency_penalty — A value in the range [-2.0, 2.0], how much to discourage repetition; positive values discourage repetition, while negative values encourage it.

  • stop_sequences — A list of strings that cause the agent to stop responding.

  • cache — Controls whether and what parts the model should cache:

    • enabled — Whether to enable caching.

    • cache_system_prompt — Whether to cache the system prompt.

    • cache_tool_definitions — Whether to cache tool definitions.

The following example shows how to create a session with the various model properties:


Set config = {
    "max_iterations": 10,
    "temperature": 0.7,
    "max_tokens": 1000,
    "top_p": 0.9,
    "presence_penalty": 0.1,
    "frequency_penalty": 0.1,
    "stop_sequences": ["END"],
    "cache": {
        "enabled": (1),
        "cache_system_prompt": (1),
        "cache_tool_definitions": (1)
    }
}

Set sessionWithProps = agent.CreateSession(config)

Session Management

%AI.Agent.Session provides various methods for inspecting and managing its state.

Inspecting Sessions

To get general information about the session, use %AI.Agent.Session.GetStats():

Set stats = session.GetStats()
Set out = "Interactions: "_stats."total_interactions"_" | Prompt tokens: "_stats."total_prompt_tokens"_" | Completion tokens: "_stats."total_completion_tokens"_" | Tool calls: "_stats."total_tool_calls"_" | LLM time: "_stats."total_llm_duration_ms"_"ms"
Write out, !

Interactions: 1 | Prompt tokens: 23 | Completion tokens: 42 | Tool calls: 12 | LLM time: 1795ms

You can use this method to monitor performance:

// Track session performance
Set stats = session.GetStats()

// Calculate tokens per second
Set totalTokens = stats."total_prompt_tokens" + stats."total_completion_tokens"
Set totalSeconds = stats."total_llm_duration_ms" / 1000
Set tokensPerSec = totalTokens / totalSeconds
Write "Throughput: ", $FNUMBER(tokensPerSec, "", 1), " tokens/sec", !

Throughput: 36.2 tokens/sec

// Context window usage
Set pctUsed = (stats."current_context_tokens" / stats."model_context_size") * 100
Write "Context: ", $FNUMBER(pctUsed, "", 1), "% used", !

Context: 1.8% used

To inspect your conversation context:

Set messages = session.GetContext()
Set iter = messages.%GetIterator()

While iter.%GetNext(.i, .msg) { Write msg.role, ": ", $EXTRACT(msg.content, 1, 80), "...", ! }

user: Is zero considered an even number?...
assistant: Yes, zero is considered an even number because it can be divided evenly by 2. In...
user: What about infinity?...
assistant: Infinity is not considered an even number because it's not a number at all in th...
Saving and Restoring Session State

You can save conversation state at a checkpoint and then restore it later. This can be useful for branching conversations or recovering from errors.

To save your session, use %AI.Agent.Session.AddCheckpoint, specifying the name of the checkpoint and an optional note:

// Save a checkpoint after the user confirms their request
DO session.AddCheckpoint("First Checkpoint", "My first checkpoint")
DO session.AddCheckpoint("Second Checkpoint")

Then, if you want to rewind back to that checkpoint, use %AI.Agent.Session.RewindTo(), specifying the name of the checkpoint:

DO session.RewindTo("mycheckpoint")

To get a list of your current checkpoints, use %AI.Agent.Session.ListCheckpoints(). This returns information about your checkpoints in a %DynamicArray:

SET checkpoints = session.ListCheckpoints()
ZW checkpoints

checkpoints=[
    {"name":"First Checkpoint","message_index":0,"note":"My first checkpoint","created_at":"2026-06-10T17:07:28.789125261+00:00"},
    {"name":"Second Checkpoint","message_index":0,"created_at":"2026-06-10T17:15:29.658711743+00:00"}
]

To remove a checkpoint, use %AI.Agent.Session.RemoveCheckpoint(), specifying the name of the checkpoint:

DO session.RemoveCheckpoint("mycheckpoint")

%AI.Agent.Session.Fork() creates a deep copy of the session. This does not modify the original session; forked sessions are independent. This can be helpful, for example, for exploring different approaches to the same problem.

By default, forked sessions do not inherit the statistics of the original session. To override this behavior, use Fork(1):

Set main = session
Set branch  = session.Fork()
Set branchWithOriginalStats = session.Fork(1)

// Run different paths
Set r1 = agent.Chat(main,   "Try approach A")
Set r2 = agent.Chat(branch, "Try approach B")
Set r3 = agent.Chat(branch, "Try approach C")


// keepStats:1 copies current stats into the fork (default is fresh stats)
Set fork2 = session.Fork(1)
Resetting Session State

To selectively clear parts of your session state:

session.Reset()          // Clear everything (context, stats, checkpoints, summary)
session.ResetContext()   // Clear context and checkpoints; preserve stats
session.ResetStats()     // Reset stats only; context and checkpoints are preserved

Sending Multi-Modal Content to Agents

To send multi-modal content like images to an agent, construct the content with the %AI.LLM.ContentPart API and then pass it to your agent with %AI.Agent.ChatWithContent. %AI.LLM.ContentPart expects the content of the image to be Base64-encoded:

SET tinyPng = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk" _ "+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="

SET content = ##class(%AI.LLM.ContentPart).Build(
    ##class(%AI.LLM.ContentPart).Text("Describe what you see in this image in one sentence."),
    ##class(%AI.LLM.ContentPart).ImageData(tinyPng, "image/png")
)

WRITE "Sending inline base64 image...", !, !
SET response = agent.ChatWithContent(session, content)
WRITE response.Content

The image appears to be a small, solid light-green square with no discernible objects or details.

The following method generalizes this process; it takes a path to an image and a prompt and returns a %AI.LLM.ContentPart:

ClassMethod CreateContentPart(imagePath As %String, prompt As %String) As %AI.LLM.ContentPart
{
    // Read and encode image in Base64
    Set stream = ##class(%Stream.FileBinary).%New()
    Do stream.LinkToFile(imagePath)
    Set bytes = stream.Read(stream.Size)
    Set base64 = $SYSTEM.Encryption.Base64Encode(bytes)
    Set extension = $piece(imagePath, ".", *)

    // Construct the ContentPart
    SET contentPart = ##class(%AI.LLM.ContentPart).Build(
        ##class(%AI.LLM.ContentPart).Text(prompt),
        ##class(%AI.LLM.ContentPart).ImageData(base64, "image/" _ extension)
    )

    Return contentPart
}

Usage:

set contentPart = ##class(Sample.MultiModal).CreateContentPart("/img/example.png", "Describe the image.")
Set response = agent.ChatWithContent(session, contentPart)
Write response.Content

The image shows a close-up of a beaver with wet brown fur sitting near the water or on a log, set against a softly blurred green-and-gold natural background.

You can also retrieve images from a remote source and pass them to the agent:

Set content = ##class(%AI.LLM.ContentPart).Build(
##class(%AI.LLM.ContentPart).Text("What do you see in this image?"),
##class(%AI.LLM.ContentPart).ImageURL("https://example.com/image.jpg"))

Tools

A tool is an instance of %AI.Tool whose methods an agent can call to help form its response. InterSystems IRIS provides some utility tools for interacting with SQL (%AI.Tools.SQL), the file system (%AI.Tools.FileSystem), and the shell (%AI.Tools.ShellTools), but you can also define your own tools.

Note:

Models can only see the return values of tools and cannot, for example, inspect the contents of an Output argumentOpens in a new tab. You should write new tools with this in mind, but if you have existing business logic code that use Output arguments, you can simply wrap them in the tool body and extract the result before returning. For details, see Reusing Existing Classes and Methods as Tools.

The simplest way to create tools is by extending %AI.Tool and defining methods (which will be called as tools) in the class body. This example defines a simple tool for counting the number of files in a directory. The Name and Description properties describe how to the LLM how the tool works:

Class Sample.CountFiles Extends %AI.Tool
{

Property Name As %String [ InitialExpression = "CountFiles" ];

Property Description As %String(MAXLEN = "") [ InitialExpression = "Returns the number of files in a directory." ];

ClassMethod CountFiles(path As %String) As %Integer
{
    Set dir = ##class(%File).NormalizeDirectory(path)
    Set res = ##class(%ResultSet).%New("%File:FileSet")
    Do res.Execute(dir, "*", , 0)
    Set count = 0
    While res.Next() { Set count = count + 1 }
    Return count
}
}
Note:

Typed tool parameters become JSON Schema properties, and any parameters without a default value are required. For details on how these types map to ObjectScript types, see Tool Parameter Types.

You can then instruct the agent to use these tools by specifying the class name with %AI.Agent.UseToolSet(). The agent will automatically use the tools when they're relevant to your request:

SET sc = agent.UseToolSet("Sample.CountFiles")
SET session = agent.CreateSession()
SET response = agent.Chat(session, "How many files are in /home/testdir?")

WRITE response.Content
There are 3 files in the directory /home/testdir.

Tool Descriptions

It can be helpful for your model if you add descriptions to your tools and their parameters. While agents can already infer the usage a tool based on its method and parameter names, a detailed description can help agents use your tools in more intelligent ways.

There are several kinds of tool descriptions and they can be used together in the same method:

  • Comments — A natural-language description of how the tool works and what its parameters mean. For example:

    /// Calculate the result of a simple arithmetic expression.
    /// a is the left operand, op is the operator (+ - * /), b is the right operand.
    ClassMethod Calculate(a As %Numeric, op As %String, b As %Numeric) As %String { ... }
    
  • Description class properties — A way to describe the %AI.Tool or %AI.ToolSet at the class level. For example:

    Property Description As %String(MAXLEN = "") [ InitialExpression = "Returns the number of files in a directory." ];
    
  • DESCRIPTION parameters — A more formal way to describe parameters. This should be used together with a standard comment description; you cannot write a description of the tool itself using the DESCRIPTION parameter tag, so the following example uses both:

    /// Calculate the result of a simple arithmetic expression.
    ClassMethod Calculate(
        a As %Numeric(DESCRIPTION = "Left operand"),
        op As %String(DESCRIPTION = "Operator: + - * /"),
        b As %Numeric(DESCRIPTION = "Right operand")
    ) As %String { ... }
    

Toolsets

A toolset (%AI.ToolSet) bundles %AI.Tools and other %AI.ToolSets together into a convenient package. You can also apply policies to toolsets to control and monitor how its constituent tools are used. For details, see Policies.

For example, suppose you have a tool for getting the server time and date:

Class Sample.GetServerTime Extends %AI.Tool
{

Property Name As %String [ InitialExpression = "GetServerTime" ];

Property Description As %String(MAXLEN = "") [ InitialExpression = "Gets the current server time." ];

ClassMethod GetServerTime() As %String
{
    Return $ZTIME($PIECE($HOROLOG,",",2),1)
}
}
Class Sample.CountFiles Extends %AI.Tool
{

Property Name As %String [ InitialExpression = "CountFiles" ];

Property Description As %String(MAXLEN = "") [ InitialExpression = "Returns the number of files in a directory." ];

ClassMethod CountFiles(path As %String) As %Integer
{
    Set dir = ##class(%File).NormalizeDirectory(path)
    Set res = ##class(%ResultSet).%New("%File:FileSet")
    Do res.Execute(dir, "*", , 0)
    Set count = 0
    While res.Next() { Set count = count + 1 }
    Return count
}
}

You can compose these into a single toolset by extending %AI.ToolSet and including the class names of any combination of %AI.Tools and %AI.ToolSets in your XData Definition, which defines which tools are visible to the model:

Class Sample.SampleToolset Extends %AI.ToolSet
{

XData Definition [ MimeType = application/xml ]
{
<ToolSet Name="SampleToolset">
    <Description>General utilities</Description>
    
    <!-- Include separately defined %AI.Tool classes -->
    <Include Class="Sample.GetServerTime" />
    <Include Class="Sample.CountFiles" />
</ToolSet>
}
}

You can then register these toolsets with %AI.Agent.UseToolSet just like you would a regular tool:

SET sc = agent.UseToolSet("Sample.SampleToolset")

Not all tools available in a given toolset need to come from those included in the XData Definition; you can also define methods inside the %AI.ToolSet itself and then expose them to your model in the XData Definition using a <Tool> block.

In this example, Sample.SampleToolset defines a GetDate method and exposes it as GetServerDate to the model (though you can make the method name and exposed tool name match if you want):

Class Sample.SampleToolset Extends %AI.ToolSet
{

XData Definition [ MimeType = application/xml ]
{
<ToolSet Name="SampleToolset">
    <Description>General utilities</Description>
    
    <!-- Include separately defined %AI.Tool classes -->
    <Include Class="Sample.GetServerTime" />
    <Include Class="Sample.CountFiles" />

    <!-- Include a method defined inside this %AI.ToolSet -->
    <Tool Name="GetServerDate" Method="GetDate"/>
</ToolSet>
}

ClassMethod GetDate() As %String
{
    Return $ZDATE($PIECE($HOROLOG,",",1),1)
}
}

If your method's usage or behavior isn't be obvious from its name, you can add a <Description>:

<Tool Name="GetServerDate" Method="GetDate">
	<Description>Returns the server date of the InterSystems IRIS instance.</Description>
</Tool>

Tool Composition and Filtering

By default, including a toolset means exposing all of its tools to the model. For example, if you <Include> a toolset A inside toolset B, all the tools exposed by toolset A will be visible to models that use toolset B. If you don't want to include all of the tools from toolset A, you can use a <Filter>.

Filters are defined in the XData Definition of the %AI.ToolSet and are applied during compile-time, so constructing tools on filters has no performance penalty.

To include a tool only if its name exactly matches a specified string, use <Include Class=class Tool=string>. In this example, the toolset ReadOnlyOrders only includes the GetOrder tool.

<ToolSet Name="ReadOnlyOrders">
    <!-- Only expose GetOrder, not CreateOrder, CancelOrder, etc. -->
    <Include Class="MyApp.OrderTools" Tool="GetOrder"/>
</ToolSet>

To match on a regular expression, use <Include Class=classname Tool=pattern>. A partial match is sufficient for including the tool.

In this example, the ^Get pattern includes all methods inside MyApp.CustomerTools that start with Get:

<ToolSet Name="GettersOnly">
    <!-- Include any tool whose name starts with "Get" -->
    <Include Class="MyApp.CustomerTools" Match="^Get"/>
</ToolSet>

In this example, the ^(Search|List) pattern includes tools that start with either Search or List:

<ToolSet Name="SearchAndList">
    <!-- Include tools starting with "Search" OR "List" -->
    <Include Class="MyApp.ProductTools" Match="^(Search|List)"/>
</ToolSet>

When you need to match tools by more than one name pattern, add <Filter> children to your <Include> block. A tool will be included if it matches at least one filter. In this example, the SelectedTools toolset includes, from the MyApp.InventoryTools toolset, the methods MGetStockLevel(), GetReorderPoint(), and any methods that begin with List:

<ToolSet Name="SelectedTools">
    <Include Class="MyApp.InventoryTools">
        <Filter Tool="GetStockLevel"/>
        <Filter Tool="GetReorderPoint"/>
        <Filter Match="^List"/>
    </Include>
</ToolSet>

You can also exclude tools from a composed with with <Exclude>. <Exclude> filters are applied after <Include> filters and use the same semantics (exact match, regular expression, child <Filter> blocks).

This example first includes all tools from MyApp.DatabaseTools and then removes all Delete, Drop, and Truncate methods:

<ToolSet Name="SafeTools">
    <Include Class="MyApp.DatabaseTools"/>
    <!-- Remove destructive operations -->
    <Exclude Match="^(Delete|Drop|Truncate)"/>
</ToolSet>

This example includes all tools from MyApp.AdminTools and then applies two exact filters to exclude ResetAllUsers() and WipeDatabase() and a regular expression filter to remove Debug methods:

<ToolSet Name="LimitedTools">
    <Include Class="MyApp.AdminTools"/>
    <Exclude>
        <Filter Tool="ResetAllUsers"/>
        <Filter Tool="WipeDatabase"/>
        <Filter Match="^Debug"/>
    </Exclude>
</ToolSet>

This example includes all tools from both MyApp.OrderTools and MyApp.ProductTools and then removes DeleteOrder() from MyApp.OrderTools. Notice that this would not remove MyApp.ProductTools.DeleteOrder():

<ToolSet Name="CompositeTools">
    <Include Class="MyApp.OrderTools"/>
    <Include Class="MyApp.ProductTools"/>
    <!-- Remove Delete only from OrderTools, not from ProductTools -->
    <Exclude Class="MyApp.OrderTools" Tool="DeleteOrder"/>
</ToolSet>
Requirement Blocks

When you <Include> a tool, you can add a <Requirement> block inside it. The information expressed in the <Requirement> is accessible to authorization and audit policies through the metadata objects passed to their respective methods.

For example, suppose you add the following requirements when you include %AI.Tools.SQL in you toolset's XData Definition:

<Include Class="%AI.Tools.SQL">
    <Requirement Name="ReadOnly" Value="1"/>
    <Requirement Name="Role" Value="%All"/>
</Include>

This means that when a model attempts to use the %AI.Tools.SQL in a tool call, the metadata object would contain the following information:

{
    "ReadOnly" : "1",
    "Role" : "%All"
}

You can then use this data to enforce certain restrictions on the call or fine tune how you audit it. For example, this these requirements express that the model should have the %All role and that the call should be a read-only operation, so you can examine $ROLES to verify that the caller has %All and use the call object (which, like metadata is passed to %AI.Policy.Authorization.%CanExecute) to verify that the operation is considered read-only.

Reusing Existing Classes and Methods as Tools

Rather than creating tools from scratch, it's often most convenient to reuse existing classes and their methods. Since tools are just methods, reusing existing classes is as simple as calling the existing method or instantiating the existing class from the body of your tool. This also gives you a good opportunity to manipulate the format of the output if, for example, you want the output to be JSON:

Class Sample.DataTools Extends %AI.ToolSet
{
    /// Delegates to Patient.SearchByName and returns JSON
    Method SearchPatients(name As %String) As %String
    {
        Set results = ##class(MyApp.Patient).SearchByName(name)

        Set output = []
        While results.%Next() {
            Do output.%Push({
                "id": (results.ID),
                "name": (results.Name),
                "dob": (results.DOB)
            })
        }

        Return output.%ToJSON()
    }
}

Stateful Tools

If you need your tools to maintain state, define a Method (as opposed to a ClassMethod). The LLM automatically instantiates a single instance of the tool class and keeps it alive for the duration of the session.

The following tool models a simple notepad and stores notes in a Property:

Class Sample.Notepad Extends %AI.Tool
{

Property Name As %String [ InitialExpression = "Notepad" ];

Property Description As %String(MAXLEN = "") [ InitialExpression = "A notepad." ];

Property Notes As %String(MAXLEN = "");

/// Store a note for later retrieval in this session.
Method Remember(note As %String(DESCRIPTION="The note to store")) As %String
{
        Set ..Notes = ..Notes _ note _ $C(10)
        Return "Noted."
}

/// Recall all notes stored in this session.
Method Recall() As %String
{
        Return $SELECT(..Notes = "": "No notes stored yet.", 1: ..Notes)
}

/// Clear all stored notes.
Method Forget() As %String
{
        Set ..Notes = ""
        Return "Notes cleared."
}

}

Example chat:

SET response = agent.Chat(session, "Make a note to buy eggs.")
SET response = agent.Chat(session, "Note the weather (chilly).")
SET response = agent.Chat(session, "I forgot the milk, so add a note for that.")
SET response = agent.Chat(session, "What do my notes say?")

WRITE response.Content

Here are your notes:

1. Buy eggs
2. The weather is chilly
3. Buy milk

SQL Tools

SQL tools are tools that let your agent query SQL tables with SELECT.

For demonstration purposes, most of the following examples in this section use this simple table:

| ID |      Name      |       Email       |
| -- | -------------- | ----------------- |
| 1  | Alice Johnson  | [email protected] |
| 2  | Bob Smith      | [email protected]   |
| 3  | Carol White    | [email protected] |
| 4  | April May      | [email protected] |

To reproduce this table in your instance:

DO $SYSTEM.SQL.Execute("CREATE TABLE users.customers (ID INTEGER IDENTITY, Name VARCHAR(100), Email VARCHAR(255))")

DO $SYSTEM.SQL.Execute("INSERT INTO users.customers VALUES ('Alice Johnson', '[email protected]')")
DO $SYSTEM.SQL.Execute("INSERT INTO users.customers VALUES ('Bob Smith', '[email protected]')")
DO $SYSTEM.SQL.Execute("INSERT INTO users.customers VALUES ('Carol White', '[email protected]')")
DO $SYSTEM.SQL.Execute("INSERT INTO users.customers VALUES ('April May', '[email protected]')")

%AI.Tools.SQL

The AI Hub SDK provides the %AI.Tools.SQL tool for querying tables in your InterSystems IRIS instance.

The following example uses the predefined %AI.Tools.SQL tool:

Set agent = ##class(%AI.Agent).%New(provider)
Set agent.Model = "gpt-4"
Set agent.SystemPrompt = "You are a helpful assistant."

SET sc = agent.UseToolSet("%AI.Tools.SQL")

SET session = agent.CreateSession()
SET response = agent.Chat(session, "Query for the users in the system and give them to me in a list.")
WRITE response.Content

Here are the names of the users in the system:
1. _Ensemble
2. _PUBLIC
3. _SYSTEM
4. Admin
5. CSPSystem
6. IAM
7. irisowner
8. SuperUser
9. UnknownUser

%AI.Tools.SQL gives your model very broad to your database's tables. If you want this access to be more granular or structured, you can define your own query.

Class Queries

To use a class query, simply define a Query inside a %AI.Tool or %AI.ToolSet:

Class Sample.CustomerQuery Extends %AI.Tool
{
    /// Returns customer information from the users.customers table
    Query getCustomerInfoByName(Name As %String) As %SQLQuery [ SqlProc ]
    {
        SELECT *
        FROM users.customers
        WHERE Name = :Name
    }

}

Register it with your agent:

SET sc = agent.UseToolSet("Sample.CustomerQuery")
SET response = agent.Chat(session, "Give me information about the customer Alice Johnson")

WRITE response.Content
    
Here is the information I found for Alice Johnson:
- ID: 1
- Name: Alice Johnson
- Email: [email protected]

Inline Queries

%AI.ToolSet lets you define SQL queries directly in its XData Definition using a <Query> block and expose it as a tool. For details on %AI.ToolSet, see Toolsets.

The following example creates a query and exposes it as the tool getCustomerInfoName:

Class Sample.InlineSQL Extends %AI.ToolSet
{

XData Definition [ MimeType = application/xml ]
{
    <ToolSet Name="InlineSql">
        <Query Name="getCustomerInfoByName"
            Description="Returns customer information from the users.customers table"
            Arguments="name As %String"
            MaxRows="20">
        SELECT *
        FROM users.customers
        WHERE Name = :name
        </Query>
    </ToolSet>
}

}

Register it with your agent:

SET sc = agent.UseToolSet("Sample.InlineSql")
SET response = agent.Chat(session, "Give me information about the customer April May")

WRITE response.Content

Here is the information I found about April May:

- ID: 4
- Name: April May
- Email: [email protected]

Dynamic SQL

To create a dynamic SQL tool, create a method in %AI.Tool or %AI.ToolSet that constructs the query and returns the result.

Create a method that performs a dynamic SQL query:

Class Sample.CustomerQueryTool Extends %AI.Tool
{

Property Name As %String [ InitialExpression = "CustomerQueryTool" ];

Property Description As %String(MAXLEN = "") [ InitialExpression = "Queries the user.customers table" ];

// Queries the user.customers table for customers with the specified name
ClassMethod QueryCustomers(Name As %String) As %DynamicArray
{
    Set tCustomers = ##class(%DynamicArray).%New()
    
    Try {
        Set tStatement = ##class(%SQL.Statement).%New()
        Set tStatus = tStatement.%Prepare("SELECT ID, Name, Email FROM users.customers WHERE Name = ?")
        
        If $$$ISERR(tStatus) Quit
        
        Set tResult = tStatement.%Execute(Name)
        
        If tResult.%SQLCODE < 0 Quit
        
        While tResult.%Next() {
            Set tCustomer = ##class(%DynamicObject).%New()
            Set tCustomer.ID = tResult.%Get("ID")
            Set tCustomer.Name = tResult.%Get("Name")
            Set tCustomer.Email = tResult.%Get("Email")
            Do tCustomers.%Push(tCustomer)
        }
    }
    Catch ex {}
    
    return tCustomers
}

}

Register it with your agent:

SET sc = agent.UseToolSet("Sample.CustomerQueryTool")

SET session = agent.CreateSession()
SET response = agent.Chat(session, "Get me information about the customer Bob Smith")
WRITE response.Content

Here is the information I found about the customer named Bob Smith:

- ID: 2
- Name: Bob Smith
- Email: [email protected]

Smart Discovery

You can configure your agent's %AI.ToolMgr to automatically find and filter out tools with RAG-based (retrieval-augmented generation) smart discovery, which reduces the size of the prompt and gives your agent a more focused set of tools to choose from:

// Enable Smart Discovery
agent.ToolMgr.EnableSmartDiscovery()
Note:

Smart discovery replaces any manual discovery policy.

Tool Parameter Types

Tool parameter types are translated from their ObjectScript typesOpens in a new tab to a JSON Schema before being passed to your agent. While not strictly required, it's best practice to include ObjectScript type information in your tool definitions, especially for custom types, to give your agent as much information as possible about how to call your tool.

The following table shows mapping between ObjectScript types and their JSON Schema representations for primitive types:

Tool Parameter Type Mappings
ObjectScript Type JSON Schema
%String {"type": "string"}
%Integer {"type": "integer"}
%Float, %Numetric, %Double {"type": "number"}
%Boolean {"type": "boolean"}
%Date {"type": "string", "format": "date"}
%Time {"type": "string", "format": "time"}
%TimeStamp {"type": "string", "format": "date-time"}
%DynamicObject {"type": "object"}
%DynamicArray {"type": "array"}
%Stream.GlobalCharacter {"type": "string"}
%Stream.GlobalBinary {"type": "string", "contentEncoding": "base64"}
Circular type references {"type": "object}
List of Class {"type": "array", "items": { ...schema... }}
Array of Class {"type": "object", "additionalProperties": { ...schema... }}

The following example illustrates this mapping; when a parameter is typed to a concrete ObjectScript class (%Persistent, %SerialObject, or %RegisteredObject), the schema is built automatically from the type information provided by your class definition.

In this example, the FindNearby() method takes as argument an instance of MyApp.Address:

/// Find businesses within the radius (in miles) of the specified address
Method FindNearby(address As MyApp.Address, radiusMiles As %Float) As %DynamicArray { ... }

Suppose that MyApp.Address is defined as:

Class MyApp.Address Extends %RegisteredObject
{
    Property Street As %String;
    Property City As %String;
    Property ZipCode As %String;
}

The agent therefore interprets the parameter address As MyApp.Address with the following JSON Schema:

{
    "type": "object",
    "properties": {
      "Street": {"type": "string"},
      "City":   {"type": "string"},
      "ZipCode": {"type": "string"}
    }
}

%JSON.Adaptor

If a parameter's type extends %JSON.AdaptorOpens in a new tab, the schema respects its JSON configuration:

  • %JSONFIELDNAME — The JSON Schema uses the remapped field name (as opposed to the property name).

  • %JSONINCLUDE = "none" or "outputonly" — The property is excluded from the JSON Schema.

For example, MyApp.Product extends %JSON.Adaptor and indicates that ProductId should be included in the JSON schema as id and that InternalCostCode should be excluded entirely:

Class MyApp.Product Extends (%RegisteredObject, %JSON.Adaptor)
    {
        Property ProductId As %Integer(%JSONFIELDNAME = "id");
        Property Name As %String;
        Property InternalCostCode As %String(%JSONINCLUDE = "none"); // hidden from the LLM
    }

The model sees the following schema:

{"type": "object", "properties": {"id": {"type": "integer"}, "Name": {"type": "string"}}}

%DynamicArray

For %DynamicArray parameters, add the ELEMENTTYPE schema hint to the parameter to tell the framework what the array contains. If you exclude this hint, the JSON schema will be {"type": "array"}.

In this example, ELEMENTTYPE indicates that the contents of the items %DynamicArray are of type MyApp.OrderItem:

ClassMethod MyMethod(
    customerId As %Integer(DESCRIPTION = "Customer ID"),
    items As %DynamicArray(ELEMENTTYPE = "MyApp.OrderItem", DESCRIPTION = "Items to order")
) As %String { ... }

Tool Class Properties

%AI.Tool and %AI.ToolSet have class-level parameters that control how the tool is used and behaves:

  • REQUIRESAUTH — Boolean, whether the tool requires an authorization policy to approve the tool call. For example:

    Class MyApp.DatabaseAdmin Extends %AI.Tool
        {
            /// All tools in this class require authorization policy approval.
            Parameter REQUIRESAUTH As BOOLEAN = 1;
        
            /// Drop a database table. Requires explicit admin authorization.
            ClassMethod DropTable(tableName As %String) As %String { ... }
        
            /// Truncate all rows. Requires explicit admin authorization.
            ClassMethod TruncateTable(tableName As %String) As %String { ... }
        }
  • DISCOVERYLIMIT — String, the name of the class to which discovery should be limited. By default, tool discovery automatically considers the methods of all classes in a hierarchy as tools. For example, if you have a subclass Derived and a superclass Base, tool discovery will automatically consider the inherited methods of Base as valid tools for agents. To restrict tool discovery to Derived, you can add the this parameter. For example:

    Class MyApp.Derived Extends MyApp.Base
        {
            /// Only expose tools defined on MyApp.Derived and its subclasses.
            /// Methods from MyApp.Base and above are not included.
            Parameter DISCOVERYLIMIT = "MyApp.Derived";
        
            ClassMethod MyNewTool() As %String { ... }
        }
  • STATEFUL — Boolean, whether the class maintains state. By default, class methods are stateless and instance methods are stateful. Adding the STATEFUL class parameter overrides this automatic detection, letting you make class methods stateful or instance methods stateless. Note that stateful classes require a persistent session connection. For example:

    Class MyApp.GlobalCounter Extends %AI.Tool
        {
            /// This class uses globals, so all tools require session affinity.
            Parameter STATEFUL As BOOLEAN = 1;
        
            ClassMethod Increment(by As %Integer = 1) As %Integer
            {
                Set ^MyApp.Counter = $GET(^MyApp.Counter) + by
                Return ^MyApp.Counter
            }
        }

Policies

Policies let you control and audit how tools are used by your agents. The types of policies are as follows:

  • Authorization (%AI.Policy.Authorization, %AI.Policy.ConsoleAuth) — Restricts who and under what conditions a tool can be run.

  • Audit (%AI.Policy.Audit, %AI.Policy.ConsoleAudit) — Logs tool executions.

  • Discovery (%AI.Policy.Discovery) — Dynamically constructs tools at runtime.

The general procedure for implementing a policy is as follows:

  1. Extend the policy type's associated abstract class.

  2. Implement the methods of the superclass, which define the how the policy behaves.

  3. Attach the policy to either your agent's %AI.ToolMgr or declaratively specify the policy in your ToolSet definition:

    To attach the policy to your agent's %AI.ToolMgr (which applies to all of that agent's tools):

    Do agent.ToolMgr.SetAuthPolicy(##class(MyApp.AuthPolicy).%New())
    Do agent.ToolMgr.SetAuditPolicy(##class(MyApp.AuditPolicy).%New())
    Do agent.ToolMgr.SetDiscoveryPolicy(##class(MyApp.DiscoveryPolicy).%New())
    
    

    To declaratively attach the policy to a %AI.ToolSet, create a <Policies> block in its XData Definition. The following example attaches MyApp.PathSanitizerPolicy and MyApp.FileAuditPolicy to the FileSystemsTools toolset:

    XData Definition [ MimeType = application/xml ]
        {
            <ToolSet Name="FileSystemTools">
                <Description>Secure file system operations.</Description>
    
                <!-- Policy Definitions -->
                <Policies>
                    <!-- Authorization Policy with Configuration -->
                    <Authorization Class="MyApp.PathSanitizerPolicy">
                        <AllowedPath>/data</AllowedPath>
                        <AllowedPath>/tmp</AllowedPath>
                        <Strict>true</Strict>
                    </Authorization>
    
                    <!-- Audit Policy -->
                    <Audit Class="MyApp.FileAuditPolicy">
                        <LogLevel>INFO</LogLevel>
                    </Audit>
                </Policies>
        }
    

    For details on how toolset- and ToolMgr-level policies interact, see Policy Composition

Authorization Policies

An authorization policy is any class that extends %AI.Policy.Authorization. These let you control who and under what conditions an agent can use your tool.

The %AI.Policy.Authorization.CanList() method returns a %Boolean and determines whether agents can see the tool. You can use the metadata object passed to this method to verify if, for example, the caller has the required roles specified by the <Requirement> blocks attached to the toolset itself.

Assuming your agent can see the tool, the agent can then call it. When it does so, the call and the tool's associated metadata are passed to %AI.Policy.Authorization%CanExecute():

  • call — A %DynamicObject containing information about the call. It contains:

    • id — A unique identifier.

    • name — The name of the tool.

    • arguments — A %DynamicObject of the arguments of the call.

    • extra — Additional metadata for the call, if any, such as "thinking" data from the model.

  • metadata — A %DynamicObject containing the toolset's <Requirement> data..

The above information can be used to determine whether the call should be allowed, disallowed, or allowed with some modification, depending on what you return:

  • $$$ERROR — The agent cannot make the tool call.

  • $$$OK — The agent can make the tool call, possibly with modifications. Returning $$$OK lets you (optionally) perform modifications on the tool call before giving your approval. To modify the call, change the contents of the call argument.

In this example, Sample.AI.Policies.PathSanitizerPolicy implements an authorization policy that restricts and modifies the file paths allowed in tool calls. A tool that uses this policy:

  • Must only use paths that match the specified AllowedPath prefixes.

  • Assuming the path does match, the policy sanitizes the path with %SYS.Library.File.NormalizeFilename() and then overwrites the path specified in the tool call before giving the caller approval.

Include %AI

/// Sample authorization policy that restricts file access to allowed paths
/// This example demonstrates how to implement security policies for file operations
Class Sample.AI.Policies.PathSanitizerPolicy Extends %AI.Policy.Authorization
{

/// List of allowed path prefixes
Property AllowedPath As list Of %String(MAXLEN = "");

/// If true, deny access to paths not in AllowedPath list
Property Strict As %Boolean;

Method %CanExecute(tool As %String, call As %DynamicObject, metadata As %DynamicObject) As %Status
{
    // Parse arguments
    Set args = {}.%FromJSON(call.arguments)

    // Check for path parameter
    If args.%IsDefined("path") {
        Set path = args.path
        Set allowed = 0

        // Check if path starts with an allowed prefix
        For i=1:1:..AllowedPath.Count() {
            Set allowedPrefix = ..AllowedPath.GetAt(i)
            If $EXTRACT(path, 1, $LENGTH(allowedPrefix)) = allowedPrefix {
                Set allowed = 1
                Quit
            }
        }

        If 'allowed && ..Strict {
            Return $$$ERROR($$$AICoreToolAccessDenied, "Path not in allowed list: "_path)
        }

        // Sanitize path (remove relative components)
        Set sanitized = ##class(%File).NormalizeFilename(path)
        If sanitized '= path {
            Set args.path = sanitized
            Set call.arguments = args.%ToJSON()
        }
    }

    Return $$$OK
}

}

Audit Policies

An audit policy is a class that extends %AI.Policy.Audit. Its %AI.Policy.Audit.%LogExecution method lets you log the activity of a tool call, including whether the call succeeded, the tool name, ID, arguments, and whatever it returned; this data is contained in its arguments:

  • call — A %DynamicObject containing information about the call. It contains:

    • id — A unique identifier.

    • name — The name of the tool that the model originally attempted to call.

    • arguments — A %DynamicObject of the call's arguments.

  • metadata — A %DynamicObject containing the toolset's <Requirement> data..

  • result — A %DynamicObject containing the results of the call. If there was an error, this is null. Otherwise, it contains:

  • duration — How long it took, in milliseconds, to finish execution.

  • status — The %Status of the call, either $$$OK if the call succeeded or an error if it failed.

In this example, each entry in the audit log is represented by an instance of MyApp.ToolAuditLog. MyApp.DatabaseAudit implements the %AI.Policy.Audit.%LogExecution method to create instances of MyApp.ToolAuditLog and record details of the tool execution to its properties:

Class MyApp.DatabaseAudit Extends %AI.Policy.Audit
{
    Method %LogExecution(call, metadata, result, duration, status)
    {
        Set log = ##class(MyApp.ToolAuditLog).%New()
        Set log.Timestamp = $ZDATETIME($HOROLOG, 3)
        Set log.ToolName = call.name
        Set log.Arguments = call.arguments.%ToJSON()
        Set log.Success = $$$ISOK(status)
        Set log.DurationMs = duration
        Set resultJson = result.%ToJSON()
        Set log.ResultSize = $LENGTH(resultJson)

        If $$$ISOK(status) {
            Set log.ResultPreview = $EXTRACT(resultJson, 1, 200)
        } Else {
            Set log.Error = $SYSTEM.Status.GetErrorText(status)
        }

        Do log.%Save()
    }
}

Discovery Policies

When you make a request to an agent the prompt includes, in addition to your natural-language question, a catalog of tools. This catalog is populated by both tools explicitly assigned to your agent (for example, with agent.UseToolSet("MyTools.SimpleTools")) and the tools dynamically constructed by your discovery policy, if any. Discovery policies let you dynamically construct a set of tools from non-tool classes and methods without compile-time registration.

A discovery policy is a subclass of %AI.Policy.Discovery that implements the following methods:

  • %Resolve() — Populates the tool catalog.

  • %Execute() — Called when the agent wants to execute a particular tool made available by this policy. Because the tools created by this discovery policies involve non-tool types, this method performs the work involved with actually executing the method and then returns the results to the agent.

The following example creates a discovery policy ClassQueryDiscovery that dynamically adds tools that perform class queries to the agent's catalog.

%Resolve() constructs and pushes tools (in this case, %AI.Tools.SQL tools which perform class queries) to the catalog, including the tool name and description. It also creates an internal record of tools (ToolMap) so the discovery policy knows which tools it's responsible for executing.

%Execute() is called when the agent actually wants to call a particular tool. This method checks (with the ToolMap created by %Resolve) whether it's responsible for executing the tool and then performing the action advertised by the tool. Again, these tools execute a class query, so your implementation of %Execute() performs the "work" involved with doing the query and then returns the result to the agent.

From the agent's perspective, these dynamically constructed class query tools behave like any other tool in its catalog.

Include %AI

/// Dynamic tool discovery from compiled class queries.
///
/// Scans one or more ObjectScript classes for SqlProc class queries and makes each one
/// callable as an agent tool without any compile-time registration.
/// Demonstrates both halves of %AI.Policy.Discovery:
///
///   %Resolve  -- query %Dictionary.CompiledQuery for all metadata in one pass,
///                build tool specs from FormalSpec (parameters) and ResultSetReturnsSchema,
///                reusing %AI.Tool.Schema and %AI.Tools.SQL helpers
///   %Execute  -- dispatch to %PrepareClassQuery + %Execute via variadic args,
///                then format results via %AI.Tools.SQL.FormatResultSet
///
/// Tool names are prefixed with the last class-name segment, e.g.:
///   Sample.AI.Orders.Order + SearchByStatus -> Order_SearchByStatus
///
Class Sample.AI.Policies.ClassQueryDiscovery Extends %AI.Policy.Discovery
{

/// Comma-separated list of fully-qualified class names to scan.
Property Classes As %String(MAXLEN = "");

/// Maximum rows returned per query call.
Property MaxRows As %Integer [ InitialExpression = 100 ];

/// Internal dispatch table: toolname -> {class, query, params}
/// params is a $LIST of parameter names in declaration order.
Property ToolMap As %DynamicObject [ Internal ];

Method %OnNew(classes As %String, maxRows As %Integer = 100) As %Status [ Internal ]
{
    Set ..Classes = classes
    Set ..MaxRows = maxRows
    Set ..ToolMap = {}
    Return $$$OK
}

// ---------------------------------------------------------------------------
// %Resolve
// ---------------------------------------------------------------------------

Method %Resolve(catalog As %DynamicArray) As %Status
{
    Set ..ToolMap = {}
    For i = 1:1:$LENGTH(..Classes, ",") {
        Set cls = $ZSTRIP($PIECE(..Classes, ",", i), "<>W")
        If cls = "" Continue
        $$$ThrowOnError(..ResolveClass(cls, catalog))
    }
    Return $$$OK
}

Method ResolveClass(cls As %String, catalog As %DynamicArray) As %Status [ Internal, Private ]
{
    Set prefix = $PIECE(cls, ".", *)  // last segment used as tool-name prefix

    // Single query fetches all metadata needed for the tool spec and dispatch table.
    // SqlRowSpec is not used directly -- return shape comes from ResultSetReturnsSchema().
    Set rs = ##class(%SQL.Statement).%ExecDirect(,
        "SELECT Name, Description, FormalSpec " _
        "FROM %Dictionary.CompiledQuery " _
        "WHERE parent->ID = ? AND SqlProc = 1 AND Deprecated = 0 " _
        "ORDER BY Name",
        cls)
    If rs.%SQLCODE < 0 {
        Return $$$ERROR($$$SQLError, rs.%SQLCODE, rs.%Message)
    }

    While rs.%Next() {
        Set qname      = rs.%Get("Name")
        Set desc       = rs.%Get("Description")
        Set formalspec = rs.%Get("FormalSpec")

        If desc = "" Set desc = "Execute " _ cls _ " query " _ qname _ "."

        // Parse the FormalSpec into a local array; ParseFormalSpec kills parsedArgs first
        $$$ThrowOnError(..ParseFormalSpec(qname, formalspec, .parsedArgs))

        // Build parameter JSON Schema and record name order for dispatch
        Set paramschema = {"type": "object", "properties": {}, "required": []}
        Set paramorder  = ""
        For i = 1:1:parsedArgs {
            Set argname = parsedArgs(i)
            Set argtype = $GET(parsedArgs(i, 2), "%String")
            Set argdesc = ##class(%Global).Unquote($GET(parsedArgs(i, 2, "DESCRIPTION")))

            Set sc = ##class(%AI.Tool.Schema).%ElementSchema(argtype, .propschema)
            If $$$ISERR(sc) Set propschema = {"type": "string"}
            If argdesc '= "" Set propschema.description = argdesc

            Do paramschema.properties.%Set(argname, propschema)
            If '$DATA(parsedArgs(i, 3)) Do paramschema.required.%Push(argname)
            Set paramorder = paramorder _ $LB(argname)
        }

        Set toolname = prefix _ "_" _ qname

        Do catalog.%Push({
            "name":        (toolname),
            "description": (desc),
            "source":      "iris",
            "parameters":  (paramschema),
            "returns":     (##class(%AI.Tools.SQL).ResultSetReturnsSchema()),
            "metadata":    {"kind": "query", "provider": "ClassQueryDiscovery"}
        })

        Do ..ToolMap.%Set(toolname, {
            "class":  (cls),
            "query":  (qname),
            "params": (paramorder)
        })
    }
    If rs.%SQLCODE < 0 {
        Return $$$ERROR($$$SQLError, rs.%SQLCODE, rs.%Message)
    }
    Return $$$OK
}

/// Parse a compiled query FormalSpec string into a local array of argument
/// descriptors, using the IRIS compiler's own parsing routine.
///
/// After the call, parsedArgs contains:
///   parsedArgs       = total number of arguments
///   parsedArgs(i)    = argument name
///   parsedArgs(i, 2) = IRIS type (e.g. "%String", "%Integer")
///   parsedArgs(i, 2, "DESCRIPTION") = DESCRIPTION type-parameter if present
///   parsedArgs(i, 3) = default value (key absent = no default = required)
///
/// $$parseFormal^%occName is the IRIS compiler utility used internally to parse
/// method and query formal specifications.  The first argument must be "Method"
/// even for queries -- it controls how the spec is interpreted.
ClassMethod ParseFormalSpec(queryName As %String, formalSpec As %String, Output parsedArgs) As %Status [ Internal, Private ]
{
    Kill parsedArgs
    Try {
        Do $$parseFormal^%occName("Method", queryName, formalSpec, .parsedArgs)
    } Catch ex {
        Return ex.AsStatus()
    }
    Return $$$OK
}

// ---------------------------------------------------------------------------
// %Execute
// ---------------------------------------------------------------------------

Method %Execute(toolName As %String, args As %DynamicObject) As %DynamicObject
{
    Set entry = ..ToolMap.%Get(toolName)
    If '$ISOBJECT(entry) Return $$$NULLOREF  // not our tool

    Set cls    = entry.class
    Set qname  = entry.query
    Set porder = entry.params  // $LIST of parameter names in declaration order

    Set stmt = ##class(%SQL.Statement).%New()
    $$$ThrowOnError(stmt.%PrepareClassQuery(cls, qname))

    // Build positional args in formalspec order then spread into %Execute.
    // callArgs = 0 ensures zero-parameter queries dispatch correctly.
    Kill callArgs
    Set callArgs = 0
    For i = 1:1:$LL(porder) {
        Set callArgs($I(callArgs)) = args.%Get($LG(porder, i))
    }

    Set t0 = $zh
    // %SQLCODE on the result is checked inside FormatResultSet (returns error object on failure)
    Set rs = stmt.%Execute(callArgs...)

    Return ##class(%AI.Tools.SQL).FormatResultSet(rs, ..MaxRows, t0)
}

}

To use the policy, instantiate it. Recall that this particular discovery policy involves constructing class queries and presenting them to the agent as tools, so the instance is created with a comma-delimited string of the classes to query:

Set policy = ##class(Sample.AI.Policies.ClassQueryDiscovery).%New("Example.Order,Example.Product")

You can then attach the policy to your agent's ToolMgr (this disables Smart Discovery):

Do agent.ToolMgr.SetDiscoveryPolicy(policy)

Example Policies

In addition to the examples below, InterSystems IRIS provides the %AI.Policy.ConsoleAuth and %AI.Policy.ConsoleAudit classes.

In this example, the agent has access to a set of tools for reading and writing to the filesystem: read_file, delete_file, write_file, and append_file. To restrict the agent to read operations, MyApp.ReadOnlyPolicy implements the %CanExecute() method to prevent agents from using write-related tools by verifying their names (which are known to users ahead of time):

Class MyApp.ReadOnlyPolicy Extends %AI.Policy.Authorization
    {
        Method %CanExecute(tool As %String, call As %DynamicObject, metadata As %DynamicObject) As %Status
        {
            // Extract tool name
            Set toolName = call.name
    
            // Deny all write/delete operations
            If (toolName = "delete_file") ||
               (toolName = "write_file") ||
               (toolName = "append_file") {
                Return $$$ERROR($$$AICoreToolAccessDenied, "Write operations not allowed in read-only mode")
            }
    
            Return $$$OK
        }
    }

In this example, each entry in the audit log is represented by an instance of MyApp.ToolAuditLog. MyApp.DatabaseAudit implements the %LogExecution() to create instances of MyApp.ToolAuditLog, which records details of each tool execution:

Class MyApp.DatabaseAudit Extends %AI.Policy.Audit
    {
        Method %LogExecution(call, metadata, result, duration, status)
        {
            Set log = ##class(MyApp.ToolAuditLog).%New()
            Set log.Timestamp = $ZDATETIME($HOROLOG, 3)
            Set log.ToolName = call.name
            Set log.Arguments = call.arguments.%ToJSON()
            Set log.Success = $$$ISOK(status)
            Set log.DurationMs = duration
            Set resultJson = result.%ToJSON()
            Set log.ResultSize = $LENGTH(resultJson)
    
            If $$$ISOK(status) {
                Set log.ResultPreview = $EXTRACT(resultJson, 1, 200)
            } Else {
                Set log.Error = $SYSTEM.Status.GetErrorText(status)
            }
    
            Do log.%Save()
        }
    }

Policy Composition

Policies to be defined at two levels:

  • Global — Attached to the agent's ToolMgr, and therefore applies to all of that agent's tools.

  • ToolSet — Defined in the %AI.ToolSet, and therefore applies to only to the tools in that ToolSet.

The behavior of these composed policies varies based on the policy type:

  • Authorization — Both the global-level and ToolSet-level policy must allow the execution of the tool. The global-level policy is applied first.

  • Audit — Both the global-level and ToolSet audit policies take effect. The global-level policy is applied first.

  • Discovery — The ToolSet-level discovery policy has priority over the global-level policy. The ToolSet-level policy is applied first.

In practice, the execution order doesn't affect how tools are used or discovered, but this information can be useful for debugging policies at a particular level if, for example, it's not clear which policy is or isn't taking effect when you see a certain behavior.

Sub-Agents

The InterSystems AI Hub provides an agent-nesting mechanism for implementing the recursive language model (RLM) pattern. This allows agents ("parents") to break tasks down hierarchically and delegate these subtasks to more specialized sub-agents. These sub-agents act independently with their own system prompts and conversation contexts, but they inherit all other properties from their parent agent. Sub-agents can even spawn their own sub-agents for further delegation.

The goal of this delegation is to prevent polluting the parent agent's conversation context; by creating agents for certain types of tasks, you can dedicate that sub-agent's conversation context to solving that particular problem. This generally leads to better results for both the task and subtask.

WARNING:

Manage your nesting carefully and monitor your costs; deep nesting can be expensive. In general, you should not exceed 2 or 3 layers of nesting.

To create a sub-agent, use the parent agent's %AI.Agent.CreateSubAgent. This method creates a sub-agent that inherits all the properties of its parent; only its system prompt changes:

// Create parent agent
    set config = ##class(%DynamicObject).%New()
    set key = $SYSTEM.Util.GetEnviron("OPENAI_API_KEY")
    do config.%Set("api_key", key)
    set provider = ##class(%AI.Provider).Create("openai", config)
    Set agent = ##class(%AI.Agent).%New(provider)
    set agent.Model = "gpt-4"
    set agent.SystemPrompt = "You are a helpful assistant."
    set agent.Temperature = 0.3
    
    // Create a sub-agent that shares the parent's provider (no extra API connection)
    // Notice that the prompt is different
    Set subagent = agent.CreateSubAgent("You are a creative poet. Write short, beautiful poems.")
    
    // Add tools specific to this sub-agent's role
    Do subagent.ToolMgr.AddTool("Sample.SimpleTools")
    
    // Run the sub-agent
    Set session = subagent.CreateSession()
    Set response = subagent.Run(session, "Write a haiku.")
    Write response.Content

Skills

A skill is a reusable prompt with optional helper methods.

You create a skill manually by extending %AI.Agent.Skill and then defining its properties in XData blocks. These properties include:

  • Parameter TOOLS — A comma-separated list of ToolSet classes that help the agent perform the skill.

  • XData Summary — A YAML-formatted specification of the skill:

    • name — The name of the skill.

    • description — The skill's description. This helps the agent know what it does and when to call it.

    • parameters — The parameters that should be passed to the skill.

    • tags — Keywords related to the skill.

  • XData INSTRUCTIONS — A system prompt for the skill.

The following is an example of a very simple skill to demonstrate how to set the various properties and how to create helper methods. When a skill is used by an agent, these helper methods appear to them tools. This particular skill prompts the agent to echo back the user's input with a word count.

Class Example.Skill Extends %AI.Agent.Skill
{

/// Previously defined %AI.Tool or %AI.ToolSet classes 
Parameter TOOLS = "Sample.SimpleTools";

/// YAML metadata for this skill
XData SUMMARY [ MimeType = "text/yaml" ]
{
name: echo
description: Echo back the user's request with analysis
}

/// SubAgent instructions (markdown format)
XData INSTRUCTIONS [ MimeType = "text/markdown" ]
{
You are an echo assistant. When the user provides input:

1. Acknowledge their input
2. Count the words
3. Echo back the original message
4. Respond with a summary in this format:
   - Word count: [number]
   - Original: [their message]
}

/// Optional: Another helper method
Method CountWords(text As %String) As %Integer
{
    Set words = $LENGTH($ZSTRIP(text, "<>W"), " ")
    Return words
}

}

This example is a document summarization skill. Notice that this skill does not contain any helper functions and instead uses the Sample.FileSystemTools tool to let the agent retrieve and summarize files straight from the filesystem:

Class Sample.SummarizeDocument Extends %AI.Agent.Skill
{
    /// ToolSet classes whose tools this skill contributes (comma-separated)
    Parameter TOOLS = "Sample.FileSystemTools";

    XData SUMMARY [ MimeType = "text/yaml" ]
    {
name: summarize-document
description: Summarize a document from the filesystem. Returns a concise summary.
tags:
  - summarization
  - documents
    }

    XData INSTRUCTIONS [ MimeType = "text/markdown" ]
    {
## Document Summarization

When asked to summarize a document:
1. Read the file using the filesystem tools.
2. Produce a clear, concise summary under 200 words.
3. Focus on key points and main conclusions.
    }
}

To prompt the agent with the skill, use %SYS.%AI.Agent.UseSkill():

// Create the provider and agen
set config = ##class(%DynamicObject).%New()
set key = $SYSTEM.Util.GetEnviron("OPENAI_API_KEY")
do config.%Set("api_key", key)
set provider = ##class(%AI.Provider).Create("openai", config)

Set agent = ##class(%AI.Agent).%New(provider)
set agent.Model = "gpt-4"
set agent.Temperature = 0.3

// Prompt the agent with a skill
$$$ThrowOnError(agent.UseSkill("Sample.SummarizeDocument"))

// Create a session and interact with the agent
do agent.CreateSession()

Set response = agent.Chat(session, "What is your prompt?")
write response.Content

My prompt is to summarize a provided document succinctly and accurately. This includes reading the file, creating a clear, concise summary under 200 words, and focusing on key points and main conclusions.

Set response = agent.Chat(session, "Summarize the content of /vanka.txt")
Write response.Content

Vanka, a character from Anton Chekhov's story, writes a desperate letter to his grandfather, detailing the harsh conditions of his life as an abused and hungry orphan. He implores his grandfather to rescue him and cherishes his concertina, not wanting it to be given away. The tragedy is in the delivery; Vanka, not knowing how to properly address the letter, simply writes, "To grandfather in the village." This makes it unlikely the letter will ever reach its destination. Despite this, Vanka falls asleep with dreams fueled by hope.

Markdown Skills

Because skills are declarative and use a consistent YAML structure for their definitions, InterSystems IRIS includes a method for importing and exporting skills as simple Markdown files so they can be easily shared.

To export an existing skill, use %AI.Agent.Skill.ExportSkill(), specifying a path to a directory, a path to a file, or a stream:

// Create an instance of the skill
Set skill = ##class(Sample.SummarizeDocument).%New()

// Export to a directory
// This exports the skill to /opt/skills/summarize-document/SKILL.md
Set path = skill.ExportSkill("/opt/skills")
    
// Export to a file
// This exports the skill to /opt/skills/summarize-document.md
Set path = skill.ExportSkill("/opt/skills/summarize-document.md")
    
// Export to a stream
Set stream = ##class(%Stream.GlobalCharacter).%New()
Do skill.ExportSkill(stream)

To import a skill, use %AI.Agent.Skill.GetSkillFromURI(), specifying a URI (local or remote):

// From a directory
Set skillFromDirectory = ##class(%AI.Agent.Skill).("file:///opt/skills/summarize")
    
// From a file path
Set skillFromFile = ##class(%AI.Agent.Skill).GetSkillFromURI("file:///opt/skills/summarize-document.md")

// From a git repository
Set skillFromRemote = ##class(%AI.Agent.Skill).GetSkillFromURI("https://github.com/myorg/skills", "summarize")

You can then use the skill with %SYS.%AI.Agent.UseSkill(), specifying the %AI.Agent.Skill instance returned by GetSkillFromURI():

agent.useSkill(skillFromFile)

Auditing

In addition to the auditing performed by audit policies, InterSystems IRIS also performs its own auditing when certain events occur. These are all enabled by default; to change these settings, go to System > Security Management > System Audit Events in the Management Portal.

  • %System/%MCP/ToolDiscovery — Triggered during tool discovery and includes the timestamp, username, roles, tool name, tool description, tool provider, and the tool's REQUIRESAUTH value.

  • %System/%MCP/ToolCall — Triggered when a tool is called and includes the tool name, authorization policies, a timestamp of the call, and the user's username and roles. Note that this does not include the tool's arguments as they might contain sensitive information; this should be implemented by audit policies.

  • %System/%MCP/Login — Triggered during login attempts to an MCP server endpoint and includes the standard login metadata, such as the user, roles, and timestamp of the attempt.

Config Store Integration

The Config Store is an ObjectScript API for centralized management of configuration data. Access and usage of configuration data are governed by the standard InterSystems IRIS resource-based security model.

Note:

To create a Configuration, you must have the %Admin_ConfigStore:UOpens in a new tab permission.

API Overview

A piece of configuration data is called a configuration, which is an instance of %ConfigStore.Configuration and consists of the following:

  • Name — String, the name of the configuration, which has the form: area.type.subtype.logicalname. The subtype is optional.

    For example:

    • AI.MCP.MyServer (subtype omitted; MyServer is the logical name)

    • AI.LLM.AWSBedrock.my_other_server

  • Details — JSON objects, the actual configuration data. For example:

    set configData = {
        "model_provider": "openai",
        "model": "gpt-4o",
        "api_key": "sk-..."
    }
    

    You can also use a %DynamicObject:

    set config = ##class(%DynamicObject).%New()
    do config.%Set("model_provider", "openai")
    do config.%Set("model", "gpt-4o")
    do config.%Set("api_key", "sk-...")
    

To create and save a configuration to the Config Store, use %ConfigStore.Configuration.Create(). This example shows how to use each field (a * (asterisk) indicates that the argument is required):

SET sampleConfigData = { "domain" : "example.com" }

DO ##class(Security.Resources).Create("ExampleReadResource")
DO ##class(Security.Resources).Create("ExampleEditResource")

DO ##(%ConfigStore.Configuration).Create(
    "Area",                      ; Area*
    "Type",                      ; Type*
    "Subtype",                   ; Subtype
    "Example",                   ; Logical name*
    sampleConfigData,            ; Details, either JSON or %DynamicObject*
    "Example Configuration",     ; Display name
    "An example configuration",  ; Description
    "ExampleReadResource",       ; Read resource; if omitted, any user can read the resource
    "ExampleEditResource",       ; Edit resource; if omitted, any user can edit the resource
    1                            ; Enables the Configuration (default: 1)
    0                            ; Validates the Configuration with a descriptor (default: 1)
)

The previous simple example disabled validation; this is an optional feature that allows you to define ConfigStore.Descriptors for a given area.type.subtype and validate configurations that match that naming pattern as they're instantiated.

The following example demonstrates the validation feature by defining a descriptor for AI.LLM configurations. These configurations will eventually be used to create an OpenAI or Google Gemini provider, and creating either of these providers requires provider ID and API key, so these fields are listed as required in the descriptor:

Class Sample.ProviderDescriptor Extends ConfigStore.Descriptor
{
    Parameter Area = "AI";
    Parameter Type = "LLM";
    Parameter Subtype = "";

    XData Schema [ MimeType = application/json ]
    {
        {
            "provider": {
                "type": "string",
                "required": true,
                "description": "The provider ID, one of the following: openai | gemini"
            },
            "api_key": {
                "type": "secret",
                "required": true,
                "description": "The API key"
            }
        }
    }
}

The secret type indicates that the data in that field should not be the secret itself, but instead be a string that references a secret stored in the Secure WalletOpens in a new tab. This field type should be used for storing things like API keys. References to secrets are strings that use the format:

secret://CollectionName.SecretName.FieldName

For example, to create a configuration for an OpenAI model:

// --- Secure Wallet Setup ---
// --- Create Collection: AISecrets ---
WRITE ##class(Security.Resources).Create("CollUseResource")
WRITE ##class(Security.Resources).Create("CollEditResource")
WRITE ##class(%Wallet.Collection).Create("AISecrets", {"UseResource" : "CollUseResource", "EditResource" : "CollEditResource"})

// --- Create a secret containing the OpenAI API key: OpenAIKey ---
// (which is retrieved from the "OPENAI_API_KEY" environment variable)
SET secret = {"Usage": "CUSTOM", "Secret": {"api_key": ($SYSTEM.Util.GetEnviron("OPENAI_API_KEY"))}}
SET sc = ##class(%Wallet.KeyValue).Create("AISecrets.OpenAIKey", secret)

// Create details, referencing the api_key secret stored in the Secure Wallet
SET details = { "provider": "openai", "api_key": "secret://AISecrets.OpenAIKey#api_key" }

// Create the configuration, specifying 1 for the final argument to validate it against the descriptor
SET sc = ##class(%ConfigStore.Configuration).Create("AI", "LLM", "", "OpenAIExample", details, "", "", "", "", 1, 1)

To retrieve a saved configuration, use %ConfigStore.Configuration.GetDetails. The final two Boolean arguments in the call are for validation and secret resolution (that is, replacing the secret reference string with the actual secret), respectively:

// Retrieve details from config store
SET sc = ##class(%ConfigStore.Configuration).Get("AI.LLM.OpenAIExample", .retrievedDetails, 1, 1)

// Create provider
SET provider = ##class(%AI.Provider).Create(retrievedDetails.provider, retrievedDetails)

You can also retrieve data for Parameters, which can be useful for creating agents declaratively:

// Full FQN -- used as-is
Parameter PROVIDERCONFIG = "@{config.AI.LLM.ProductionLLM}";

// Short form -- AI.LLM is prepended automatically
Parameter PROVIDERCONFIG = "@{config.ProductionLLM}";

// With subtype
Parameter PROVIDERCONFIG = "@{config.AI.LLM.OpenAI.ProductionLLM}";

To modify a configuration, you must have the Use permission on the Configuration's EditResource (if it has one) and use %ConfigStore.Configuration.Modify(). You can modify any of the properties of a Configuration except its name; the name is used to specify which Configuration to change:

%ConfigStore.Configuration.Modify(
    name,             ; %String
    details,          ; JSON/%DynamicObject
    displayName,      ; %String
    description,      ; %String
    readResourceName, ; %String, modifying this requires %Admin_ConfigStore:U
    editResourceName, ; %String, modifying this requires %Admin_ConfigStore:U
    enabled           ; %Boolean
    validateDetails   ; %Boolean, whether to validate the configuration with a descriptor
)

To not modify a particular field, leave it empty. For example, to only change the description:

SET sc = ##class(%ConfigStore.Configuration).Modify(
    "AI.LLM.openai",,,"New description"
)

To delete a configuration, use %ConfigStore.Configuration.Delete():

do ##class(%ConfigStore.Configuration).Delete("AI.LLM.openai")

Required Permissions

Like other parts of InterSystems IRIS, the Config Store uses resourcesOpens in a new tab to control who can create, read, and modify Configurations:

  • Create Configurations — Requires %Admin_ConfigStore:U

  • Modify (with Modify()) Configurations — Requires the Use permission on the Configuration's Edit resource, if any. If you want to change the Configuration's Edit or Read resource, you also need %Admin_ConfigStore:U.

  • Retrieve (with Get()) Configurations — Requires the Use permission on the Configuration's Read resource, if any.

FeedbackOpens in a new tab