<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: InterSystems Developer</title>
    <description>The latest articles on DEV Community by InterSystems Developer (@intersystemsdev).</description>
    <link>https://dev.to/intersystemsdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F405886%2F59cd7fa8-de28-47cf-acb0-c800febc5986.png</url>
      <title>DEV Community: InterSystems Developer</title>
      <link>https://dev.to/intersystemsdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/intersystemsdev"/>
    <language>en</language>
    <item>
      <title>Model Context Protocol (MCP) with InterSystems IRIS - From Zero to Hero</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sat, 31 Oct 2026 20:45:42 +0000</pubDate>
      <link>https://dev.to/intersystems/model-context-protocol-mcp-with-intersystems-iris-from-zero-to-hero-1nnb</link>
      <guid>https://dev.to/intersystems/model-context-protocol-mcp-with-intersystems-iris-from-zero-to-hero-1nnb</guid>
      <description>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Today, coding assistants like Claude, GitHub Copilot and Cursor have transformed the way developers write code. However, these tools are limited by being isolated from the systems and data sources that developers work with daily. This limitation can be overcome through the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;, an open standard designed to connect AI assistants to external data sources and tools in a secure and standardized way.&lt;/p&gt;
&lt;p&gt;In this review article, we'll explore the current state-of-the-art regarding the MCP within the InterSystems ecosystem. Moreover, we will examine how this open standard connects AI assistants to external data sources, presenting a first example of a custom MCP server developed in Python and integrated with the &lt;span&gt;&lt;code&gt;intersystems-irispython&lt;/code&gt;&amp;nbsp;&lt;/span&gt;package.&lt;/p&gt;
&lt;h2&gt;Table of Contents&lt;/h2&gt;&amp;nbsp;&lt;p&gt;Table of Contents&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Part 1: What is the Model Context Protocol?&lt;ul&gt;
&lt;li&gt;1.1 Origins and Background&lt;/li&gt;
&lt;li&gt;1.2 Why MCP Matters&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: Understanding MCP Architecture&lt;ul&gt;
&lt;li&gt;2.1 The Client-Host-Server Model&lt;/li&gt;
&lt;li&gt;2.2 Transport Layer&lt;/li&gt;
&lt;li&gt;2.3 Data Layer&lt;/li&gt;
&lt;li&gt;2.4 Core Primitives&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Part 3: Building MCP Servers from Scratch with Python&lt;ul&gt;
&lt;li&gt;3.1 Using uv to Create a New Python Package&lt;/li&gt;
&lt;li&gt;3.2 Basic MCP Server Structure&lt;/li&gt;
&lt;li&gt;3.3 Implementing Core Primitives&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Part 4: Using MCP with AI Development Tools&lt;ul&gt;&lt;li&gt;4.1 Configuring MCP Servers&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Part 5: Distributing and Running MCP Servers with uv and uvx&lt;ul&gt;
&lt;li&gt;5.1 Local Clone (Development Mode)&lt;/li&gt;
&lt;li&gt;5.2 GitHub Repository&lt;/li&gt;
&lt;li&gt;5.3 PyPI&lt;ul&gt;&lt;li&gt;5.3.1 Publishing a Package to PyPI&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Part 6: MCP and InterSystems IRIS&lt;ul&gt;&lt;li&gt;6.1 Existing MCP Servers for IRIS&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Part 7: Building Your Own MCP Server for IRIS&lt;ul&gt;
&lt;li&gt;7.1 The Blueprint Implementation of an IRIS MCP Server&lt;/li&gt;
&lt;li&gt;7.2 Highlights of the Code&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Part 8: Conclusion&lt;/li&gt;
&lt;li&gt;Part 9: Sources&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Part 1: What is the Model Context Protocol?&lt;/h2&gt;
&lt;h3&gt;1.1 Origins and Background&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; was introduced by &lt;strong&gt;Anthropic&lt;/strong&gt; on November 25, 2024 &lt;a href="https://www.anthropic.com/news/model-context-protocol" rel="noopener noreferrer"&gt;[1]&lt;/a&gt; as open-source with the goal of connecting AI systems to data sources, replacing fragmented integrations. Before MCP, every AI application required custom integrations to access each data source, like a database or an API. MCP solves this by providing a single, standardized protocol that any AI application can use to connect to compatible servers.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;1.2 Why MCP Matters&lt;/h3&gt;
&lt;p&gt;The protocol addresses three key challenges:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Standardization&lt;/strong&gt;: It is no longer necessary to write custom integrations for every new tool. Any AI assistant can use MCP servers that expose specific capabilities to interact with various resources in a standardized format that LLMs can easily understand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: MCP isolates each server, meaning the AI can only access the specific context required for the immediate task and cannot access other servers or the full conversation history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seamless Integration&lt;/strong&gt;: Since it is an open protocol, you can easily combine multiple servers at the same time, allowing you to build modular, customized AI workflows without overly complicating your architecture.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Official Resources:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/news/model-context-protocol" rel="noopener noreferrer"&gt;Anthropic Announcement: Introducing the Model Context Protocol [1]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/latest" rel="noopener noreferrer"&gt;Model Context Protocol - Specification [2]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/modelcontextprotocol" rel="noopener noreferrer"&gt;GitHub Repository - Model Context Protocol [3]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/docs/getting-started/intro" rel="noopener noreferrer"&gt;What is the Model Context Protocol (MCP)? [4]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Part 2: Understanding MCP Architecture&lt;/h2&gt;
&lt;h3&gt;2.1 The Client-Host-Server Model&lt;/h3&gt;
&lt;p&gt;MCP follows a &lt;strong&gt;client-host-server architecture &lt;/strong&gt;&lt;a href="https://modelcontextprotocol.io/docs/learn" rel="noopener noreferrer"&gt;[5]&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP Host&lt;/strong&gt;: The AI application (Claude Desktop, Cursor, VS Code, etc.) that coordinates and manages one or more MCP clients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Client&lt;/strong&gt;: A component within the host that maintains a dedicated connection to an MCP server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Server&lt;/strong&gt;: A program that provides some functionalities to MCP clients. MCP servers can run locally on your machine or remotely.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each host can create multiple clients, with each client maintaining a &lt;strong&gt;1:1 dedicated connection&lt;/strong&gt; with its corresponding server.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;For example, Cursor acts as an MCP Host. When Cursor establishes a connection to an MCP server it instantiates an MCP client object that maintains the connection to that specific MCP server. When Cursor later connects to a different MCP server, it instantiates an additional MCP client object to maintain the new connection.&lt;/p&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8hjmpe0jfws49dj318z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh8hjmpe0jfws49dj318z.png" alt=" " width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find the original image source &lt;a href="https://mcpcat.io/blog/mcp-server-client-host/" rel="noopener noreferrer"&gt;here [6]&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;2.2 Transport Layer&lt;/h3&gt;
&lt;p&gt;MCP supports two transport mechanisms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local servers&lt;/strong&gt;: this uses standard Standard Input/Output (STDIO) streams for direct process communication between local processes on the same machine. Since this avoids avoids network overhead, it is a better choice for local integrations or CLI utilities. MCP currently uses STDIO as the default transport layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote servers&lt;/strong&gt;: this uses Streamable HTTP to manage interactions over a network. The client sends messages via &lt;span&gt;&lt;span&gt;HTTP POST, while the server optionally uses Server-Sent Events (SSE) for streaming data. This&lt;/span&gt;&lt;/span&gt; transport allows MPC servers to run remotely (on a different machine, in the cloud, etc.) and to be accessible via a URL.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 Data Layer&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;In MCP, the message structure and semantics is based on &lt;/span&gt;&lt;/span&gt;&lt;strong&gt;JSON-RPC 2.0&lt;/strong&gt; protocol &lt;a href="https://www.jsonrpc.org/" rel="noopener noreferrer"&gt;&lt;span&gt;&lt;span&gt;[7]&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;span&gt;&lt;span&gt;. &lt;/span&gt;&lt;span&gt;All communication follows a structure like the following:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;&amp;nbsp;&lt;p&gt;JSON-RPC 2.0&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "query_database",
    "arguments": {
      "sql": "SELECT * FROM Patient LIMIT 10"
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 Core Primitives&lt;/h3&gt;
&lt;p&gt;MCP defines three core primitives that servers can expose &lt;a href="https://modelcontextprotocol.io/docs/learn" rel="noopener noreferrer"&gt;[5]&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt;: The functions your AI agent can call to perform actions according to user requests. Tools can for example write to the database, call APIs or modify files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt;: Passive data sources that provide contextual information to AI applications offering read-only access to file contents, database schemas or API documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompts&lt;/strong&gt;: Pre-built instruction templates that tell the model to work with specific tools and resources helping the user to structure interactions with the AI agent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Part 3: Building MCP Servers from scratch with Python&lt;/h2&gt;
&lt;p&gt;Before diving into code examples, let's understand how to build MCP servers in Python.&lt;/p&gt;
&lt;p&gt;MCP provides official SDKs for multiple programming languages, including TypeScript, Java, and Python &lt;a href="https://modelcontextprotocol.io/docs/sdk" rel="noopener noreferrer"&gt;[8]&lt;/a&gt;. In this article, we'll focus on &lt;strong&gt;Python&lt;/strong&gt; as it's widely used in the InterSystems community and integrates well with IRIS through the Native SDK &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=PAGE_python_native" rel="noopener noreferrer"&gt;[9]&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;3.1 Using uv to create a new Python package&lt;/h3&gt;
&lt;p&gt;Before talking about code, we should understand how to create a proper environment for our application, which won't just contain the Python code, but it also includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dependencies&lt;/li&gt;
&lt;li&gt;Python version&lt;/li&gt;
&lt;li&gt;entry points&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To set up the environment we will use the &lt;code&gt;uv&lt;/code&gt; Python package manager, a modern Python package manager that simplifies environment creation, dependency resolution, and reproducible execution of MCP servers &lt;a href="https://docs.astral.sh/uv/concepts/projects/dependencies/" rel="noopener noreferrer"&gt;[10]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can initialize properly your folder structure for your MCP project using the &lt;code&gt;uv init&lt;/code&gt; statement.&lt;/p&gt;&amp;nbsp;&lt;p&gt;Init package with uv&lt;/p&gt;
&lt;p&gt;You can initialize a new project with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv init --package my-mcp-server&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where the &lt;code&gt;--package&lt;/code&gt; option tells &lt;code&gt;uv&lt;/code&gt; to create a Python package.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;This command generates the following folder tree:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my-mcp-server/
├── pyproject.toml      
├── README.md
├── .python-version     
└── src/
    └── my_mcp_server/
        └── __init__.py&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;.python-version&lt;/code&gt; file specifies which Python version the project should use.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;__init__.py&lt;/code&gt; file marks a directory as a Python package. Since Python packages are expected to contain one or more Python modules, which are directories containing an &lt;code&gt;__init__.py&lt;/code&gt;, each subfolder of the project needs an &lt;code&gt;__init__.py&lt;/code&gt; (can be empty) to be importable as a package.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;pyproject.toml&lt;/code&gt; contains the metadata and configuration for the project. It defines how the package is built, its dependencies, and the Python version requirements. It is composed by several sections like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[build-system]&lt;/code&gt;: specifies how the package is built. &lt;code&gt;uv&lt;/code&gt; uses &lt;em&gt;hatchling &lt;/em&gt;as the default build backend for Python packages &lt;a href="https://docs.astral.sh/uv/concepts/build-backend/" rel="noopener noreferrer"&gt;[11]&lt;/a&gt;.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[project]&lt;/code&gt;: contains metadata and dependency definitions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is an example of &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[build-system] 
requires = ["hatchling"] 
build-backend = "hatchling.build" 

[project] 
name = "my-mcp-server" 
version = "0.1.0" 
description = "" 
readme = "README.md" 
requires-python = "&amp;gt;=3.12" 
dependencies = []&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After creating the structure, you can add dependencies to the project using the &lt;code&gt;uv add&lt;/code&gt; statement.&lt;/p&gt;&amp;nbsp;&lt;p&gt;Add server dependencies&lt;/p&gt;
&lt;p&gt;You can use the &lt;code&gt;uv add&lt;/code&gt; statement to add dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv add fastmcp intersystems-irispython requests&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adds the packages to &lt;code&gt;[project.dependencies]&lt;/code&gt; in &lt;code&gt;pyproject.toml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Resolves all transitive dependencies and writes a &lt;code&gt;uv.lock&lt;/code&gt; file. This is used to recreate the environment while building your project on another machine.&lt;/li&gt;
&lt;li&gt;Installs everything into a local &lt;code&gt;.venv/&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After this step, the &lt;code&gt;pyproject.toml&lt;/code&gt; will contain a new section for all your dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[project]
...
dependencies = [
    "fastmcp&amp;gt;=3.2",
    "intersystems-irispython&amp;gt;=5.3",
    "requests&amp;gt;=2.32",
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While the &lt;code&gt;pyproject.toml&lt;/code&gt; contains all the direct dependencies, the &lt;code&gt;uv.lock&lt;/code&gt; is a file that contains all resolved dependencies, namely the exact versions of all the direct and transitive dependencies in the project.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;At build time, instead of resolving dependencies again, &lt;code&gt;uv&lt;/code&gt; reads &lt;code&gt;uv.lock&lt;/code&gt; and installs exactly what’s written there.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;uv.lock&lt;/code&gt; file looks like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[package]]
name = "fastmcp"
version = "3.2.1"
dependencies = [
    "pydantic",
    "typing-extensions",
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, it's time to add an entry point to start the server.&lt;/p&gt;&amp;nbsp;&lt;p&gt;Adding an entry point for the MCP server&lt;/p&gt;
&lt;p&gt;To make the MCP server executable, define a CLI entry point in &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[project.scripts]
my-mcp-server = "my_mcp_server.entrypoint:main"&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This maps a command name (&lt;code&gt;my-mcp-server&lt;/code&gt;) to a Python function using the format &lt;code&gt;module.path:function&lt;/code&gt;. So, when the command is executed, Python imports &lt;code&gt;my_mcp_server.entrypoint&lt;/code&gt; and runs the &lt;code&gt;main()&lt;/code&gt; function.&lt;/p&gt;
&lt;h3&gt;3.2 Basic MCP Server Structure&lt;/h3&gt;
&lt;p&gt;Let's now take a look at the available packages that we can use to implement the MCP server. While the official &lt;code&gt;mcp&lt;/code&gt; package provides the core protocol implementation &lt;a href="https://pypi.org/project/mcp/" rel="noopener noreferrer"&gt;[12]&lt;/a&gt;, &lt;code&gt;FastMCP&lt;/code&gt; package has emerged as one of the easiest and fastest frameworks for building MCP servers with Python &lt;a href="https://gofastmcp.com/" rel="noopener noreferrer"&gt;[13]&lt;/a&gt;. This article will provide the minimal structure of an MCP server using FastMCP.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;A server is defined by three main steps:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;initializing the server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;defining tools, resources and prompt decorating the functions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;running the process&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;&amp;nbsp;&lt;p&gt;FastMCP first example&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from fastmcp import FastMCP

# 1. Create the MCP server instance
mcp = FastMCP(name="My MCP Server")

# 2. Define tools, resources and prompts using decorators
@mcp.tool
def my_tool(param: str) -&amp;gt; str:
    """Description of what this tool does. This text is sent to the LLM."""
    return f"Result: {param}"

# 3. Run the server (defaults to STDIO transport)
if __name__ == "__main__":
    mcp.run()&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4 Implementing Core Primitives&lt;/h3&gt;
&lt;p&gt;Let's see how to implement each primitive type:&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt;: &lt;u&gt;Tools allow the AI to perform actions.&lt;/u&gt; In this example we define a tool that the AI agent can use to query the database using the &lt;code&gt;@mcp.tool&lt;/code&gt; decorator. Note that the docstring becomes the tool's description shown to the AI. In fact, when using this decorator, FastMCP automatically generates a JSON schema from Python type hints and docstrings, telling the AI exactly how to use the function.&lt;/li&gt;&lt;/ol&gt;&amp;nbsp;&lt;p&gt;Tools Example&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@mcp.tool
def query_database(sql: str) -&amp;gt; str:
    """
    Execute an SQL query against the database.
    
    Args:
        sql: The SQL query to execute
    
    Returns:
        Query results as a string
    """
    # Logic to connect to IRIS and execute SQL goes here
    return execute_query(sql)&lt;/code&gt;&lt;/pre&gt;
&lt;ol start="2"&gt;&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt;: &lt;u&gt;Resources are passive data sources that the AI can read to gather context before it takes an action. They act as background information.&lt;/u&gt; In this example we define a resource using the &lt;code&gt;@mcp.resource("uri")&lt;/code&gt; decorator and assigning it a unique URI.&lt;/li&gt;&lt;/ol&gt;&amp;nbsp;&lt;p&gt;Resource Example&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@mcp.resource("resource://database/schema")
def get_schema() -&amp;gt; dict:
    """Provides the database schema information to help the AI write better SQL."""
    return {
        "tables": {
            "Patient": ["id", "first_name", "last_name", "dob"],
            "Encounter": ["id", "patient_id", "date", "reason"],
            "Observation": ["id", "encounter_id", "type", "value"]
        },
        "version": "1.0",
        "storage_type": "relational"
    }&lt;/code&gt;&lt;/pre&gt;
&lt;ol start="3"&gt;&lt;li&gt;
&lt;strong&gt;Prompts&lt;/strong&gt;: &lt;u&gt;Prompts are reusable templates that help the user to interact with the AI.&lt;/u&gt; In this example we define a prompt through the &lt;code&gt;@mcp.prompt&lt;/code&gt; decorator that can guide the AI assistant to perform specific repetitive tasks like writing queries or importing data into the database.&lt;/li&gt;&lt;/ol&gt;&amp;nbsp;&lt;p&gt;Prompt Example&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@mcp.prompt()
def sql_assistant() -&amp;gt; str:
    """A prompt template for IRIS SQL query assistance."""
    return """You are an expert SQL developer specializing in InterSystems IRIS.
    When writing queries:
    - Use proper IRIS SQL syntax (e.g., handle schema names correctly).
    - Favor performance-optimized indices.
    - Explain your reasoning if the query is complex."""&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Part 4: Using MCP with AI Development Tools&lt;/h2&gt;
&lt;p&gt;AI development tools are rapidly evolving from simple chat interfaces into sophisticated orchestrators capable of interacting with your local environment and remote servers by adopting the MCP protocol. Currently, while several platforms now support the protocol, the setup process is generally standardized across them.&lt;/p&gt;
&lt;p&gt;Most popular platforms nowadays are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt;: An AI-powered IDE built on Visual Studio Code that includes AI agent's capabilities and MCP integrations into the code editor &lt;a href="https://cursor.com/" rel="noopener noreferrer"&gt;[14]&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Claude Desktop&lt;/strong&gt;: Anthropic's official client, which uses MCP to give Claude "skills" like searching your local files or databases &lt;a href="https://claude.com/download" rel="noopener noreferrer"&gt;[15]&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;: A CLI-based agent that can integrate MCP servers directly from the terminal &lt;a href="https://claude.com/product/claude-code" rel="noopener noreferrer"&gt;[16]&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.1 Configuring MCP servers&lt;/h3&gt;Both Cursor and Claude Desktop are configured through a JSON file, and the content of that file is the same for both platforms.&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Cursor &lt;/em&gt;allows you to configure MCP servers at two different levels:&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Project-level configuration&lt;/strong&gt;: Uses a local &lt;code&gt;.cursor/mcp.json&lt;/code&gt; file within your repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global configuration&lt;/strong&gt;: Defined in user settings (found at &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt; on macOS/Linux or the equivalent AppData path on Windows).&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Claude Desktop &lt;/em&gt;supports only a &lt;strong&gt;single user-level configuration file&lt;/strong&gt;, called &lt;code&gt;claude_desktop_config.json&lt;/code&gt;, that you can find within the application itself using the menu&lt;ol&gt;&lt;li&gt;Settings → Developer → Edit Config.&lt;/li&gt;&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The following JSON structure shows how to connect Cursor or Claude to an IRIS MCP server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  "mcpServers": {
    "my-server-name": {
      "command": "executable-or-runtime",
      "args": ["path/to/script-or-package", "--option", "value"],
      "env": {
        "KEY_1": "value-1",
        "KEY_2": "value-2"
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every MCP server configuration is based on a few key fields that tell the AI how to launch and communicate with the server.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Let's look at the JSON fields to understand how this configuration works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mcpServers&lt;/code&gt;: The root object that lists all your connected servers. You can have multiple servers running simultaneously (for example: one for IRIS, one for Google Drive, one for Atlassian, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;command&lt;/code&gt;: The command used to start the server. Common examples include &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;uvx&lt;/code&gt;, or &lt;code&gt;npx&lt;/code&gt;. In particular, use &lt;code&gt;uvx&lt;/code&gt; (for Python) or &lt;code&gt;npx&lt;/code&gt; (for Node.js) to run packages directly from the web without manually installing them (see the next chapter for more information).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;args&lt;/code&gt;: An array of arguments passed to the command. This usually includes the MCP server file path or the name of a package to execute.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;env&lt;/code&gt;: An array containing your environment variables. This is the standard way to pass sensitive or environment-specific data like hostnames, port numbers, and credentials without hardcoding them into the server logic itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you have this configured, the AI assistant can directly interact with your IRIS database, execute queries, and access schema information without you manually copying and pasting context.&lt;/p&gt;

&lt;h2&gt;Part 5: Distributing and running MCP servers with uv and uvx&lt;/h2&gt;The configuration JSON file tells the client what to launch, but you also need to decide where the client should point to reach the server. Three distribution modes cover the vast majority of real-world setups, and all three rely on the &lt;code&gt;pyproject.toml&lt;/code&gt; and &lt;code&gt;uv.lock&lt;/code&gt; ee already created in Part 3.1, which is what makes the MCP server runnable as a single command.&amp;nbsp;&lt;h3&gt;5.1 Local clone (development mode)&lt;/h3&gt;Use this mode while you are actively writing or debugging the server. The user clones the repository, and the client points at the local working copy with &lt;code&gt;uv run&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;{
  "command": "uv",
  "args": ["run", "&amp;lt;mcp-server-name&amp;gt;"],
  "env": { "...": "..." }
}&lt;/code&gt;&lt;/pre&gt;The CLI is launched directly from your live source tree, so any change to a tool, prompt, or resource is picked up the next time you reload the server in the client, without having to rebuild or reinstall. This is the only mode that lets in-progress edits take effect immediately.&lt;h3&gt;5.2 GitHub repository&lt;/h3&gt;
&lt;p&gt;Once the repo is pushed, anyone with &lt;code&gt;uv&lt;/code&gt; installed can run the server without cloning it by pointing &lt;code&gt;uvx&lt;/code&gt; at the GitHub URL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  "command": "uvx",
  "args": [
    "--from", "git+https://github.com/&amp;lt;repository&amp;gt;.git",
    "&amp;lt;mcp-server-name&amp;gt;"
  ],
  "env": { "...": "..." }
}&lt;/code&gt;&lt;/pre&gt;The first time the client launches, &lt;code&gt;uvx&lt;/code&gt; clones the repo into an isolated cache, builds the wheel, and runs the entry-point. Subsequent launches reuse the cache and start quickly.&lt;h3&gt;5.3 PyPI&lt;/h3&gt;After publishing the package to PyPI, the configuration collapses to its simplest form:&lt;pre&gt;&lt;code&gt;{
  "command": "uvx",
  "args": ["&amp;lt;mcp-server-name&amp;gt;"],
  "env": { "...": "..." }
}&lt;/code&gt;&lt;/pre&gt;
&lt;code&gt;uvx&lt;/code&gt; resolves the latest wheel over HTTP and Git is not required at runtime. The first start is the fastest of the three modes and the install instructions consist only in one line.&amp;nbsp;&lt;h3&gt;5.3.1 Publishing a package to PyPI&lt;/h3&gt;Publishing the package follows three commands, all run from the project root:&amp;nbsp;&lt;p&gt;Publishing on PyPI&lt;/p&gt;Bump the version (PyPI rejects re-uploads of an existing version)&lt;pre&gt;&lt;code&gt;# Use --bump patch / minor / major depending on the change
uv version --bump patch&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Build the wheel and the source distribution into ./dist&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv build&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upload to PyPI using an API token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#    Export the token as an env var so it does not end up in shell history
#    uv publish reads it automatically from UV_PUBLISH_TOKEN. 
export UV_PUBLISH_TOKEN=pypi-&amp;lt;your-token&amp;gt;     # Linux / macOS / Git Bash 
$env:UV_PUBLISH_TOKEN = "pypi-&amp;lt;your-token&amp;gt;"   # PowerShell 
uv publish &lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Part 6: MCP and InterSystems IRIS&lt;/h2&gt;
&lt;p&gt;Nowadays we already have several example of MCP servers working with InterSystems IRIS. Since IRIS is a multi-model database, it’s a perfect candidate for this protocol. Instead of just giving an AI your code, you’re giving it a live connection to your data, your globals, and your system metrics.&lt;/p&gt;
&lt;h3&gt;6.1 Existing MCP Server for IRIS&lt;/h3&gt;
&lt;p&gt;Several developers have shared MCP implementations or MCP-related tools on the InterSystems Developer Community. Among these, the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://openexchange.intersystems.com/package/mcp-server-iris" rel="noopener noreferrer"&gt;mcp-server-iris [17]&lt;/a&gt; by &lt;span&gt;&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/dmitry"&gt;@dmitry&lt;/a&gt;.Maslennikov&lt;/span&gt;&lt;/span&gt;: An MCP server to monitor and manage Interoperability productions using natural language&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://openexchange.intersystems.com/package/intersystems-objectscript-mcp" rel="noopener noreferrer"&gt;intersystems-objectscript-mcp [18]&lt;/a&gt; by &lt;a href="https://github.com/cjy513203427" rel="noopener noreferrer"&gt;Chen Jinyao&lt;/a&gt;: An MCP server that fetches compiled routines (&lt;code&gt;.int&lt;/code&gt;, &lt;code&gt;.mac&lt;/code&gt;, &lt;code&gt;.inc&lt;/code&gt;) directly from the IRIS server.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/esalas-devel/iris-mcp-atelier" rel="noopener noreferrer"&gt;iris-mcp-atelier [19]&lt;/a&gt; by &lt;span&gt;&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/ernest"&gt;@ernest&lt;/a&gt;.Salas&lt;/span&gt;&lt;/span&gt;: A specialized MCP server that uses the Atelier REST API to give AI assistants direct read/write access to ObjectScript code on a live IRIS instance.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://openexchange.intersystems.com/package/gj-::-servAI" rel="noopener noreferrer"&gt;servAI [20]&lt;/a&gt; by &lt;span&gt;&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/john"&gt;@john&lt;/a&gt;.Murray&lt;/span&gt;&lt;/span&gt;: A VS Code extension that acts as a secure credential wrapper for MCP servers, automatically pulling connection details and passwords from the InterSystems Server Manager and avoiding the need to hardcode sensitive credentials in configuration JSON files.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://openexchange.intersystems.com/package/IRIS-MCP-Server-Suite-2" rel="noopener noreferrer"&gt;IRIS MCP Server Suite [21]&lt;/a&gt; by &lt;span&gt;&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/joshua"&gt;@joshua&lt;/a&gt;.Brandt&lt;/span&gt;&lt;/span&gt;: A comprehensive suite of services to work with InterSystems IRIS including five different MCP servers related to different domains.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/intersystems-community/ai-hub-eap" rel="noopener noreferrer"&gt;InterSystems IRIS AI Hub [22]&lt;/a&gt;: currently in Early Access and recently presented at READY 2026, it provides a unified SDK and MCP Server integration to securely connect AI models and external agents with InterSystems IRIS business logic.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Feel free to cite in the comments all the applications and developers you believe worth to be mentioned.&lt;/p&gt;

&lt;h2&gt;Part 7: Building Your Own MCP Server for IRIS&lt;/h2&gt;
&lt;p&gt;While the community tools are excellent, you may eventually need a server tailored to your specific application logic or you may want to understand how the previously mentioned tools work. In this section I'm providing an example of how to implement an MCP server to work with InterSystems IRIS.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;To build an MCP server for InterSystems IRIS I recommend you to follow two main approaches:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Using the &lt;strong&gt;InterSystems Python Native SDK &lt;/strong&gt;&lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=PAGE_python_native" rel="noopener noreferrer"&gt;[9]&lt;/a&gt;&lt;a href="https://pypi.org/project/intersystems-irispython/" rel="noopener noreferrer"&gt;[23]&lt;/a&gt;: &lt;code&gt;intersystems-irispython&lt;/code&gt; is the official Python package to connect with InterSystems IRIS providing a lightweight interface to access through Python all the resources once only available to ObjectScript, like Globals or Class Methods. This is best for heavy data operations, manipulating Globals, calling existing Business Logic and high-speed SQL execution.&lt;/li&gt;
&lt;li&gt;Using the &lt;strong&gt;Source Code File REST API&lt;/strong&gt; (a.k.a. &lt;strong&gt;Atelier API&lt;/strong&gt;) &lt;a href="https://docs.intersystems.com/iris20261/csp/docbook/DocBook.UI.Page.cls?KEY=GSCF_intro" rel="noopener noreferrer"&gt;[24]&lt;/a&gt;: The Atelier API provides a RESTful interface (&lt;code&gt;/api/atelier/&lt;/code&gt;) designed specifically for source code management. This was originally built for the Atelier IDE (and now it is used by the InterSystems Server Manager VS Code extension) and it is useful to work with source code files, compile classes, or manage development workflows.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;7.1 The blueprint implementation of an IRIS MCP server&lt;/h3&gt;To put hands on the patterns exposed from Parts 3 to 6, the rest of this article walks through a working example of an MCP server that talks to a local IRIS instance through both the Native Python SDK and the Atelier REST API called &lt;strong&gt;iris-mcp-blueprint&lt;/strong&gt;. The full source code can be found on Open Exchange or GitHub and is linked to this article.What you'll see is intentionally a &lt;strong&gt;blueprint&lt;/strong&gt;, not a production-ready MCP server. The set of tools, prompts, and resources is deliberately small, aimed only to show the patterns. Treat the code as a starting point: clone it, modify it and adapt it to implement your own MCP server tailored to your IRIS application.For full setup instructions (Docker, environment variables, prerequisites), the exact &lt;code&gt;mcp.json&lt;/code&gt; snippets for Cursor and Claude Desktop in each of the three distribution modes, the list of every tool and prompt with example arguments refer to the repository's README.&lt;h3&gt;7.2 Highlights of the code&lt;/h3&gt;The blueprint implements all three MCP primitives: &lt;strong&gt;Tools&lt;/strong&gt;, &lt;strong&gt;Prompts&lt;/strong&gt;, and &lt;strong&gt;Resources&lt;/strong&gt;. Tools act as the "hands" of the AI, using the Native SDK to perform actions like querying SQL or reading Globals. They are grouped into five macro categories, each focused on a different IRIS surface:&amp;nbsp;&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQL data access&lt;/strong&gt; (&lt;code&gt;sql.py&lt;/code&gt;)&lt;ul&gt;&lt;li&gt;The Native SDK provides a fast way to run SQL from Python using a DB-API cursor. You can perform queries both on custom and system tables. The latter, for example the &lt;code&gt;INFORMATION_SCHEMA&lt;/code&gt;, can be used to retrieve information and helping the AI to write its own SQL queries later.&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct global access&lt;/strong&gt; (&lt;code&gt;globals.py&lt;/code&gt;)&lt;ul&gt;&lt;li&gt;Globals are IRIS's physical storage layer. The blueprint reads them through the Native SDK helpers:&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;iris_obj.isDefined("^GlobalName")&lt;/code&gt; to check global status (it returns &lt;strong&gt;0&lt;/strong&gt; if node is empty, &lt;strong&gt;1 &lt;/strong&gt;if node has a value, &lt;strong&gt;10&lt;/strong&gt; if node has children (subscripts), &lt;strong&gt;11&lt;/strong&gt; if node has both a value and children).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iris_obj.get("^Global", sub1)&lt;/code&gt; and &lt;code&gt;iris_obj.set(val, "^Global", sub1)&lt;/code&gt; to get or set values from/in a global subscript.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executing existing Logic via ClassMethods &lt;/strong&gt;(&lt;code&gt;class_methods.py&lt;/code&gt;)&lt;ul&gt;&lt;li&gt;You can execute already existing &lt;em&gt;ClassMethods &lt;/em&gt;in IRIS using the &lt;code&gt;iris_obj.classMethodValue("Package.Class", "MethodName", *args)&lt;/code&gt;. This effectively turns your entire IRIS codebase into a library of functions the AI can call, including both custom and built-in methods.&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atelier REST API usage &lt;/strong&gt;(&lt;code&gt;atelier_api.py&lt;/code&gt;)&lt;ul&gt;&lt;li&gt;The Atelier API provides a REST interface for source code management and it is used by the InterSystems Server Manager VS Code extension as well.&amp;nbsp;The API has a lot of different features, but the blueprint shows two representative endpoints used to:&amp;nbsp;&lt;ul&gt;
&lt;li&gt;fetch the raw &lt;code&gt;.cls&lt;/code&gt; class definition to explain the ObjectScript logic&lt;/li&gt;
&lt;li&gt;search for a specific string into the codebase&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interoperability productions&lt;/strong&gt; (&lt;code&gt;interoperability.py&lt;/code&gt;)&amp;nbsp;&lt;ul&gt;&lt;li&gt;Beyond data access, the blueprint also provide a set of simple tools to manage an IRIS Production environment without the user ever open the Management Portal.&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;Beyond the tools, Prompts were designed as structured workflows including the instructions on how to use the provided tools (for example, the logical steps to create a table importing a .csv file from a specified local folder). Resources are a read-only context providing the AI with system information, such as the IRIS version or namespace details, without requiring an active tool call.&amp;nbsp;&lt;p&gt;This implementation contains a limited number of use cases designed to explain of how to use both the native Python SDK and the Atelier API. Again, this example is not a complete implementation of services but rather a blueprint to be used as a guide to develop your own MCP server, tailored to satisfy the specific needs of your project.&lt;/p&gt;
&lt;p&gt;If you want more information about how this blueprint work, check the related video on YouTube:
&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/h20YIDVeVK4" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;Part 8: Conclusion&lt;/h2&gt;The Model Context Protocol turns InterSystems IRIS from a database the AI knows nothing about into a live system the AI can query, inspect, and even modify on demand through SQL, Globals, ClassMethods, the Atelier API or Interoperability productions. The blueprint that accompanies this article shows how few moving parts are actually needed to get there. From here, every project will pull in different domain logic — your own Business Processes, your own queries, your own globals. But the protocol, the transport, the FastMCP decorators, and the deployment story stay the same.
&lt;h2&gt;Part 9: Sources&lt;/h2&gt;&amp;nbsp;&lt;p&gt;The following list contains all the cited sources&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.anthropic.com/news/model-context-protocol" rel="noopener noreferrer"&gt;Introducing the Model Context Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/specification/latest" rel="noopener noreferrer"&gt;MCP Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/modelcontextprotocol" rel="noopener noreferrer"&gt;MCP GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/docs/getting-started/intro" rel="noopener noreferrer"&gt;What is the Model Context Protocol (MCP)?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/docs/learn" rel="noopener noreferrer"&gt;MCP Architecture overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mcpcat.io/blog/mcp-server-client-host/" rel="noopener noreferrer"&gt;MCP Server, Client and Host&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.jsonrpc.org/" rel="noopener noreferrer"&gt;JSON RPC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/docs/sdk" rel="noopener noreferrer"&gt;MCP SDKs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=PAGE_python_native" rel="noopener noreferrer"&gt;InterSystems Native SDK for Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astral.sh/uv/concepts/projects/dependencies/" rel="noopener noreferrer"&gt;uv Managing dependencies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astral.sh/uv/concepts/build-backend/" rel="noopener noreferrer"&gt;uv Build backend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/mcp/" rel="noopener noreferrer"&gt;PyPI MCP Python SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gofastmcp.com/" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cursor.com/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claude.com/download" rel="noopener noreferrer"&gt;Claude Desktop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://claude.com/product/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openexchange.intersystems.com/package/mcp-server-iris" rel="noopener noreferrer"&gt;mcp-server-iris&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openexchange.intersystems.com/package/intersystems-objectscript-mcp" rel="noopener noreferrer"&gt;intersystems-objectscript-mcp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/esalas-devel/iris-mcp-atelier" rel="noopener noreferrer"&gt;iris-mcp-atelier&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openexchange.intersystems.com/package/gj-::-servAI" rel="noopener noreferrer"&gt;servAI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://openexchange.intersystems.com/package/IRIS-MCP-Server-Suite-2" rel="noopener noreferrer"&gt;IRIS-MCP-Server-Suite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/intersystems-community/ai-hub-eap" rel="noopener noreferrer"&gt;InterSystems IRIS AI Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pypi.org/project/intersystems-irispython/" rel="noopener noreferrer"&gt;PyPI Intersystem IRIS Native Python SDK (intersystems-irispython)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.intersystems.com/iris20261/csp/docbook/DocBook.UI.Page.cls?KEY=GSCF_intro" rel="noopener noreferrer"&gt;Source Code File REST API (a.k.a. Atelier API)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;


</description>
      <category>ai</category>
      <category>productivity</category>
      <category>resources</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Intro to AI Hub, Part 3: Stateful Tools</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 27 Oct 2026 16:42:31 +0000</pubDate>
      <link>https://dev.to/intersystems/intro-to-ai-hub-part-3-stateful-tools-1gpf</link>
      <guid>https://dev.to/intersystems/intro-to-ai-hub-part-3-stateful-tools-1gpf</guid>
      <description>&lt;p&gt;Hi all! This is the third installment in my series introducing the AI Hub, which is currently in an early access program. In this article, we are going into detail on a pattern which I think is super cool: &lt;strong&gt;Stateful Tools&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;As with the previous articles, the following disclaimer applies:&amp;nbsp;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Disclaimer: AI Hub is an early access preview, with features likely to change before production releases, any issues identified can be raised as issues on the documentation GitHub repo linked above. The EAP preview is not to be used in production settings.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;And to give the links again: &lt;a href="https://evaluation.intersystems.com/Eval/early-access/AIHub" rel="noopener noreferrer"&gt;EAP Site&lt;/a&gt; for download, &lt;a href="https://github.com/intersystems-community/ai-hub-eap/tree/master" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;, and to add one new link, there is now a &lt;a href="https://discord.com/channels/698987699406766170/1521152420963418122" rel="noopener noreferrer"&gt;Discord channel&lt;/a&gt; to share ideas and ask questions.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;A recap on %AI.Tool&lt;/h2&gt;
&lt;p&gt;I introduced tools in my first article in this series, but to recap, an AI agent tool is a function which can be directly called by the agent. Large Language Models have been trained to read tool definitions, including a tool name, description and required parameters, and respond in JSON structured output requesting a tool call. The tool then executes and the output is returned to the agent.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Tools are what allows modern AI Agents to actually &lt;em&gt;do&lt;/em&gt; things, rather than just &lt;em&gt;"say"&lt;/em&gt; things. They can be anything, like reading a file, performing a web search, querying a database or something more complex and bespoke like opening a specific application and taking a screenshot. Anything that can be abstracted down to a small number of input parameters, a coded function and an output can be turned into a tool.&lt;/p&gt;
&lt;p&gt;It's super easy to create tools with AI Hub. All you need to do is extend &lt;code&gt;%AI.Tool&lt;/code&gt; and write custom methods, class methods or queries. You can also use &lt;code&gt;%AI.ToolSet&lt;/code&gt; to define and organise tools, add external tools and combine with custom policies.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;What Are Stateful Tools in AI Hub?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;%AI.Tool&lt;/code&gt; and &lt;code&gt;%AI.ToolSet&lt;/code&gt; classes can be &lt;strong&gt;stateful&lt;/strong&gt;, meaning that they remember the state between executions. Information or data can be stored as a &lt;code&gt;Property&lt;/code&gt; of the tool, meaning it can be used or recalled later.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;%AI.Tool&lt;/code&gt; or &lt;code&gt;%AI.ToolSet&lt;/code&gt; class in question is instantiated &lt;em&gt;when an agent is instantiated&lt;/em&gt; and remains alive for the life of the agent. This provides a way for agents to save information which will last across sessions. Any properties are stored and can be accessed by tools which are defined as methods (note, ClassMethods and Query Components can't access instance properties).&lt;/p&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;Providing Tool Memory&lt;/h3&gt;
&lt;p&gt;The example in the documentation shows stateful tools being used for notes that an agent can autonomously read and write. You can do this with the following (&lt;a href="https://github.com/intersystems-community/ai-hub-eap/blob/master/ObjectScript_SDK_Guide.md#stateful-tools-instance-methods" rel="noopener nofollow noreferrer"&gt;adapted from the documentation&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Sample.Memories Extends %AI.Tool&lt;br&gt;
{&lt;br&gt;
Property Memories As %String(MAXLEN = "");

&lt;p&gt;// Ensure the tool is registered to be stateful. This is set by default by the inclusion of instance methods.&amp;nbsp;&lt;br&gt;
PARAMETER STATEFUL=1;&lt;/p&gt;

&lt;p&gt;///  Save to memory&lt;br&gt;
Method SaveToMemory(memory As %String(DESCRIPTION = "Note to store in memory")) As %String&lt;br&gt;
{&lt;br&gt;
Set ..Memories = ..Memories &lt;em&gt;memory&lt;/em&gt;$C(10)&lt;br&gt;
return "Memory"&lt;br&gt;
}&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;/// Recall Saved Memories&lt;br&gt;&lt;br&gt;
Method RecallMemories() As %String&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
Return ..Memories&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;This means the agent can create and retrieve memories which are specific to the agent instance. For example:&amp;nbsp;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set agent = ##class(Sample.Agent).%New()&lt;br&gt;&lt;br&gt;
do agent.%Init()&amp;nbsp;&lt;br&gt;&lt;br&gt;
do agent.ToolManager.AddTool(##class(Sample.Memories).%New())&lt;br&gt;&lt;br&gt;
set s1 = agent.CreateSession()

&lt;p&gt;do agent.Chat(s1, "Write a memory about a trip to Peru to see some llamas to your memory bank")&lt;/p&gt;

&lt;p&gt;kill s1 // Even if we delete the original session, the memory is kept by the tool&lt;/p&gt;

&lt;p&gt;set s2 = agent.CreateSession()&lt;br&gt;
set response = agent.Chat(s2, "Tell me whats in your memory bank")&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;write response.Content&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;Outputted:&amp;nbsp;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Ahoy, matey! Here be what me memory bank holds right now:

&lt;p&gt;Memory log: Peru llama encounter&lt;/p&gt;

&lt;p&gt;We set sail from the coast and dropped anchor in the highlands near Cusco. The air was thin, sharp as a cutlass, and the mountains rose like towering waves. In the Sacred Valley, we found a small herd of llamas on the hillside. Their patient, curious eyes watched us as I approached slowly, offering a bit of maize. A sturdy llama led the way, its bells jangling softly, and a wary cria peeked from behind its mother. I patted the soft fleece and felt a warm shiver of adventure run through my bones. We traded stories with a kindly herder, and I bought a woven scarf as a treasure to remind me of the quiet grace of these beasts. Sunset bled across the peaks, turning the snow-capped summits to gold, and the llamas stood as silent sentinels of the day. Aye, that memory stays with me, a calm voyage ashore in the heart of Peru.&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;If ye want to add more memories, just name the voyage, and I shall chart it in yer memory bank.&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;I guess now would be a good time to mention that I've been playing around with a "&lt;strong&gt;Talk like a Pirate&lt;/strong&gt;" system prompt...&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;Setting properties deterministically&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;Now I don't deny, the saving memories is pretty nice, but you could easily achieve this by setting a global, with the added advantage that the data would persist across agent instances.&lt;/p&gt;
&lt;p&gt;The biggest practical benefit of stateful tools (in my opinion) is that the &lt;strong&gt;properties can also be set deterministically&lt;/strong&gt;. We can hardcode properties in the tool when instantiating a new agent, so tool settings are pre-defined for the agent. This makes it very easy to scope an agent's access.&lt;/p&gt;
&lt;p&gt;For example, let's say you had an agent to query a patient's medical history, you could create a tool which takes in the patient ID as a runtime parameter and trust that the agent will call the tool with the correct ID. But what happens if a malicious actor convinces the agent that it should bypass guardrails and give the medical information for a different patient ID?&lt;/p&gt;
&lt;p&gt;Instead we can set the PatientId for the individual Tool instance, meaning the agent can use the tool, but never choose, or potentially even know, which patient it is using it for.&lt;/p&gt;
&lt;p&gt;To do this, we can create a tool class with a &lt;code&gt;PatientId&lt;/code&gt; property, and one or more methods which use the property:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Sample.PatientQueryTool Extends %AI.Tool&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
Property PatientId;&amp;nbsp;

&lt;p&gt;Method QueryPatientHistory() As %DynamicArray&lt;br&gt;
{&lt;/p&gt;

&lt;p&gt;set sql = "SELECT TOP 20 RecordedAt, EncounterType, Condition, Treatment, Clinician "&lt;br&gt;
            _ "FROM Sample.PatientHistory "&lt;br&gt;
            _ "WHERE PatientId = ? "&lt;br&gt;
            _ "ORDER BY RecordedAt DESC"&lt;/p&gt;

&lt;p&gt;set stmt = ##class(%SQL.Statement).%New()&lt;br&gt;
   set sc = stmt.%Prepare(sql)&lt;br&gt;
   set rs = stmt.%Execute(..PatientId)&lt;/p&gt;

&lt;p&gt;set results = []&lt;br&gt;
   while rs.%Next() {&lt;br&gt;
       do results.%Push({&lt;br&gt;
           "RecordedAt":     (rs.%Get("RecordedAt")),&lt;br&gt;
           "EncounterType":  (rs.%Get("EncounterType")),&lt;br&gt;
           "Condition":      (rs.%Get("Condition")),&lt;br&gt;
           "Treatment":      (rs.%Get("Treatment")),&lt;br&gt;
           "Clinician":      (rs.%Get("Clinician"))&lt;br&gt;
       })&lt;br&gt;
   }&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;quit results&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;Now all we need to do is set the &lt;code&gt;PatientId&lt;/code&gt; when the Agent class is instantiated. We can do this as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set Provider = ...&amp;nbsp;&lt;br&gt;&lt;br&gt;
set agent = ##class(%AI.Agent).%New(Provider)&lt;br&gt;&lt;br&gt;
set tool = ##class(Sample.PatientQueryTool).%New()&lt;br&gt;&lt;br&gt;
set tool.PatientId = 42

&lt;/code&gt;&lt;p&gt;&lt;code&gt;agent.ToolManager.AddTool(tool)&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;And now, this instance of an agent can only access the patient record of the patient in question. No non-deterministic guardrails about the AI only accessing the patient-in-question's record are needed, because the agent's tool &lt;strong&gt;can only access the data for the relevant patient&lt;/strong&gt;. The agent doesn't have setter or getter tools built-in for tool properties, so it cannot see or edit this &lt;code&gt;PatientId&lt;/code&gt; itself.&lt;/p&gt;
&lt;p&gt;One additional thing to be aware of, is that if you wish to use stateful tools like this, you are best to add them as instance objects. That is to say, you can't create an agent class with &lt;code&gt;PARAMETER TOOLS = "Sample.PatientQueryTool";&lt;/code&gt;, you instead need to add the tool after instantiation and property setting. You can do this with something like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Sample.PatientAgent Extends %AI.Agent{

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Method %OnInit() As %Status
{&amp;nbsp;
// Set provider with API key from environment variable&amp;nbsp;
Set key = $System.Util.GetEnviron("OPENAI_API_KEY")&amp;nbsp;// or whatever  
Set ..Provider = ##class(%AI.Provider).Create("openai", {"api_key": (key)})
return $$$OK
}

Method AddPatientTools(pPatientId As %Integer) As %Status
{
    // Check the patientID has been set to a positive integer
    if 'pPatientId || pPatientId'&amp;amp;gt;0 {
        quit $$$ERROR($$$GeneralError, "PatientId must be set to a positive integer.")
    }
    // Create a new instance of our tool class
    Set tool = ##class(Sample.PatientQueryTool).%New()

    // Set the Patient ID on the tools
    set tool.PatientId = pPatientId

    // Add the tools to the agent instance
    do ..ToolManager.AddTool(tool)

    quit $$$OK
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;Then you can instantiate this with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set agent = ##class(Sample.PatientAgent).%New()&lt;br&gt;&lt;br&gt;
do agent.%Init()&lt;br&gt;&lt;br&gt;
do agent.AddPatientTools(42)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And chat with it as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set session = agent.CreateSession()&lt;br&gt;&lt;br&gt;
set question = "Tell me about my recent history"&lt;br&gt;&lt;br&gt;
set response  = agent.Chat(session, question)&lt;br&gt;&lt;br&gt;
write response.Content&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;In this article, I've shown how you can use stateful tools to create memory, or to add deterministic scope for your AI Agent Tools.&lt;/p&gt;
&lt;p&gt;There are so many other uses for these kinds of stateful tools, like caching outputs from one tool (method) to be used by another, providing they are defined in the same class. I'd love to see other cool, innovative uses for stateful tools from anyone who is interested in participating in the next round of the &lt;a href="https://community.intersystems.com/post/community-bounty-program-idea-application-%E2%80%94-round-2-live" rel="noopener nofollow noreferrer"&gt;Community Bounty Program&lt;/a&gt;!&lt;/p&gt;&amp;nbsp;&lt;p&gt;Frequently Asked Questions&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What is a stateful tool in InterSystems IRIS AI Hub?&lt;/li&gt;
&lt;li&gt;A stateful tool is a %AI.Tool or %AI.ToolSet instance that retains property values between executions. The tool instance remains available for the lifetime of the agent instance, so stored information can be reused by later tool calls.&lt;/li&gt;
&lt;li&gt;Can stateful tools provide memory across AI agent sessions?&lt;/li&gt;
&lt;li&gt;Yes. Because the state belongs to the tool instance associated with the agent, information stored in a property can remain available when one session ends and another session is created with the same agent instance. It does not automatically persist across separate agent instances.&lt;/li&gt;
&lt;li&gt;How do stateful tools store and access information?&lt;/li&gt;
&lt;li&gt;Information can be stored in properties of a %AI.Tool or %AI.ToolSet class. Instance methods can read or update those properties, allowing later tool calls to use values saved earlier.&lt;/li&gt;
&lt;li&gt;How can stateful tools restrict an AI agent's access to data?&lt;/li&gt;
&lt;li&gt;A property can be set deterministically when the tool is instantiated. For example, a patient-query tool can be assigned a specific PatientId before it is added to an agent. The tool then queries only that patient's data without requiring the AI agent to select or provide the identifier itself.&lt;/li&gt;
&lt;li&gt;Do stateful tools have uses beyond memory and access scoping?&lt;/li&gt;
&lt;li&gt;Yes. Stateful tools can be used to cache information or outputs from one tool method so they can be reused by another method defined in the same class.&lt;/li&gt;
&lt;/ul&gt;


</description>
      <category>ai</category>
      <category>programming</category>
      <category>tooling</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>DTL Deep Dive: Conditionals on Length, ReplaceStr, Special Character Cleaning, and Timestamp Formatting</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 27 Oct 2026 16:36:33 +0000</pubDate>
      <link>https://dev.to/intersystems/dtl-deep-dive-conditionals-on-length-replacestr-special-character-cleaning-and-timestamp-1mbd</link>
      <guid>https://dev.to/intersystems/dtl-deep-dive-conditionals-on-length-replacestr-special-character-cleaning-and-timestamp-1mbd</guid>
      <description>&lt;p&gt;Hi Community,&lt;/p&gt;

&lt;p&gt;Data Transformation Language (DTL) is one of those tools that appears deceptively simple at first—drag a source field, drop it on a target, and you are done. However, once you start working on real-world integration projects, you quickly realize that raw field mapping only gets you so far. The true challenge begins when you need to validate field lengths, clean up incoming strings, strip unwanted characters from IDs, or produce timestamps in a specific format required by a downstream system.&lt;/p&gt;

&lt;p&gt;In this article, we will move beyond the basics and explore four practical DTL scenarios that occur regularly in real-world integrations. Each section is built around a single sample HL7 message, allowing you to follow along in your own environment and see the results for yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What We Will Cover:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating a DTL from scratch in the Management Portal.&lt;/li&gt;
&lt;li&gt;Conditional logic based on field length (&lt;code&gt;$Length&lt;/code&gt;, &lt;code&gt;$Extract&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Stripping and replacing text using &lt;code&gt;ReplaceStr&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Stripping special characters from a Patient ID with chained &lt;code&gt;ReplaceStr&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reformatting HL7 timestamps into a custom display format.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;InterSystems IRIS or HealthShare with an Interoperability-enabled namespace.&lt;/li&gt;
&lt;li&gt;Access to the Management Portal.&lt;/li&gt;
&lt;li&gt;Basic familiarity with HL7 v2 message structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: While all examples use HL7 v2 ADT messages, the same DTL functions apply equally to XML, JSON, or custom message classes.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is a DTL?
&lt;/h2&gt;

&lt;p&gt;A DTL (Data Transformation Language) in InterSystems IRIS is a class that defines how to map and transform message fields — either between different message formats or within the same message type. You build it visually in the Management Portal, and IRIS generates the underlying ObjectScript code.&lt;/p&gt;

&lt;p&gt;Each transformation consists of actions. The most common ones are the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assign:&lt;/strong&gt; Maps or sets a field value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If: &lt;/strong&gt;Handles conditional branching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code: &lt;/strong&gt;Allows you to write ObjectScript directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foreach: &lt;/strong&gt;Iterates over repeating segments.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Sample HL7 Message
&lt;/h2&gt;

&lt;p&gt;All four scenarios in this article use the same incoming ADT A01 message. Keeping this consistent is vital, since you rarely get to transform only one field at a time in production.&lt;/p&gt;

&lt;p&gt;Here is our sample message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MSH|^~\&amp;amp;|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20250315143022||ADT^A01|MSG00001|P|2.5
EVN|A01|20250315143022
PID|1||MRN#001/2025^^^HOSP||VanDerBerghMontgomeryXYZABC^John^M||19850101|M|||123 Main Street^^Lahore^Punjab^54000^PK||+92-03001234567|||M||ACC001|
PV1|1|I|WARD-A^Room101^Bed1|E|||DOC001^Smith^James|||SUR|||||||V01|ACC001||||||||||||||||||||||||||20250315143022
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Items each field carries that matters for our examples:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PID-5.1&lt;/strong&gt; (Family Name): &lt;code&gt;VanDerBerghMontgomeryXYZABC&lt;/code&gt; (27 characters; intentionally exceeds the 25-character limit).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PID-3.1&lt;/strong&gt; (Patient ID): &lt;code&gt;MRN#001/2025&lt;/code&gt; contains &lt;code&gt;#&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; characters that the target system cannot accept&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PID-13&lt;/strong&gt; (Phone Number): &lt;code&gt;+92-03001234567&lt;/code&gt; (Includes an unwanted country code prefix ).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PV1-44&lt;/strong&gt; (Admit Date/Time): &lt;code&gt;20250315143022&lt;/code&gt; (Standard HL7 DTM format; requires conversion to 15/03/2025 14:30).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: Keep this message open in a text editor as you work through each section.&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating a DTL from Scratch
&lt;/h2&gt;

&lt;p&gt;Before jumping into transformations, let us set up the DTL in which all four examples will live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navigate to the DTL editor:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;Management Portal → Interoperability → Build → Data Transformations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8co9eucdl2tu0nkchvca.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8co9eucdl2tu0nkchvca.png" alt=" " width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;New&lt;/strong&gt; to create a new transformation.&lt;/p&gt;

&lt;p&gt;Fill in the dialog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Package&lt;/strong&gt;: &lt;code&gt;Demo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: &lt;code&gt;ADTTransform&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description&lt;/strong&gt;: DTL deep dive demo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: &lt;code&gt;EnsLib.HL7.Message&lt;/code&gt; set &lt;strong&gt;DocType&lt;/strong&gt; to &lt;code&gt;2.5:ADT_A01&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target&lt;/strong&gt;: &lt;code&gt;EnsLib.HL7.Message&lt;/code&gt; set &lt;strong&gt;DocType&lt;/strong&gt; to &lt;code&gt;2.5:ADT_A01&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxz06gv5ldgpq6x7cmko9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxz06gv5ldgpq6x7cmko9.png" alt=" " width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;OK&lt;/strong&gt;. The visual DTL editor will open with an empty canvas, showing the source on the left and the target on the right.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbu8ke925ht0y7b57yu99.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbu8ke925ht0y7b57yu99.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the top of the canvas you will see a toolbar with action buttons: &lt;strong&gt;Assign&lt;/strong&gt;, &lt;strong&gt;If&lt;/strong&gt;, &lt;strong&gt;Code&lt;/strong&gt;, &lt;strong&gt;Foreach&lt;/strong&gt;, and others. This is where we add each transformation. Click &lt;strong&gt;Save&lt;/strong&gt; once before adding any actions. This compiles the class and avoids losing work.&lt;/p&gt;

&lt;p&gt;Now we are ready to build.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scenario 1: Conditional Logic Based on Field Length
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;The target system only accepts a patient's family name up to 25 characters. Anything longer than that triggers a silent rejection. Our sample message sends &lt;code&gt;VanDerBerghMontgomeryXYZABC&lt;/code&gt; to PID-5.1, which is 27 characters.&lt;/p&gt;

&lt;p&gt;The rule: if the family name exceeds 25 characters, truncate it to 25 and append &lt;code&gt;*&lt;/code&gt; so the receiving team can flag and review it. If it is within the limit, pass it through unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding the If Action
&lt;/h3&gt;

&lt;p&gt;First, set target = source so that the source data is assigned to the target message.&lt;br&gt;
In the DTL canvas, click the &lt;strong&gt;+&lt;/strong&gt; button to add an action and choose &lt;strong&gt;If&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Set the &lt;strong&gt;condition&lt;/strong&gt; to the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$Length(source.{PID:PatientName(1).FamilyName}) &amp;gt; 25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$Length&lt;/code&gt; returns the character count of a string. You can use it directly in DTL conditions without any special wrapper.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmijrwq9mgipzm5xbilnk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmijrwq9mgipzm5xbilnk.png" alt=" " width="799" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Inside the &lt;strong&gt;true&lt;/strong&gt; branch (the &lt;code&gt;If&lt;/code&gt; side), add an &lt;strong&gt;Assign&lt;/strong&gt; action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Property&lt;/strong&gt;: &lt;code&gt;target.{PID:PatientName(1).FamilyName}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value&lt;/strong&gt;: $Extract(source.{PID:PatientName(1).FamilyName},1,25) _ "*"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;$Extract(string, start, end)&lt;/code&gt; pulls characters by position, meaning that &lt;code&gt;$Extract(value,1,25)&lt;/code&gt; takes the first 25 characters. The &lt;code&gt;_ "*"&lt;/code&gt; appends the asterisk using ObjectScript's string concatenation operator.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frdqbmyxh1djjsy9xlc8c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frdqbmyxh1djjsy9xlc8c.png" alt=" " width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;Else&lt;/strong&gt; branch, add another &lt;strong&gt;Assign&lt;/strong&gt; as a passthrough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Property&lt;/strong&gt;: &lt;code&gt;target.{PID:PatientName(1).FamilyName}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value&lt;/strong&gt;: &lt;code&gt;source.{PID:PatientName(1).FamilyName}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdhalcw0rwupsp8a0bprp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdhalcw0rwupsp8a0bprp.png" alt=" " width="799" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing It
&lt;/h3&gt;

&lt;p&gt;Save and compile the DTL.&lt;br&gt;
Paste the sample message and click the &lt;strong&gt;Test&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;In the output message, PID-5.1 should show &lt;code&gt;VanDerBerghMontgomeryXYZAB*&lt;/code&gt; , which is exactly 25 characters plus the asterisk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpanz1j5fi828009135j9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpanz1j5fi828009135j9.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Scenario 2: Using ReplaceStr to Clean Field Values
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;PID-13 (Phone Number) in our sample comes through as &lt;code&gt;+92-03001234567&lt;/code&gt;. The target system only wants the local number, no country code, no dash. We need to strip &lt;code&gt;+92-&lt;/code&gt; from every phone number before passing it on.&lt;/p&gt;
&lt;h3&gt;
  
  
  The ReplaceStr Function
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ReplaceStr&lt;/code&gt; is a static method on the &lt;code&gt;Ens.Util.FunctionSet&lt;/code&gt; class. It takes three arguments: the original string, what to find, and what to replace it with. Passing an empty string as the third argument deletes the matching text.&lt;/p&gt;

&lt;p&gt;You can call it directly inside an Assign value field in the DTL.&lt;/p&gt;
&lt;h3&gt;
  
  
  Adding the Assign Action
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;+&lt;/strong&gt; and add an &lt;strong&gt;Assign&lt;/strong&gt; action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Property&lt;/strong&gt;: &lt;code&gt;target.{PID:PhoneNumberHome(1).TelephoneNumber}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value&lt;/strong&gt;: ##class(Ens.Util.FunctionSet).ReplaceStr(source.{PID:PhoneNumberHome(1).TelephoneNumber},"+92-","")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qiab5u15qty3gz4nkmr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1qiab5u15qty3gz4nkmr.png" alt=" " width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Testing It
&lt;/h3&gt;

&lt;p&gt;Save and compile the DTL.&lt;/p&gt;

&lt;p&gt;Paste the sample message and click the &lt;strong&gt;Test&lt;/strong&gt; button. In the output, PID-13 should show &lt;code&gt;03001234567&lt;/code&gt;; the prefix should be gone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzm22qashufyc692jpqvi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzm22qashufyc692jpqvi.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Another Practical Use
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ReplaceStr&lt;/code&gt; is used not only for removing prefixes. If you need to replace a code value, say your source sends &lt;code&gt;M&lt;/code&gt; or &lt;code&gt;F&lt;/code&gt; for gender, but the target expects &lt;code&gt;Male&lt;/code&gt; or &lt;code&gt;Female&lt;/code&gt;, a Code action with explicit If/ElseIf logic is the cleanest approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set sex = source.{PID:AdministrativeSex}
If sex = "M" { Set target.{PID:AdministrativeSex} = "Male" }
ElseIf sex = "F" { Set target.{PID:AdministrativeSex} = "Female" }
Else { Set target.{PID:AdministrativeSex} = sex }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nested &lt;code&gt;ReplaceStr&lt;/code&gt; calls can work for simple substitutions, but be careful, &lt;code&gt;ReplaceStr(value,"M","Male")&lt;/code&gt; would also match the &lt;code&gt;M&lt;/code&gt; inside &lt;code&gt;Female&lt;/code&gt; on a second pass if you are not careful with ordering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scenario 3: Stripping Special Characters from Patient ID
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Look at PID-3.1 in our sample message. The Patient ID comes through as &lt;code&gt;MRN#001/2025&lt;/code&gt;. The &lt;code&gt;#&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; characters were likely generated by a legacy system that used them internally as separators. The target system expects a clean alphanumeric ID and will reject or misroute the message if these characters are present.&lt;/p&gt;

&lt;p&gt;We need to strip both characters so that &lt;code&gt;MRN#001/2025&lt;/code&gt; can become &lt;code&gt;MRN0012025&lt;/code&gt;. We should use two chained &lt;strong&gt;Assign&lt;/strong&gt; actions here — one for each character to remove.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First Assign — strip the #:&lt;/strong&gt;&lt;br&gt;
Click + and add an Assign action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Property&lt;/strong&gt;: target.{PID:PatientIdentifierList(1).IDNumber}&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value:&lt;/strong&gt; ##class(Ens.Util.FunctionSet).ReplaceStr(source.{PID:PatientIdentifierList(1).IDNumber},"#","")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Second Assign — strip the / from what the first Assign already wrote:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click + and add an Assign action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Property: target.{PID:PatientIdentifierList(1).IDNumber}&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value:&lt;/strong&gt; ##class(Ens.Util.FunctionSet).ReplaceStr(target.{PID:PatientIdentifierList(1).IDNumber},"/","")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: The second Assign reads from the target, not the source, since the first Assign already wrote the #-cleaned value there; now we clean it further.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxolg5d6vor0vzozdjcgo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxolg5d6vor0vzozdjcgo.png" alt=" " width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing It
&lt;/h3&gt;

&lt;p&gt;Save and compile the DTL.&lt;/p&gt;

&lt;p&gt;Paste the sample message and click the &lt;strong&gt;Test&lt;/strong&gt; button. In the output, PID-3.1 should show &lt;code&gt;MRN0012025&lt;/code&gt; with both the &lt;code&gt;#&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; removed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnivpzn1njpxv2mh221wz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnivpzn1njpxv2mh221wz.png" alt=" " width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Scenario 4: Reformatting HL7 Timestamps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;PV1-44 (Admit Date/Time) in our sample comes in as &lt;code&gt;20250315143022&lt;/code&gt;, which is a standard HL7 DTM format (YYYYMMDDHHMMSS). The target system wants a human-readable format: &lt;code&gt;15/03/2025 14:30&lt;/code&gt; (DD/MM/YYYY HH:MM).&lt;/p&gt;

&lt;h3&gt;
  
  
  Why $Extract Works Well Here
&lt;/h3&gt;

&lt;p&gt;HL7 DTM timestamps are fixed-position strings, which makes them ideal for &lt;code&gt;$Extract&lt;/code&gt;. The positions are always the same:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;&amp;nbsp; &amp;nbsp; Characters&lt;/th&gt;
&lt;th&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;How to Extract&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Year&lt;/td&gt;
&lt;td&gt;&amp;nbsp; &amp;nbsp; 1 – 4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$Extract(dt,1,4)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month&lt;/td&gt;
&lt;td&gt;&amp;nbsp; &amp;nbsp; 5 – 6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$Extract(dt,5,6)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day&lt;/td&gt;
&lt;td&gt;&amp;nbsp; &amp;nbsp; 7 – 8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$Extract(dt,7,8)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hour&lt;/td&gt;
&lt;td&gt;&amp;nbsp; &amp;nbsp; 9 – 10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$Extract(dt,9,10)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minute &amp;nbsp;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&amp;nbsp; &amp;nbsp; 11 – 12&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$Extract(dt,11,12)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Adding the Assign Action
&lt;/h4&gt;

&lt;p&gt;Click &lt;strong&gt;+&lt;/strong&gt; at the root level and add an &lt;strong&gt;Assign&lt;/strong&gt; action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Property&lt;/strong&gt;: &lt;code&gt;target.{PV1:AdmitDateTime.Time}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Value: &amp;nbsp;&lt;/strong&gt;$Extract(source.{PV1:AdmitDateTime},7,8)_"/"_$Extract(source.{PV1:AdmitDateTime},5,6)_"/"_$Extract(source.{PV1:AdmitDateTime},1,4)_" "_$Extract(source.{PV1:AdmitDateTime},9,10)_":"_$Extract(source.{PV1:AdmitDateTime},11,12)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3r74skk55y5qvdhy3btl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3r74skk55y5qvdhy3btl.png" alt=" " width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing It
&lt;/h3&gt;

&lt;p&gt;Save and compile the DTL.&lt;/p&gt;

&lt;p&gt;Paste the sample message and click the &lt;strong&gt;Test&lt;/strong&gt; button. PV1-44 in the output should read &lt;code&gt;15/03/2025 14:30&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdyunmtil2i3ka4afo541.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdyunmtil2i3ka4afo541.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;







&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Our final DTL has four action blocks, each targeting a different field in the same ADT A01 message:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;If&lt;/strong&gt; checks &lt;code&gt;$Length&lt;/code&gt; of PID-5.1 and either truncates with &lt;code&gt;*&lt;/code&gt; or passes it through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assign&lt;/strong&gt; uses &lt;code&gt;ReplaceStr&lt;/code&gt; to strip &lt;code&gt;+92-&lt;/code&gt; from PID-13.&lt;/li&gt;
&lt;li&gt;First&lt;strong&gt; Code&lt;/strong&gt; chains two &lt;code&gt;ReplaceStr&lt;/code&gt; calls to remove &lt;code&gt;#&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; from PID-3.1.&lt;/li&gt;
&lt;li&gt;Second&lt;strong&gt; Code&lt;/strong&gt; reformats PV1-44 from HL7 DTM to &lt;code&gt;DD/MM/YYYY HH:MM&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Run the full sample message through the tester one final time and verify all four fields in the output at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwglhwnaa7zmzalebn7e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwglhwnaa7zmzalebn7e.png" alt=" " width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;DTL becomes genuinely powerful once you start combining it with such ObjectScript functions as &lt;code&gt;$Length&lt;/code&gt;, &lt;code&gt;$Extract&lt;/code&gt;, &lt;code&gt;$ZStrip&lt;/code&gt;, and utility methods like &lt;code&gt;ReplaceStr&lt;/code&gt;. The four patterns in this article are not edge cases; they show up on practically every real integration project.&lt;/p&gt;

&lt;p&gt;The habit of building is to test each transformation before it goes anywhere near a live production. The DTL tester handles most of it, and the terminal is always there for quick logic verification when needed.&lt;/p&gt;

&lt;p&gt;Thanks!&lt;/p&gt;




</description>
      <category>dtl</category>
      <category>json</category>
      <category>programming</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Advanced Programmatic FHIR Execution in InterSystems IRIS: A Deep Dive into ObjectScript Integration</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Fri, 23 Oct 2026 16:34:25 +0000</pubDate>
      <link>https://dev.to/intersystems/advanced-programmatic-fhir-execution-in-intersystems-iris-a-deep-dive-into-objectscript-integration-31gh</link>
      <guid>https://dev.to/intersystems/advanced-programmatic-fhir-execution-in-intersystems-iris-a-deep-dive-into-objectscript-integration-31gh</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; InterSystems IRIS for Health lets developers perform FHIR CRUD operations directly from backend ObjectScript routines without relying on an HTTP REST layer. Using&amp;nbsp;HS.FHIRServer.Service, developers can create service contexts, build dynamic queries, validate schemas, and retrieve FHIR resources efficiently.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Traditional FHIR implementations typically process requests through an HTTP/REST pipeline. This article explores what happens when that layer is bypassed, and FHIR operations are executed directly within the IRIS backend, focusing on the architecture, service context, query handling, validation, and resource access involved in this approach.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who This Article Is For:&lt;/strong&gt; This guide is designed for InterSystems IRIS for Health, HealthShare, and Health Connect developers, and health informatics engineers who need to execute low-overhead FHIR operations directly inside backend ObjectScript routines without relying on external HTTP endpoints.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Core Architecture: The Standard Flow vs. Programmatic Execution&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Standard FHIR implementations deployed on InterSystems IRIS for Health, HealthShare, and Health Connect rely on an incoming HTTP pipeline mapped to HS.FHIRServer.Service through the REST application layer. In this standard web architecture, client applications transmit requests over HTTP/HTTPS; the system evaluates security tokens, parses request payloads, and delegates operations down to the storage repository implementation to support Create, Read, Update, and Delete (CRUD) paradigms, along with specialized search capabilities.&lt;/p&gt;
&lt;p&gt;By bypassing the standard web server dispatch layer entirely, ObjectScript routines can instantiate backend service contexts and interact directly with the core engine room of the InterSystems IRIS FHIR repository. This approach lets developers build native query objects, manage sort parameters, validate schemas, and run storage-level searches or read records directly with maximum performance and precise control.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;GET&lt;/code&gt;: Used to read a single resource (&lt;code&gt;GET [base]/Patient/123&lt;/code&gt;) or search a collection using query filters (GET [base]/Patient?gender=male&amp;amp;birthdate=ge1980-01-01).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;POST&lt;/code&gt;: Utilized to create a brand-new resource (&lt;code&gt;POST [base]/Patient&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PUT&lt;/code&gt;: Employed to update an existing resource or upsert it based on an identifier (&lt;code&gt;PUT [base]/Patient/123&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&amp;nbsp;&lt;code&gt;DELETE&lt;/code&gt;: Used to remove or archive a record (&lt;code&gt;DELETE [base]/Patient/123&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;When you throw query parameters into a GET request, you are telling the server how to slice and dice the data. However, how does IRIS process those parameters programmatically when you step away from the browser or Postman? Instead of relying solely on the external HTTP REST layer, you want to execute FHIR operations directly inside your IRIS backend using&amp;nbsp;HS.FHIRServer.Service.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Constructing Query Parameters Programmatically&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;When performing advanced searches internally, managing query parameters cleanly is essential to prevent malformed queries and ensure optimal performance through the&amp;nbsp;JsonAdvSQL execution engine. InterSystems provides&amp;nbsp;HS.FHIRServer.API.Data.QueryParameters for this exact purpose. Depending on where your data originates, you can construct and populate this object using two primary approaches:&lt;/p&gt;
&lt;p&gt;· &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;Approach A: Manual Construction via&amp;nbsp;&lt;strong&gt;AddParameter()&lt;/strong&gt;: If you are dynamically building programmatic search criteria within your application logic (e.g., constructing a query based on internal business rules, user selections in a custom UI, or workflow states), you instantiate the&amp;nbsp;QueryParameters class directly and append filters one by one using the&amp;nbsp;AddParameter() method. This approach gives you absolute control over individual parameter names, modifiers, prefixes, and values before the query ever hits the IRIS storage layer.&lt;/p&gt;
&lt;p&gt;· &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;Approach B: Parsing from a Query String via&amp;nbsp;&lt;strong&gt;CreateFromQueryString()&lt;/strong&gt;: When handling raw HTTP requests, you do not need to parse URL parameters manually. InterSystems IRIS provides a native utility method—CreateFromQueryString()—on the query parameter handler class to automate this transformation seamlessly.&lt;/p&gt;
&lt;h2&gt;Sorting and Linking Data Programmatically&lt;/h2&gt;
&lt;p&gt;Advanced searches require precise control over result ordering and related clinical records through explicit parameter configuration.&lt;/p&gt;
&lt;p&gt;· &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;The Automatic Way: When building standard FHIR applications, developers usually rely on incoming REST URLs. A client might request a list of patients sorted by their last update date in descending order:&amp;nbsp;GET [base]/Patient?gender=female&amp;amp;_sort=-_lastUpdated. When you pass this string into&amp;nbsp;HS.FHIRServer.API.Data.QueryParameters.CreateFromQueryString(), IRIS automatically parses out the&amp;nbsp;_sort parameter.&lt;/p&gt;
&lt;p&gt;· &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;The Manual Way: If you are not using a URL query string, or if you want to intercept a request in your custom interactions class and force a specific sort order regardless of what the user asked for, you can construct&amp;nbsp;.tSortKeys manually. Instead of relying on a string parser, you can build the sort array explicitly in ObjectScript.&lt;/p&gt;
&lt;p&gt;· &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;Forward Links (_include)&amp;nbsp;: They follow references outward from the matching resource to pull related entities (e.g., searching for MedicationRequest resources and including the referenced Practitioner or Patient [MedicationRequest:patient]).&lt;/p&gt;
&lt;p&gt;· &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;Reverse Links (_revinclude): They follow references backward from other resources that point to your match (e.g., searching for a Patient and pulling all Observation or Encounter records that reference that specific patient ID [Observation:patient]).&lt;/p&gt;
&lt;h2&gt;Technical Reference &amp;amp; Implementation Code Examples&lt;/h2&gt;
&lt;p&gt;The following ObjectScript code blocks illustrate how to execute these programmatic workflows directly inside InterSystems IRIS:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;Manual Parameter Construction:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ClassMethod CreateParamsManually() As HS.FHIRServer.API.Data.QueryParameters
{
    Set params = ##class(HS.FHIRServer.API.Data.QueryParameters).%New()
    // Equivalent to filtering by ?gender=male
    Do params.AddParameter("gender", "male")
    // Equivalent to filtering by ?birthdate=ge1980-01-01
    Do params.AddParameter("birthdate", "ge1980-01-01")
    Quit params
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Parsing from a Query String:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ClassMethod CreateParamsFromString() As HS.FHIRServer.API.Data.QueryParameters
{
    Set queryString = "gender=male&amp;amp;name=Smith"
 &amp;nbsp;&amp;nbsp; // Set tQueryString = "patient=123&amp;amp;date=ge2026-01-01&amp;amp;_include=Observation:patient&amp;amp;_count=50"
 &amp;nbsp;&amp;nbsp;&amp;nbsp;// Automatically handles keys, values, prefixes (like 'ge'), and modifiers
    Set params = ##class(HS.FHIRServer.API.Data.QueryParameters).CreateFromQueryString(queryString)
    Return params
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Schema Metadata Validation via&amp;nbsp;FindSearchParam: To prevent runtime errors caused by invalid query criteria or resource-attribute mismatches, the active schema metadata should be inspected before execution.&amp;nbsp;tStrategy.Schema.FindSearchParam(pResourceType, pParamName) acts as a safety check.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ClassMethod ValidateSearchParameter(pStrategy As HS.FHIRServer.Storage.JsonStrategy, pResourceType As %String, pParamName As %String) As %Boolean
{
// Evaluates if the parameter is valid for the specified resource in the active FHIR version schema
    Set searchParam = pStrategy.Schema.FindSearchParam(pResourceType, pParamName)

 &amp;nbsp;&amp;nbsp;&amp;nbsp;If $IsObject(searchParam) {
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     Write "Parameter '", pParamName, "' is valid for resource '", pResourceType, "'.", !
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     Quit 1
    }

 &amp;nbsp;&amp;nbsp;&amp;nbsp;Write "Invalid search parameter for this resource schema.", !
    Quit 0
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Direct Search Execution against the Interactions Engine:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ClassMethod ExecuteDirectSearch(pAppKey As %String) As %Status
{
    Set tSC = $$$OK
    Try {
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     // Instantiate the FHIR Service, which contains the strategy and interactions objects
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     Set fhirService = ##class(HS.FHIRServer.Service).EnsureInstance(pAppKey)
 &amp;nbsp;&amp;nbsp;   
 &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // Access the underlying interactions engine through the strategy
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     Set tInteractions = fhirService.Strategy.GetInteractions()
 &amp;nbsp;&amp;nbsp;   
 &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // Reusing our query parameter builder
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     Set tParams = ##class(HS.FHIRServer.API.Data.QueryParameters).CreateFromQueryString("gender=male")
 &amp;nbsp;&amp;nbsp;   
 &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // Execute the Search method directly against the Interactions class
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     // Parameters: (ResourceType, Compartment, CompartmentId, QueryParameters, .SortKeys)
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     Set tSearchResult = tInteractions.Search("Patient", "", "", tParams, .tSortKeys)
 &amp;nbsp;&amp;nbsp;   
 &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // Iterate through the search results object
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     Write "Total matches found: ", tSearchResult.Total, !
 &amp;nbsp;&amp;nbsp;   
 &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; // Loop through matching resource IDs or entries
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     While tSearchResult.Next(.tId, .tResourceJson, .tStatusCode) {
 &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;     Write "Patient ID: ", tId, " | Details: ", tResourceJson.%ToJSON(), !
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     }
 &amp;nbsp;&amp;nbsp;   
 &amp;nbsp;&amp;nbsp;&amp;nbsp; } Catch tException {
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     Set tSC = tException.AsStatus()
 &amp;nbsp;&amp;nbsp;&amp;nbsp;     Write "Error during search: ", $System.Status.GetErrorText(tSC), !
    }
    Quit tSC
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Direct Resource Retrieval via&amp;nbsp;.Read(): The&amp;nbsp;Read() method on&amp;nbsp;HS.FHIRServer.Storage.JsonAdvSQL.Interactions is the core programmatic function responsible for fetching a specific, individual FHIR resource's JSON data directly from the repository storage layer without running search indexes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set pResourceType = "Patient"
Set resourceId = "1"
// Retrieve the resource as a %DynamicObject directly using interactions.Read()
Set tInteractions = fhirService.Strategy.GetInteractions()
Set resourceString = tInteractions.Read(pResourceType, resourceId)
Write "Patient ID: ", resourceId, " | Resource Data retrieved successfully.", !&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Common Mistakes to Avoid&lt;/p&gt;
&lt;p&gt;· &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;Bypassing Schema Validation: Skipping calls to&amp;nbsp;FindSearchParam() before executing dynamic queries, which can lead to unhandled runtime exceptions when an attribute does not match the active FHIR resource schema version.&lt;/p&gt;
&lt;p&gt;· &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;Hardcoding REST Dependency: Forcing internal microservices or batch engines to route through local loopback HTTP requests instead of calling&amp;nbsp;HS.FHIRServer.Service and&amp;nbsp;GetInteractions() directly in ObjectScript.&lt;/p&gt;
&lt;p&gt;· &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;Improper Result Traversal: Failing to evaluate result status codes properly or using incorrect property lookups when iterating through&amp;nbsp;HS.FHIRServer.Util.SearchResult object loops.&lt;/p&gt;&amp;nbsp;&lt;p&gt;Frequently Asked Questions (FAQ)&lt;/p&gt;
&lt;h3&gt;Q: Can you execute FHIR queries without an HTTP server request?&lt;/h3&gt;
&lt;p&gt;A: Yes. By instantiating&amp;nbsp;HS.FHIRServer.Service and extracting the interactions engine, ObjectScript code can execute searches and read records entirely in-memory.&lt;/p&gt;
&lt;h3&gt;Q: How do you programmatically add&amp;nbsp;_include parameters?&lt;/h3&gt;
&lt;p&gt;A: You use the&amp;nbsp;AddParameter() method on your&amp;nbsp;QueryParameters instance, passing&amp;nbsp;_include as the key and the target reference path (e.g.,&amp;nbsp;Observation:subject) as the value.&lt;/p&gt;
&lt;h3&gt;Q: What is the purpose of&amp;nbsp;HS.FHIRServer.API.Data.QueryParameters?&lt;/h3&gt;
&lt;p&gt;A: It provides a structured mechanism to cleanly build, parse, and encapsulate query criteria, modifiers, and prefixes before execution through the&amp;nbsp;JsonAdvSQL storage layer.&lt;/p&gt;
&lt;h3&gt;Q: How does&amp;nbsp;.Read() differ from&amp;nbsp;.Search()?&lt;/h3&gt;
&lt;p&gt;A: Read() takes an explicit Resource Type and logical ID to fetch a single record instantly as a&amp;nbsp;%DynamicObject, whereas&amp;nbsp;Search() evaluates multi-parameter filters across a collection of resources.&lt;/p&gt;
&lt;h3&gt;Q: How do you handle custom sorting without a URL string?&lt;/h3&gt;
&lt;p&gt;A: You can manually define sort arrays using&amp;nbsp;.tSortKeys alongside schema definitions retrieved via&amp;nbsp;tStrategy.Schema.FindSearchParam().&lt;/p&gt;
&lt;h3&gt;Q: Why is schema validation necessary during programmatic execution?&lt;/h3&gt;
&lt;p&gt;A: It ensures that search keys, attributes, and modifier mappings conform to the active FHIR profile definitions, preventing execution errors.&lt;/p&gt;
&lt;h2&gt;Practical Recommendations&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;Always initialize your runtime context using&amp;nbsp;HS.FHIRServer.Service.EnsureInstance(pAppKey) before extracting the interactions engine. Utilize&amp;nbsp;CreateFromQueryString() when processing incoming text maps, and implement schema validation checks via&amp;nbsp;FindSearchParam to guarantee robust, error-free backend integrations.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;Conclusion&amp;nbsp;&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;By bypassing standard REST routing and programmatically invoking the InterSystems IRIS FHIR interactions engine, developers gain direct, low-overhead access to the core repository. Utilizing manual query parameters for searches and the&amp;nbsp;Read() method for targeted resource retrieval provides the maximum speed, precision, and architectural flexibility required for high-performance backend integrations and custom business logic.&lt;br&gt;&amp;nbsp;&lt;/p&gt;

</description>
      <category>json</category>
      <category>ai</category>
      <category>performance</category>
      <category>programming</category>
    </item>
    <item>
      <title>Connect Microsoft Excel to InterSystems IRIS via ODBC (Windows)</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 13 Oct 2026 16:15:17 +0000</pubDate>
      <link>https://dev.to/intersystems/connect-microsoft-excel-to-intersystems-iris-via-odbc-windows-p1l</link>
      <guid>https://dev.to/intersystems/connect-microsoft-excel-to-intersystems-iris-via-odbc-windows-p1l</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;span&gt;How to create an ODBC connection on your native Windows laptop to IRIS running on a Windows VM on the same computer, test the connection, and pull data from IRIS into Excel.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Recently I learned that Excel can connect to external databases via ODBC. This includes basically any ODBC data source. Since IRIS speaks ODBC via the ODBC API, we can take advantage of the&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href="https://docs.intersystems.com/iris20233/csp/docbook/DocBook.UI.Page.cls?KEY=BNETODBC_intro" rel="noopener noreferrer"&gt;&lt;span&gt;&lt;span&gt;InterSystems ODBC Driver&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;span&gt;&lt;span&gt; to establish an ODBC connection to IRIS on Windows that Excel can utilize.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;strong&gt;Before getting started,&lt;/strong&gt; make sure you have installed the latest InterSystems ODBC driver on your native Windows, which can either be found on WRC or on &lt;/span&gt;&lt;a href="https://github.com/intersystems-community/iris-driver-distribution/tree/main/ODBC" rel="noopener noreferrer"&gt;&lt;span&gt;&lt;span&gt;GitHub&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;span&gt;&lt;span&gt;. I assume that you are running IRIS on a Windows VM using VMWare hosted on your Windows machine. I also assume that you have Microsoft Excel installed on your native Windows as well. You’ll also need some data pre-loaded into your namespace to access. For this post, I am using data from the &lt;/span&gt;&lt;/span&gt;&lt;a href="https://learning.intersystems.com/course/view.php?name=Python%20QS" rel="noopener noreferrer"&gt;&lt;span&gt;&lt;span&gt;Python Quickstart&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;span&gt;&lt;span&gt;. The data can be found on &lt;/span&gt;&lt;/span&gt;&lt;a href="https://github.com/intersystems/Samples-Stock-Data" rel="noopener noreferrer"&gt;&lt;span&gt;&lt;span&gt;GitHub&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;strong&gt;First you need to find the IP address for the VM. &lt;/strong&gt;(Keep in mind that you may need to repeat this step if you restart the VM, connect to a different network or connect/disconnect a VPN connection.)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;To find your IP address in the Windows VM, go to the Windows Command Prompt (cmd):&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;In the VM, hold the Windows key and the R key. This opens the Run dialog (as seen below). Type in “cmd” and click &amp;lt;OK&amp;gt;:&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/10jbj2wcemvvgoroidag.png)
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol start="2"&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;On the Command Prompt, type in “ipconfig -all” to find your IP address. You want to locate the line that says “IPv4 Address….” And make note of the numeric IP address given on that line:&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/m95oqonho5gccomss5ch.png)
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol start="3"&gt;&lt;li&gt;
&lt;span&gt;&lt;span&gt;Also &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;strong&gt;make note of the Web Server Port &lt;/strong&gt;of your VM IRIS, because you’ll need that later.&lt;/span&gt;
&lt;/li&gt;&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Now, r&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;strong&gt;eturn to the Windows host system &lt;/strong&gt;and open the ODBC Data Source Administrator (ODBC DSA) and configure the connection:&lt;/span&gt;&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;In the ODBC DSA, open the “System DSN” tab and click “Add”:&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/f3y8d546u2ehd5ovacof.png)
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol start="2"&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;You will be prompted to select a driver for the data source. If you installed the InterSystems ODBC driver on your native Windows machine, you should select “InterSystems IRIS ODBC…” as I have done below and click “Finish”:&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cf8avvft80fbvg1z9at.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cf8avvft80fbvg1z9at.png" alt=" " width="696" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol start="3"&gt;&lt;li&gt;&lt;span&gt;&lt;span&gt;This will open a new dialog where you need to name the data source, provide a description, input the IP address you copied previously and the Web Server Port. Input the namespace you want to connect to, the User Name and Password that you use for the Management Portal as well. Then click &amp;lt;Test Connection&amp;gt; as shown below:&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcqecxw8qyz7bic6eygr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcqecxw8qyz7bic6eygr.png" alt=" " width="781" height="688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol start="4"&gt;&lt;li&gt;
&lt;span&gt;&lt;span&gt;If all went well, you should see that the &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;strong&gt;test completed successfully&lt;/strong&gt; as shown below. This means that we now have a connection via our native Windows to IRIS on the VM:&lt;/span&gt;
&lt;/li&gt;&lt;/ol&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/wcjyxbt067h201gxjza4.png)
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Now we can &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;strong&gt;open Microsoft Excel&lt;/strong&gt; and access data via the ODBC data source. We do this by selecting the “Data” tab (1), then clicking “Get Data” (2). Click “From Other Sources” (3), and then “From ODBC” (4).&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F36jxflfzr1vgqwnenj9t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F36jxflfzr1vgqwnenj9t.png" alt=" " width="800" height="860"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;From the drop down, select the data source you created previously and click OK:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwjg9p6m9tf2domj70bo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwjg9p6m9tf2domj70bo.png" alt=" " width="749" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;In the Navigator, select the table you want to connect to and click “Load”:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsn6z0phc60aezygllg2k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsn6z0phc60aezygllg2k.png" alt=" " width="800" height="623"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;From here, we can see that we successfully connected to the data on IRIS on a VM and are able to access it in Microsoft Excel running on native Windows:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1w83fgzd90u3z350j92z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1w83fgzd90u3z350j92z.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Wrapping up, we might wonder why we would want to access IRIS data in Excel. Many people have years of Excel expertise. They can use Excel to explore data, create pivot tables, visualizations, and dashboards.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>microsoft</category>
      <category>odbc</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building Branded, Scheduled Reports on InterSystems IRIS with InterSystems Reports: An End-to-End Field Report</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 13 Oct 2026 16:07:23 +0000</pubDate>
      <link>https://dev.to/intersystems/building-branded-scheduled-reports-on-intersystems-iris-with-intersystems-reports-an-end-to-end-371n</link>
      <guid>https://dev.to/intersystems/building-branded-scheduled-reports-on-intersystems-iris-with-intersystems-reports-an-end-to-end-371n</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;TL;DR&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We connected InterSystems Reports, powered by Logi Report from insightsoftware (Designer + Server), to a production InterSystems IRIS instance to deliver branded, scheduled PDF and Excel reports built on IRIS SQL views. This article is the walkthrough we would have liked to have on day one: least-privilege IRIS setup, a reliable JDBC configuration, SQL views as the contract between IRIS and the reporting layer, and branding centralized in CSS plus a template report. Our main takeaway on the tooling itself is that although report files can be edited as code, the Designer UI should be your default — reserve direct file editing for features you already understand deeply. Each step includes the tips that made the setup repeatable for us.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Who This Article Is For&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This guide is for IRIS developers and integration engineers who need to expose IRIS data — in our case,, healthcare remittance data — through a third-party reporting tool, and who want the connectivity, modeling, and templating decisions laid out before they start.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;A Note on Our Approach: Code Editing vs. the Designer&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;InterSystems Reports store their reports, catalogs, and stylesheets as editable files, so everything you can do in the Designer, you can also do in a text editor. So, we deliberately pushed on that to find the limits.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a team that prefers version-controlled, scriptable workflows, this is genuinely appealing, so we explored it: editing report XML directly and maintaining our styling as a hand-written CSS file.&lt;/p&gt;

&lt;p&gt;The conclusion we came to is worth stating up front, because it shapes every recommendation that follows:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Opt for the Designer UI by default.&lt;/strong&gt; Outside of it, behavior is harder to predict: the Designer maintains internal invariants and relationships between attributes that are not obvious from reading a file. Also, a hand-edited file can be structurally valid yet behave differently from one the Designer produced. Keep direct file editing for the pieces you already understand deeply.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;What We Were Building&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The goal was scheduled: brand-consistent client reports generated directly from IRIS production data, with no intermediate data warehouse.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our source data was EDI remittance content (835 / Electronic Remittance Advice) already persisted in IRIS. Our clients needed periodic operational reports — a payer routing summary with a claim-level detail tab and a remittance register — delivered on a schedule rather than run ad hoc from a portal.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What we used&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data platform&lt;/td&gt;
&lt;td&gt;InterSystems IRIS&lt;/td&gt;
&lt;td&gt;Source of truth; SQL views as the reporting contract,&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connectivity&lt;/td&gt;
&lt;td&gt;InterSystems JDBC driver&lt;/td&gt;
&lt;td&gt;Single connection point for the report server,&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Report authoring&lt;/td&gt;
&lt;td&gt;InterSystems Report Designer&lt;/td&gt;
&lt;td&gt;Layout, parameters, grouping, summaries,&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery&lt;/td&gt;
&lt;td&gt;InterSystems Report Server (Windows VM)&lt;/td&gt;
&lt;td&gt;Scheduling, PDF/Excel export, distribution,&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The architecture is deliberately thin: IRIS does the querying, while the reporting tool handles presentation and delivery. Every design decision below originates from the need to keep that separation clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Why InterSystems Report and Not Power BI or Tableau&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;We evaluated Power BI and Tableau alongside InterSystems Report, and chose InterSystems Report mainly because it added the least infrastructure to an environment that IRIS already had.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Four points that influenced our choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First-class IRIS support:&lt;/strong&gt; InterSystems Reports is powered by Logi Report (formerly JReport from insightsoftware) and officially supported by InterSystems IRIS and IRIS for Health. Because of this, the connection path is fully documented and supported, saving us from having to prove it out ourselves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No second database engine:&lt;/strong&gt; Since every tool requires space to store its configuration, catalog, and scheduling state, the InterSystems Reports Server keeps track of its configuration in a system database maintained by an InterSystems IRIS instance. This means that the same instance can act as both the data source and the system database. The alternatives need their own separate engine, which means additional licensing and extra things to back up and patch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery was simple to stand up:&lt;/strong&gt; Scheduled generation and distribution shipped with the report server, and could run on the same VM as everything else. Alternatively, getting comparable unattended delivery working on the other options involved noticeably more moving parts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lowest total cost for our footprint&lt;/strong&gt; (once the extra database and hosting for the alternatives were calculated).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Power BI and Tableau are stronger for interactive, self-service exploration. Our requirement, however, was the opposite — pixel-controlled, branded documents produced on a schedule — which is exactly where a banded, page-oriented report tool fits best.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 1: Granting the Reporting User Least-Privilege Access in IRIS&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Since the reports read from a production instance, we needed to grant the smallest set of privileges that would still let a report run — nothing more.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This turned out to be a short list, which was worth setting up deliberately rather than starting extensive trimming later. The minimum that worked for us included the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A dedicated user&lt;/strong&gt; exploited only by the report server'sconnection pool without interactive or Management Portal usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One role&lt;/strong&gt; holding read access to the &lt;code&gt;%DB_&amp;lt;resource&amp;gt;&lt;/code&gt; of the database behind the namespace.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SELECT&lt;/code&gt;&lt;strong&gt; on the specific views the reports consume&lt;/strong&gt;, rather than on the whole schema, so new tables added later are not automatically exposed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;/strong&gt;&lt;code&gt;%Service_SQL&lt;/code&gt;&lt;strong&gt; service that is enabled&lt;/strong&gt; for that user, which is the SuperServer/JDBC path the report server uses.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is all a report needs: no &lt;code&gt;%All&lt;/code&gt;, no &lt;code&gt;%Development&lt;/code&gt;, no write privileges anywhere.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; the database resource name is not necessarily derived from the namespace name — a namespace called &lt;code&gt;REPORTING&lt;/code&gt; can sit on a database whose resource has an entirely different name. Look up the actual resource in the Management Portal (System Administration → Configuration → Namespaces, then check the database behind it) and grant permissions on that exact &lt;code&gt;%DB_*&lt;/code&gt; name. Confirming it up front is the difference between a five-minute setup and a puzzling &lt;code&gt;Access Denied Error&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The JDBC detail worth writing on a sticky note:&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Use the loopback &lt;strong&gt;IP address&lt;/strong&gt;, not the hostname:&lt;/p&gt;

&lt;p&gt;# Reliable&lt;br&gt;
jdbc:IRIS://127.0.0.1:1972/MYNAMESPACE&lt;/p&gt;

&lt;p&gt;# Can fail depending on host resolution order&lt;br&gt;
jdbc:IRIS://localhost:1972/MYNAMESPACE&lt;/p&gt;

&lt;p&gt;Driver class: &lt;code&gt;com.intersystems.jdbc.IRISDriver&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The reason matters: On a dual-stack Windows host, &lt;code&gt;localhost&lt;/code&gt; can resolve to the IPv6 loopback (&lt;code&gt;::1&lt;/code&gt;) while the SuperServer is listening on IPv4, so the connection attempt never reaches the listener. The symptom looks like a plain refused connection, with nothing pointing at the address family issues. Specifying &lt;code&gt;127.0.0.1&lt;/code&gt; removes the ambiguity entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checklist of things to verify if the connection is hard to troubleshoot:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SuperServer port reachable (&lt;code&gt;1972&lt;/code&gt; by default)&lt;/li&gt;
&lt;li&gt;Driver JAR on the report server's classpath, matching the Java version in use&lt;/li&gt;
&lt;li&gt;Loopback specified as an IP&lt;/li&gt;
&lt;li&gt;Reporting role holds the correct &lt;code&gt;%DB_*&lt;/code&gt; resource&lt;/li&gt;
&lt;li&gt;Namespace name spelled exactly as IRIS reports it&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 2: Let SQL Views Be the Contract&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pointing a reporting tool at views instead of tables or persistent classes was the single most highest-leverage decision we made.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We defined views in IRIS that shape the remittance data into report-ready rows, then had Logi import only those. A simplified version of the pattern is as follows:&lt;/p&gt;

&lt;p&gt;Class Remit.View.Documents Extends %Persistent [ ClassType = view,&lt;br&gt;
SqlTableName = Documents, ViewQuery = { ... } ]&lt;br&gt;
{&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Why this helped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Report layouts stop depending on physical structure.&lt;/strong&gt; Storage or class changes stay inside the view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business logic lives in IRIS&lt;/strong&gt;, where it can be easily tested with plain SQL, instead of remaining inside report expressions that are harder to inspect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Column names become presentation-friendly&lt;/strong&gt; only once, rather than being aliased in every report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex navigation stays server-side.&lt;/strong&gt; Correlated subqueries and &lt;code&gt;-&amp;gt;&lt;/code&gt; implicit-join syntax are far easier to reason about in a view than in a report designer's query builder.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 3: Running the Report Server as a Windows Service&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Install the report server as a Windows service so that scheduled runs survive reboots and logoffs, and if it behaves inconsistently under the service wrapper, reinstall it in interactive mode.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The report server will ship a service utility, and the install command will accept an &lt;code&gt;-interactive&lt;/code&gt; option:&lt;/p&gt;

&lt;p&gt;JRService -install -interactive&lt;/p&gt;

&lt;p&gt;What the option does: Installing with &lt;code&gt;-interactive&lt;/code&gt; alongside &lt;code&gt;-install&lt;/code&gt; registers the service in interactive mode, so a Command Prompt window appears when the service starts. Without it, the server runs silently, and its console output is not shown anywhere conveniently. That visibility is the practical value — startup messages, classpath problems, and licensing or port complaints appear as the server emits them, rather than having to be inferred from a service that simply did not come up. If a service-hosted server starts behaving differently than the same server launched from a console, reinstalling this way is a quick, reversible thing to try.&lt;/p&gt;

&lt;p&gt;The lesson generalizes beyond this product: when a Java-based server behaves differently as a service than it does interactively, look at the registration flags, the service account, and the JVM the service actually resolves before changing anything in the application configuration.&lt;/p&gt;

&lt;p&gt;Two more environment notes worth setting up deliberately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If &lt;/strong&gt;&lt;code&gt;JAVA_HOME&lt;/code&gt;&lt;strong&gt; inheritance gives you trouble, pin the JDK path explicitly.&lt;/strong&gt; The service reads its JVM path and Java command line from &lt;code&gt;NTService.ini&lt;/code&gt; in the install &lt;code&gt;bin&lt;/code&gt; folder. Set the &lt;code&gt;JavaVM&lt;/code&gt; parameter to the exact JDK you intend to use, rather than depending on what the service account inherits. The same file is stored where heap size lives, so it is worth knowing regardless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the IRIS JDBC JAR in the server's own library folder&lt;/strong&gt; so upgrades do not silently orphan it.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; A service-hosted server cannot reach mapped network drives because of a JVM limitation. If reports read from or write to a network location, use a UNC path rather than a mapped drive letter.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 4: Binding Data Inside the Report&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Most "cannot find dataset" errors come from binding the dataset to a nested component instead of the report body container.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reading the report XML makes this requirement visible: the dataset element must be present on the &lt;code&gt;ReportBody&lt;/code&gt; container itself.&lt;/p&gt;

&lt;p&gt;&amp;lt;ReportBody name="Body"&amp;gt;&lt;br&gt;
&amp;lt;dataSet name="DS_Documents"/&amp;gt;&lt;br&gt;
&amp;lt;!-- tables, banded panels, charts --&amp;gt;&lt;br&gt;
&amp;lt;/ReportBody&amp;gt;&lt;/p&gt;

&lt;p&gt;If it appears only on the inner table, the report can fail at pre-initialization before anything renders — an error that reads like a missing resource rather than a placement issue.&lt;/p&gt;

&lt;p&gt;Two more binding behaviours worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;additionalMappingNames&lt;/code&gt;&lt;strong&gt; uses a counted, delimited format&lt;/strong&gt; (a count followed by each name in sequence). It is precise and unforgiving: if the count drifts out of step with the list, the failure tends to surface later and less obviously. This is a good example of an attribute to set in the Designer rather than by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A summary's reset level controls aggregation scope, not the location where the value appears.&lt;/strong&gt; A sum placed in a group footer will still total the whole report unless its reset level is set to the grouping field. Wrong-looking totals in the right-looking place almost always mean reset level issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;When you change a view, refresh the metadata:&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Adding a column to an IRIS view does not automatically propagate to the report catalog. The column metadata has to be re-imported, and in our environment, a server or connection-pool restart was needed before the new column appeared in running reports. Building "refresh catalog metadata" into our schema-change routine removed a recurring source of confusion where the database and the report clearly disagreed.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 5: Brand Consistency Without Copy-Paste&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reusable branding came from two artifacts — a CSS file for tokens and a template report for structure — not from styling each report manually.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A brand CSS file&lt;/strong&gt; defining classes for our palette and type scale, appended to the default stylesheet the report server serves. We can change colors and fonts in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A template shell report&lt;/strong&gt; ccontaining the page header and footer with report title, date period, and run timestamp in the header (address found on the left, page number in the center, and company URL in the right part of the footer). New reports start as copies of the shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registered custom fonts&lt;/strong&gt; referenced consistently by their registered names. Register your TrueType fonts in the report server's &lt;code&gt;font&lt;/code&gt; directory &lt;strong&gt;and&lt;/strong&gt; on every Designer machine. If a font is missing at render time, the output falls back to a substitute and the layout shifts, which is easy to miss until a client points at a PDF that looks slightly off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We initially looked at XSD-based style groups for this, since they promise exactly this kind of centralized styling. However, since that mechanism is deprecated in current versions, we moved to CSS plus the template shell, which turned out to be simpler to review and version anyway. It is worth checking the deprecation status of any styling feature before building a system on it.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Step 6: Parameterize for Scheduling, Not for Clicking&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reports designed for scheduling need parameters with sensible defaults and no reliance on user interaction.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every report shares the same parameter shape: client, date range, and entity identifier. Because schedules are interchangeable, adding a new client only requires a new schedule rather than a new report. Design implications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give every parameter a valid default so an unattended run will never block on input.&lt;/li&gt;
&lt;li&gt;Echo the parameter values into the header so a delivered PDF is self-describing.&lt;/li&gt;
&lt;li&gt;Prefer date ranges computed relative to run time over hard-coded windows.&lt;/li&gt;
&lt;li&gt;Test the scheduled path early since it exercises a different code path than the Designer preview.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Common Mistakes&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pitfalls worth avoiding:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Granting namespace access without the underlying &lt;code&gt;%DB_*&lt;/code&gt; database resource.&lt;/li&gt;
&lt;li&gt;Editing counted-and-delimited XML attributes by hand.&lt;/li&gt;
&lt;li&gt;Assuming a group footer position implies group-level aggregation.&lt;/li&gt;
&lt;li&gt;Expecting IRIS schema changes to appear in the report catalog automatically.&lt;/li&gt;
&lt;li&gt;Building on a deprecated styling mechanism.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Frequently Asked Questions&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Q: Do I need a separate reporting database or read replica?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A: Not necessarily. We report directly from production through read-only views and scheduled off-peak runs. If report queries grow heavy, an IRIS mirror or shard is the natural next step. Starting simple, however, avoids creating infrastructure we do not need yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Q: Why do I get "Access Denied" when my credentials are correct?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A: Most often because the role lacks the &lt;code&gt;%DB_*&lt;/code&gt; resource for the database behind the namespace. Namespace and resource names are independent. Confirm the resource name in the Management Portal.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Q: Why does &lt;/strong&gt;&lt;code&gt;localhost&lt;/code&gt;&lt;strong&gt; fail in my JDBC URL when the instance is running?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A: Because it may resolve to the IPv6 loopback while the listener is bound to IPv4. Use &lt;code&gt;127.0.0.1&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Q: Should I edit report files as XML or use the Designer?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A: Use the Designer by default. The files are editable, and reading them is genuinely useful for understanding how a report is wired. However, behavior outside the Designer is less predictable. It maintains invariants between attributes that are not visible in the file. Reserve direct editing for things you already understand deeply.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Q: My report shows the wrong totals. Where do I look first?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A: At the summary's reset level. Panel placement affects where a value prints; reset level determines what it aggregates.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Q: I added a column to my view, and the report cannot see it. Why?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A: The report catalog caches column metadata. Re-import the view's metadata and, if needed, restart the server or connection pool so running reports pick up the change.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Q: How do we keep dozens of reports visually consistent?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A: A CSS file for color and type tokens, plus a template report carrying the page header and footer. Since new reports are copies of the template, structure and branding are inherited rather than rebuilt.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Practical Recommendations&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you are starting a similar project, this is the order we recommend you adopt:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prove connectivity first&lt;/strong&gt; with a trivial one-column view before any layout work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design the views before the reports.&lt;/strong&gt; The view list is your reporting API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the branded template shell second&lt;/strong&gt;, so the first real report is already correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Externalize color and type into CSS&lt;/strong&gt; from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardize the parameter signature&lt;/strong&gt; across reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test every export format and the scheduled path&lt;/strong&gt; before showing anything to a client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write down the environment specifics&lt;/strong&gt;: driver path, JDK path, font directory, and resource names. In the future, you will need them at an inconvenient moment.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Connecting a third-party reporting tool to InterSystems IRIS is mostly a modeling and configuration exercise, not a rendering one. Once the security resources, JDBC details, and view layer are right, the reporting tool does what it is good at, and IRIS keeps doing what it does best. The details that matter are specific and learnable — resource names, host resolution, dataset placement, metadata caching — and each one becomes a checklist item rather than a recurring problem. We recommend spending the first day on the views and the branded template since, with those two in place, every report after the first one is mostly layout.&lt;/p&gt;

&lt;p&gt;Having tried a few other solutions, what stands out about InterSystems Report is how approachable it makes complex queries in a low-code way. It assumes a baseline understanding of SQL, but beyond that, the deeper concepts (grouping, nested groups, aggregation, and reset scopes) are unusually easy to reason about once you see how they are expressed in the Designer. That combination is what made it a good fit for reports that are structurally rich but need to be maintained by more than one person.&lt;/p&gt;

&lt;p&gt;A practical tip for getting started: &lt;strong&gt;build a plain table first.&lt;/strong&gt; A table shows you exactly what the query returned, so you can confirm that the data is what you expect before any presentation decisions are made. Once the table is right, base everything else (charts, summaries, grouped layouts) on that same verified dataset. It is a much shorter path than debugging a chart that is beautifully drawing the wrong numbers.&lt;/p&gt;

&lt;p&gt;The one area where we would love to see the product evolve is styling: &lt;strong&gt;a little consolidation there would go a long way.&lt;/strong&gt; Today, it is spread across several models: some styling is standard CSS, some uses Logi-specific class and property naming, and some deprecated mechanisms still appear in the UI. This makes it hard to tell which approach is current. There is also no first-class way to save a report's look as a reusable template; the practical workaround is to copy an existing report and build the new one on top of it, which works but means visual standards live in a file rather than in something explicitly designed to be shared. A single, clearly documented styling path plus a native template object would make brand consistency across a report suite considerably easier to achieve and to hand over. Everything we needed was reachable — it just took some exploration to find the right combination, and that is exactly the kind of thing better defaults could solve.&lt;/p&gt;

&lt;p&gt;The setup has been stable in production, and we are glad to compare notes with anyone building something similar.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>css</category>
      <category>debugging</category>
      <category>security</category>
    </item>
    <item>
      <title>Complementing IRIS with MLflow for a Continuous Training (CT) pipeline</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Wed, 30 Sep 2026 15:23:05 +0000</pubDate>
      <link>https://dev.to/intersystems/complementing-iris-with-mlflow-for-a-continuous-training-ct-pipeline-3h0e</link>
      <guid>https://dev.to/intersystems/complementing-iris-with-mlflow-for-a-continuous-training-ct-pipeline-3h0e</guid>
      <description>&lt;p&gt;A Continuous Training (CT) pipeline formalises a Machine Learning (ML) model developed through data science experimentation, using the data available at a given point in time. It prepares the model for deployment while enabling autonomous updates as new data becomes available, along with robust performance monitoring, logging, and model registry capabilities for auditing purposes.&lt;/p&gt;

&lt;p&gt;InterSystems IRIS already provides nearly all the components required to support such a pipeline. However, one key element is missing: a standardised tool for model registry. In this article, I present &amp;nbsp;&lt;a href="https://github.com/JorgeIvanJH/IRIS_and_MLflow-Continuous-Training-Pipeline/tree/v0.1.0" rel="noopener noreferrer"&gt;this approach (v0.1.0)&lt;/a&gt;, which combines the strengths of IRIS with the open-source AI engineering platform &lt;a href="https://mlflow.org/docs/latest/ml/" rel="noopener noreferrer"&gt;MLflow&lt;/a&gt;. Together, they act as complementary tools for building an effective Continuous Training (CT) pipeline.&lt;/p&gt;

&lt;p&gt;The implementation in this repo leverages MLflow's built-in configuration to store &lt;a href="https://shap.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;SHAP&lt;/a&gt; explainers to provide explanations behind the predictions made by the corresponding model made at the time, including "black-box" complex ones such as Random Forest, XGBoost, Neural Networks, etc.&lt;/p&gt;

&lt;p&gt;Demo: &lt;a href="https://youtu.be/qLdc4jhn83c" rel="noopener noreferrer"&gt;https://youtu.be/qLdc4jhn83c&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CT Pipeline Components
&lt;/h2&gt;

&lt;p&gt;The theory behind the modules of this CT pipeline is based on the industry standard for MLOps level 1 defined by Google in &lt;a href="https://docs.cloud.google.com/architecture/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning?hl=en" rel="noopener noreferrer"&gt;this article&lt;/a&gt;, and the implementation of each of its components leverages the best features of both IRIS and MLflow, highlighted in red as seen in the image below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1vffylve1klg4luvyvid.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1vffylve1klg4luvyvid.png" alt=" " width="799" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For those new to CT pipelines, the image above describes how a traditional experimentation phase of a Data Science project (upper section "experimentation/development/test"), usually done in Jupyter notebooks, is transformed into a production-grade deployment of the developed model that allows continuous monitoring of performance over time, and automatic retraining whenever model performance degrades. All this with proper model versioning and logging for auditing purposes.&lt;/p&gt;

&lt;p&gt;We delve into the details on the &lt;a href="https://github.com/JorgeIvanJH/IRIS_and_MLflow-Continuous-Training-Pipeline/blob/main/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt; of the repo, but for early understanding of each component, we briefly define below what each one does, and how that relates to the IRIS/MLflow tool chosen for that purpose.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Feature Store: Is the single source of truth for sourcing the data, and where every constant parameter or definition related to the data itself is defined, which might vary across clients and use cases (e.g. each client might define readmission after 15, 30, or any other number of days. Late arrival to an appointment might be considered arriving after 5, 10, or any other number of minutes). IRIS SQL Tables' multidimensional globals allow high-speed storage, and stored computed properties ease the definition of custom properties along with the raw data itself.&lt;/li&gt;
&lt;li&gt;Automated Pipeline: Is the formalized and clearly modularized version of the "orchestrated experiment" that's usually done in a jupyter notebook, prepared to be executed every time the model has to be retrained if required. It contains every data and model-training process needed to get the model with the best overall performance. In this section, every constant related to the model itself chosen during the experimentation phase (previously done by data scientists in jupyter notebooks) is defined (e.g. seed, test size, K-folds for validation, etc). In our implementation, we leverage embedded python to easily access IRIS classes directly, along with all required standard Machine Learning python libraries (Pandas, scikit-learn, MLflow, etc).&lt;/li&gt;
&lt;li&gt;Model Registry: During Training, each model that is trained is logged into the MLflow's backend registry, automatically configured when building the project. We can at any moment re-download models from there and query performance of previous models.&lt;/li&gt;
&lt;li&gt;Trained Model: Though the MLflow backend has an Artifact Store where the weights of all trained models are stored, this project additionally saves the artifacts directly into a persistent location (Docker volume) for quick loading when needed. In case these are deleted, they are downloaded and re-saved in the same location from the MLflow Artifact Store.&lt;/li&gt;
&lt;li&gt;Model Serving: This block is in charge of managing the model that is served to production. We store the path to the model's artifacts to be used in production in an IRIS global, which is what gets updated whenever necessary. In this repo we directly promote the new model if it performs better than the previous one, but in a real scenario this might require human approval. We decided to store the path to the model in a global and not the model itself because doing that would imply additional processing time for serialization and deserialization of the python object, whereas text is faster and straightforward to read from a global regardless of classes in the whole repository, making it easy and fast to read anywhere.&lt;/li&gt;
&lt;li&gt;Prediction Service: Is the actual service that a client would use to request inferences on the current model set in production. At the moment this repo uses an embedded python method for this, but this block can potentially be improved by transforming the model into PMML format to make the production service executable using Integrated ML for any sklearn models, or any python model like LightGBM using the upcoming IntegratedML custom models in IRIS 2026.1.&lt;/li&gt;
&lt;li&gt;Performance Monitoring: Is any sort of monitoring that can be made implemented to keep track of the performance of the current model in production, along with previous ones if necessary. For this we leverage MLflow's UI, where we can make custom plots with any of the variables logged, datetime, performance metrics, both for each model and for all models historically trained.&lt;/li&gt;
&lt;li&gt;Trigger: Is whatever activates the execution of the Automated Pipeline. It can be data drift, model degradation beyond a certain threshold, availability of a certain amount of new data with enough ground truth, or a simple periodic schedule. In this project we directly compare against a defined threshold of the R² metric, so every time performance falls below the value in MLpipeline.PerformanceMonitoring.R2THRESHOLD, we execute the automated pipeline. For this task, another valid approach would be to use the task manager to schedule a method that looks at the performance registered during performance monitoring in MLflow tracking, and decide whether to retrain a new model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The addition of MLflow enables straightforward historical performance monitoring of models over time through a single, locally hosted UI in a Docker container. It allows you to store and access artifacts from all previously trained models, including those deployed to production. These artefacts can be downloaded whenever needed, along with standard and custom performance metrics and custom visualisations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2cm6bfm7o7kfjhwtvoi5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2cm6bfm7o7kfjhwtvoi5.png" alt=" " width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04k94cymyebnayxwlzac.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F04k94cymyebnayxwlzac.png" alt=" " width="799" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For each run associated with a model, it is also possible to log additional artifacts such as SHAP explainers, custom plots, and user-defined metrics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F86h6b8f8lyz0r9qge21h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F86h6b8f8lyz0r9qge21h.png" alt=" " width="800" height="793"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo81a8qlguquhkpr1vydn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo81a8qlguquhkpr1vydn.png" alt=" " width="800" height="199"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;The implementation of this CT pipeline leverages &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCM_structuredlog" rel="noopener noreferrer"&gt;Structured Logging&lt;/a&gt; to separate the logs from this pipeline from the rest of the system's, into a persistent path inside the volume of the container hosting IRIS.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In the future, this pipeline can leverage the newly released feature in IRIS 2026.1: IntegratedML Custom Models, to make the prediction service easier through SQL commands, regardless of the model (added support for LightGBM, NNs, XGBoost, etc).&lt;/li&gt;
&lt;li&gt;Right now, the model is only updated automatically by retraining on new data, but the hyperparameters remain static. By following the instructions in &lt;a href="https://mlflow.org/docs/latest/ml/getting-started/hyperparameter-tuning/" rel="noopener noreferrer"&gt;this guide&lt;/a&gt; this pipeline could be improved by introducing hyperparameter flexibility and allowing the model to be re-optimised using the open-source hyperparameter optimisation framework &lt;a href="https://optuna.org/" rel="noopener noreferrer"&gt;Optuna&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;In this project, when prediction performance falls below a predefined threshold, the model is retrained and automatically updated. However, in some cases, human approval should be required before making changes in production, which is something that should be considered in a real-life scenario.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>docker</category>
      <category>machinelearning</category>
      <category>integratedml</category>
    </item>
    <item>
      <title>Understanding InterSystems IRIS Architecture</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Mon, 28 Sep 2026 15:09:22 +0000</pubDate>
      <link>https://dev.to/intersystems/understanding-intersystems-iris-architecture-1b6c</link>
      <guid>https://dev.to/intersystems/understanding-intersystems-iris-architecture-1b6c</guid>
      <description>&lt;p&gt;When people first hear about InterSystems IRIS, they often get overwhelmed by the sheer amount of information surrounding this technology. It is described simultaneously as a data platform, a database, an interoperability engine, an analytics platform, and even an application development platform. Of course, it is easy to get lost in all of this. Unlike many technology stacks where you must assemble multiple products — a database, an integration engine, an API server, a machine learning platform, and reporting tools — InterSystems IRIS actually brings these capabilities together into a single runtime. Rather than learning half a dozen separate products, you are simply learning the different capabilities of one unified platform.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fquqqqf9a9rj366zz0fus.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fquqqqf9a9rj366zz0fus.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is precisely why IRIS (or rather Caché at that point) was chosen as a good example for my subject - to show students different things without the&amp;nbsp;need to learn different software. Besides, instead of moving information between separate products, all components work directly with the same underlying data.&lt;br&gt;In this article, I will try to explain the architecture of InterSystems IRIS from a developer's perspective. Rather than diving into&amp;nbsp;low-level implementation details, we will look at the major building blocks, how they fit together, and the kinds of applications they are designed to support.&lt;/p&gt;
&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;h3&gt;Multi-Model Data Engine&lt;/h3&gt;
&lt;p&gt;Every application starts with data. At the core of InterSystems IRIS is an extremely high-performance multi-model database engine supporting the following data models:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hierarchical&lt;/li&gt;
&lt;li&gt;Key-value&lt;/li&gt;
&lt;li&gt;Object&lt;/li&gt;
&lt;li&gt;Document (JSON)&lt;/li&gt;
&lt;li&gt;Relational&lt;/li&gt;
&lt;li&gt;Columnar&lt;/li&gt;
&lt;li&gt;Vector&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Internally, all of these models are simply different ways of viewing the same stored data. You can read more about the data models in my &lt;a href="https://community.intersystems.com/post/intersystems-iris-data-models-explained-hierarchical-key-value-object-document-relational-and" rel="noopener noreferrer"&gt;previous article&lt;/a&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;This data engine forms the foundation for every other subsystem in IRIS.&lt;/p&gt;
&lt;h3&gt;Interoperability&lt;/h3&gt;
&lt;p&gt;One of the best-known parts of InterSystems technology is its interoperability engine.&lt;/p&gt;
&lt;p&gt;Real-world systems rarely exist in isolation. Applications must constantly exchange data using the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REST&lt;/li&gt;
&lt;li&gt;SOAP&lt;/li&gt;
&lt;li&gt;HTTP&lt;/li&gt;
&lt;li&gt;TCP&lt;/li&gt;
&lt;li&gt;Files&lt;/li&gt;
&lt;li&gt;Message queues&lt;/li&gt;
&lt;li&gt;Healthcare standards&lt;/li&gt;
&lt;li&gt;Custom protocols&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The interoperability engine provides the infrastructure to receive, transform, route, and deliver these messages. Instead of writing networking code repeatedly, developers configure productions composed of services, processes, and operations.&lt;/p&gt;
&lt;p&gt;A typical flow might look like the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrnvt5tc4h9reylyxxv3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffrnvt5tc4h9reylyxxv3.png" alt=" " width="800" height="1200"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inbound Adapter&lt;/strong&gt;: Connects to an external system or data source and receives incoming messages, files, or requests into the inte&lt;a href="https://dev.tourl"&gt;&lt;/a&gt;roperability production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Service&lt;/strong&gt;: Accepts data from the inbound adapter, validates or preprocesses it, and converts it into an internal message for further processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Transformation&lt;/strong&gt;: Maps and converts data from one format or schema into another so the next component or target system can understand it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Process&lt;/strong&gt;: Implements the workflow or business logic, determining how messages should be processed, routed, or orchestrated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Operation&lt;/strong&gt;: Sends the processed message to its destination, such as another application, a database, a web service, or a messaging system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbound Adapter&lt;/strong&gt;: Handles the low-level communication with the target system using the appropriate protocol (e.g., HTTP, TCP, FTP, JDBC, or HL7).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;IRIS includes numerous built-in adapters for common communication protocols together with graphical tools for building integration pipelines. For developers building enterprise software, this means a massive portion of the coding is already done!&lt;/p&gt;
&lt;h3&gt;Analytics&lt;/h3&gt;
&lt;p&gt;Modern applications need to do more than just store data - they need to extract insights from it. While many technology stacks rely on separate analytics platforms, InterSystems IRIS integrates analytics directly into the core platform. This allows developers to query, analyze, and visualize operational data without moving it into another system. Depending on the edition, this includes the following capabilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IRIS BI&lt;/strong&gt; (Business Intelligence): Enables developers and business users to build interactive dashboards, perform multidimensional analysis, and explore data from different perspectives to identify trends and support decision making.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;InterSystems Reports&lt;/strong&gt;: Provides a flexible reporting environment for designing, generating, and distributing professional reports in various formats, making it easy to present business data to end users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Columnar Storage&lt;/strong&gt;: Complements the transactional row-based storage by organizing analytical data in a column-oriented format. This significantly improves the performance of large-scale aggregations and reporting queries without impacting day-to-day transactional workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector Search&lt;/strong&gt;: Enables semantic search by storing and querying vector embeddings alongside your operational data. Instead of searching for exact keywords, it finds information based on meaning, enabling retrieval of conceptually similar documents even when they do not share the same wording.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These components work together to support everything from traditional business reporting to modern AI-powered applications. Rather than exporting information into a separate analytics database, applications can analyze operational data directly. This architecture reduces latency and avoids the need to maintain duplicate datasets. Also, IRIS integrates seamlessly with existing data and application technologies, including popular third-party BI tools, such as Tableau, Looker, Qlik, Microsoft Power BI, and Excel.&lt;/p&gt;
&lt;h3&gt;AI and ML&lt;/h3&gt;
&lt;p&gt;Rather than requiring a collection of specialized databases and AI services, InterSystems IRIS provides built-in capabilities to develop AI-powered solutions that work directly with operational data. Depending on the edition, this includes the following capabilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IntegratedML&lt;/strong&gt;: Allows developers and SQL users to build, train, and deploy machine learning models using simple SQL statements, eliminating the need for specialized machine learning expertise or complex data science pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector Search&lt;/strong&gt;: Stores and indexes vector embeddings directly within InterSystems IRIS, enabling semantic search and similarity matching across enterprise data without requiring a separate vector database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GenAI Frameworks&lt;/strong&gt;: Offer libraries and development tools for building AI-powered applications, including Retrieval-Augmented Generation (RAG) systems, intelligent assistants, semantic search, and document question-answering solutions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Together, these capabilities enable developers to integrate structured enterprise data with modern AI techniques on a single platform. By keeping operational data, vector embeddings, and AI workflows within the same environment, InterSystems IRIS simplifies application architecture, reduces data movement, and accelerates the development of intelligent applications.&lt;/p&gt;
&lt;h3&gt;Application Services&lt;/h3&gt;
&lt;p&gt;Beyond data storage and processing, IRIS is also an application platform. Applications can be written in several languages, including the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ObjectScript&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;REST and GraphQL endpoints can also be exposed directly from the platform, making it straightforward to build APIs without introducing additional middleware.&lt;/p&gt;
&lt;h3&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;At first glance, InterSystems IRIS can seem difficult to categorize because it refuses to fit into a single box. Is it a database? An integration engine? An analytics platform? An application server? The answer is yes to all of the above. Its architecture is built around a single high-performance multi-model data engine, with interoperability, analytics, APIs, application development, and operational capabilities layered on top. Rather than stitching together separate products, developers work within one cohesive environment where every component shares the same underlying data.&lt;/p&gt;


</description>
      <category>architecture</category>
      <category>programming</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Business Rules Deep Dive: Dynamic Objects and Property Access Pitfalls - Part (2)</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Thu, 24 Sep 2026 15:46:30 +0000</pubDate>
      <link>https://dev.to/intersystems/business-rules-deep-dive-dynamic-objects-and-property-access-pitfalls-part-2-ggb</link>
      <guid>https://dev.to/intersystems/business-rules-deep-dive-dynamic-objects-and-property-access-pitfalls-part-2-ggb</guid>
      <description>&lt;p&gt;Hi, Community!&lt;/p&gt;

&lt;p&gt;In Part 1 of this series, we built a production using the HL7 Messaging type and created a BPL Business Process that enriches an incoming HL7 message with a &lt;code&gt;%DynamicObject&lt;/code&gt; before passing it to the routing engine. If you have not read Part 1 yet, start there. It fully covers the production setup, context properties, and the Build Dynamic Object Code activity.&lt;/p&gt;

&lt;p&gt;This article focuses on what happens after you start using &lt;code&gt;%DynamicObject&lt;/code&gt; in production. Seven specific property-access pitfalls catch every developer sooner or later. They all share the same frustrating characteristic: no error, no warning in the Event Log, just an empty value that causes your routing rule to fall through to the default action with no clue why.&lt;/p&gt;

&lt;p&gt;To fix that, we added a second Code activity to the same Demo.HL7Router BPL from Part 1. Then we demonstrated all seven pitfalls with working code and verified the results in the Event Log and Visual Trace.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All seven common %DynamicObject pitfalls produce the same symptom: empty value, no error, no warning.&lt;/li&gt;
&lt;li&gt;Underscore in property names, wrong case, and nested object access are the top three; use &lt;code&gt;%Get()&lt;/code&gt; for all of them.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;%IsDefined()&lt;/code&gt; to check if a property exists, &lt;code&gt;%GetIterator()&lt;/code&gt; to loop over properties, and &lt;code&gt;%GetTypeOf()&lt;/code&gt; to distinguish null from an empty string.&lt;/li&gt;
&lt;li&gt;Numeric JSON values require numeric comparison; string comparison with a number silently fails to match.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Who This Article Is For
&lt;/h2&gt;

&lt;p&gt;This guide is for IRIS Interoperability developers who work with &lt;code&gt;%DynamicObject&lt;/code&gt; in BPL Business Processes and have experienced unexpected empty values or routing rule fall-throughs that are difficult to diagnose.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Complete Part 1 of this series first. You should have a running &lt;code&gt;Demo.HL7RulesDemo&lt;/code&gt; production with the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;HL7FileService&lt;/code&gt; configured and pointing to &lt;code&gt;HL7Router&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Demo.HL7Router&lt;/code&gt; BPL with the &lt;code&gt;Build Dynamic Object&lt;/code&gt; Code activity verified in the Event Log and working&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MsgRouter&lt;/code&gt; and &lt;code&gt;HL7FileOperation&lt;/code&gt; configured and running&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Adding the Property Pitfalls Code Activity
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;Demo.HL7Router&lt;/code&gt; in the Business Process Designer.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Add Activity&lt;/strong&gt; and select &lt;strong&gt;Code&lt;/strong&gt;. Drag it between &lt;code&gt;Build Dynamic Object&lt;/code&gt; and the Call activity.&lt;/p&gt;

&lt;p&gt;Reconnect the arrows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove the arrow from &lt;code&gt;Build Dynamic Object&lt;/code&gt; to the Call activity.&lt;/li&gt;
&lt;li&gt;Connect &lt;code&gt;Build Dynamic Object&lt;/code&gt; to the new Code activity.&lt;/li&gt;
&lt;li&gt;Connect the new Code activity to the Call activity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Label it &lt;code&gt;Property Pitfalls&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdwcwhwf6ed7xg36s5vaj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdwcwhwf6ed7xg36s5vaj.png" alt=" " width="800" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the Code activity to select it. In the right-side panel, find the &lt;strong&gt;Code&lt;/strong&gt; field and click the magnifier icon. Enter the following code; each pitfall is clearly labelled in the comments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// -------------------------------------------------------
// PITFALL 1: Underscore in property name
// _ is string concatenation in ObjectScript
// context.MetaData.patient_id reads as (context.MetaData.patient) _ (id)
// -------------------------------------------------------
Try {
    Set wrongValue = context.MetaData.patient_id
} Catch ex {
    Set wrongValue = ""
}
$$$LOGINFO("Pitfall 1 - Wrong (underscore)  : [" _ wrongValue _ "]")

// CORRECT: use %Get() for underscore-named properties
Set context.PatientId = context.MetaData.%Get("patient_id")
$$$LOGINFO("Pitfall 1 - Correct            : " _ context.PatientId)

// -------------------------------------------------------
// PITFALL 2: Case sensitivity
// Patient_Id and patient_id are different properties
// -------------------------------------------------------
Set wrongCase = context.MetaData.%Get("Patient_Id")
$$$LOGINFO("Pitfall 2 - Wrong case         : [" _ wrongCase _ "]")

Set correctCase = context.MetaData.%Get("patient_id")
$$$LOGINFO("Pitfall 2 - Correct case       : " _ correctCase)

// -------------------------------------------------------
// PITFALL 3: Nested object access
// Cannot chain dot notation for nested %DynamicObject
// -------------------------------------------------------
Set addressObj = ##class(%DynamicObject).%New()
Set addressObj.city    = "Lahore"
Set addressObj.country = "PK"
Do context.MetaData.%Set("address", addressObj)

// Wrong way - fails silently
Set wrongNested = context.MetaData.address.city
$$$LOGINFO("Pitfall 3 - Wrong nested access: [" _ wrongNested _ "]")

// CORRECT: get nested object first then access its property
Set addr = context.MetaData.%Get("address")
Set city = addr.%Get("city")
$$$LOGINFO("Pitfall 3 - Correct nested     : " _ city)

// -------------------------------------------------------
// PITFALL 4: Checking if a property exists
// %Get() returns empty for both missing and empty properties
// Use %IsDefined() to check existence reliably
// -------------------------------------------------------
If context.MetaData.%IsDefined("patient_id") {
    $$$LOGINFO("Pitfall 4 - patient_id exists  : " _ context.MetaData.%Get("patient_id"))
} Else {
    $$$LOGINFO("Pitfall 4 - patient_id does not exist")
}

If context.MetaData.%IsDefined("ward_code") {
    $$$LOGINFO("Pitfall 4 - ward_code exists   : " _ context.MetaData.%Get("ward_code"))
} Else {
    $$$LOGINFO("Pitfall 4 - ward_code does not exist - use default")
}

// -------------------------------------------------------
// PITFALL 5: Iterating over properties
// For loops do not work on %DynamicObject
// Use %GetIterator() instead
// -------------------------------------------------------
$$$LOGINFO("Pitfall 5 - Iterating properties:")
Set iter = context.MetaData.%GetIterator()
While iter.%GetNext(.key, .val) {
    If $IsObject(val) {
        $$$LOGINFO("  Key: " _ key _ " = [object]")
    } Else {
        $$$LOGINFO("  Key: " _ key _ " = " _ val)
    }
}

// -------------------------------------------------------
// PITFALL 6: Null vs Empty String
// Both return empty from %Get() but are different types
// Use %GetTypeOf() to distinguish
// -------------------------------------------------------
Set nullJson = ##class(%DynamicObject).%FromJSON("{""nullProp"":null,""emptyProp"":""""}")
$$$LOGINFO("Pitfall 6 - emptyProp value    : [" _ nullJson.%Get("emptyProp") _ "]")
$$$LOGINFO("Pitfall 6 - nullProp value     : [" _ nullJson.%Get("nullProp") _ "]")
$$$LOGINFO("Pitfall 6 - emptyProp type     : " _ nullJson.%GetTypeOf("emptyProp"))
$$$LOGINFO("Pitfall 6 - nullProp type      : " _ nullJson.%GetTypeOf("nullProp"))

// -------------------------------------------------------
// PITFALL 7: Number type handling
// Numeric values in JSON are stored as numbers not strings
// Use numeric comparison or unary + for conversion
// -------------------------------------------------------
Set numObj = ##class(%DynamicObject).%FromJSON("{""age"":25}")

If numObj.%Get("age") = "25" {
    $$$LOGINFO("Pitfall 7 - String comparison  : matched (unexpected)")
} Else {
    $$$LOGINFO("Pitfall 7 - String comparison  : [no match - wrong approach]")
}

If numObj.%Get("age") = 25 {
    $$$LOGINFO("Pitfall 7 - Numeric comparison : matched correctly")
}

If +numObj.%Get("age") = 25 {
    $$$LOGINFO("Pitfall 7 - Unary + conversion : matched correctly")
}

// Store context values
Set context.PatientSex = context.MetaData.%Get("patient_sex")

// Send message to MsgRouter for routing decision
Set sc = ..%Process.SendRequestAsync("MsgRouter", request)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;OK&lt;/strong&gt; to close the editor, then click &lt;strong&gt;Save&lt;/strong&gt; and &lt;strong&gt;Compile&lt;/strong&gt;. Confirm that no errors appear before testing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifjrr8qyutr0sgkkw1im.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fifjrr8qyutr0sgkkw1im.png" alt=" " width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Test and Verify
&lt;/h2&gt;

&lt;p&gt;Drop a new test file into your incoming folder. Go to &lt;strong&gt;Management Portal &amp;gt; Interoperability &amp;gt; View &amp;gt; Messages&lt;/strong&gt; and click the message to open the Visual Trace.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi55gpcxbz20ipdkjnlme.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi55gpcxbz20ipdkjnlme.png" alt=" " width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mark the &lt;strong&gt;Show Events&lt;/strong&gt; checkbox at the top. This makes the &lt;code&gt;$$$LOGINFO&lt;/code&gt; entries from your Code activities visible in line in the trace. Click on any event entry to expand it and see the full log message.&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;Management Portal &amp;gt; Interoperability &amp;gt; View &amp;gt; Event Log&lt;/strong&gt;. All the log entries from both Code activities appear here. Look for the pitfall entries to confirm the results match what you expect:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqhjvvuuz6uubm4m12fj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqhjvvuuz6uubm4m12fj.png" alt=" " width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding the Seven Pitfalls
&lt;/h2&gt;

&lt;p&gt;All seven pitfalls share the same symptom: an empty value or an unexpected result with no error in the Event Log. That is what makes them hard to debug without knowing what to look for.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 1: Underscore in Property Name
&lt;/h3&gt;

&lt;p&gt;This is the most common silent failure when consuming REST APIs that use snake_case naming. When you write the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set val = dynObj.patient_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ObjectScript does not read &lt;code&gt;patient_id&lt;/code&gt; as a single property name. The underscore &lt;code&gt;_&lt;/code&gt; is the string concatenation operator, so this line is read as the value of &lt;code&gt;dynObj.patient&lt;/code&gt; concatenated with the string &lt;code&gt;id&lt;/code&gt;. Since &lt;code&gt;dynObj.patient&lt;/code&gt; does not exist, the result is empty, with no error or warning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Wrong - returns empty silently
Try {
    Set val = dynObj.patient_id
} Catch ex {
    Set val = ""
}

// Correct - use %Get() for underscore-named properties
Set val = dynObj.%Get("patient_id")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pitfall 2: Case Sensitivity
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;%DynamicObject&lt;/code&gt; property names are fully case-sensitive. &lt;code&gt;PATIENT_ID, patient_id, and Patient_Id are three completely different properties.&lt;/code&gt; This comes up when you consume third-party APIs with inconsistent casing or when you type the property name slightly differently in two places.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Wrong - wrong case returns empty silently
Set val = dynObj.%Get("Patient_Id")

// Correct - exact case must match what was set
Set val = dynObj.%Get("patient_id")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always check the raw JSON to confirm the exact casing before writing your access code. Log the property value immediately after setting it during development to confirm it is there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 3: Nested Object Access
&lt;/h3&gt;

&lt;p&gt;Modern REST APIs almost always return nested JSON structures. When IRIS parses nested JSON, each nested object becomes its own &lt;code&gt;%DynamicObject&lt;/code&gt; instance. Dot notation does not automatically traverse nested objects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Wrong - fails silently
Set city = dynObj.address.city

// Correct - get the nested object first, then access its property
Set addr = dynObj.%Get("address")
Set city = addr.%Get("city")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern applies at every level of nesting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set patientObj = dynObj.%Get("patient")
Set contactObj = patientObj.%Get("contact")
Set phone      = contactObj.%Get("phone")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pitfall 4: Checking If a Property Exists
&lt;/h3&gt;

&lt;p&gt;Many developers use &lt;code&gt;%Get()&lt;/code&gt; and check if the result is empty to determine whether a property exists. However, this is unreliable because a property can exist with an empty value, and both cases return empty from &lt;code&gt;%Get()&lt;/code&gt;. The correct method is &lt;code&gt;%IsDefined()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Wrong - cannot distinguish missing property from empty value
If dynObj.%Get("patient_id") = "" {
    // Is it missing or just empty?
}

// Correct - explicitly checks if property exists
If dynObj.%IsDefined("patient_id") {
    Set val = dynObj.%Get("patient_id")
} Else {
    $$$LOGINFO("patient_id does not exist - using default")
    Set val = "UNKNOWN"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Event Log, you will see &lt;code&gt;ward_code&lt;/code&gt; logged as "does not exist" because we never set it, while &lt;code&gt;patient_id&lt;/code&gt; is correctly found and returned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 5: Iterating Over Properties
&lt;/h3&gt;

&lt;p&gt;ObjectScript &lt;code&gt;For&lt;/code&gt; loops do not work on &lt;code&gt;%DynamicObject&lt;/code&gt;. The correct way is &lt;code&gt;%GetIterator()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Wrong - does not work on %DynamicObject
For i = 1:1:dynObj.%Size() {
    Write dynObj.%Get(i), !
}

// Correct - use %GetIterator()
Set iter = dynObj.%GetIterator()
While iter.%GetNext(.key, .val) {
    If $IsObject(val) {
        $$$LOGINFO("Key: " _ key _ " = [object]")
    } Else {
        $$$LOGINFO("Key: " _ key _ " = " _ val)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Event Log, you will see all properties listed in order, confirming that &lt;code&gt;%GetIterator()&lt;/code&gt; traverses all of them correctly, including the nested &lt;code&gt;address&lt;/code&gt; object.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 6: Null vs. Empty String
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;%DynamicObject&lt;/code&gt; distinguishes between a property set to JSON &lt;code&gt;null&lt;/code&gt; and a property set to an empty string &lt;code&gt;""&lt;/code&gt;. Both return empty from &lt;code&gt;%Get()&lt;/code&gt;, but they are different types. Use &lt;code&gt;%GetTypeOf()&lt;/code&gt; to distinguish between them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set testObj = ##class(%DynamicObject).%FromJSON("{""nullProp"":null,""emptyProp"":""""}")

// Both return empty from %Get()
Write testObj.%Get("emptyProp"), !   // empty
Write testObj.%Get("nullProp"), !    // empty

// Use %GetTypeOf() to distinguish
Write testObj.%GetTypeOf("emptyProp"), !  // string
Write testObj.%GetTypeOf("nullProp"), !   // null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters when you need to know whether a field was intentionally left empty or was absent from the JSON payload entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pitfall 7: Number Type Handling
&lt;/h3&gt;

&lt;p&gt;When JSON contains numeric values, &lt;code&gt;%DynamicObject&lt;/code&gt; stores them as numbers, not strings. Comparing a numeric property with a string value silently fails to match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set numObj = ##class(%DynamicObject).%FromJSON("{""age"":25}")

// Wrong - string comparison does not match a numeric value
If numObj.%Get("age") = "25" { ... }

// Correct - use numeric comparison
If numObj.%Get("age") = 25 { ... }

// Also correct - force numeric conversion with unary +
If +numObj.%Get("age") = 25 { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Using dot notation for anything other than simple property names:&lt;/strong&gt; If the property name has an underscore, has inconsistent casing, or could be nested, always use &lt;code&gt;%Get()&lt;/code&gt;. Making it your default for all &lt;code&gt;%DynamicObject&lt;/code&gt; access prevents pitfalls 1, 2, and 3 with one habit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not logging values during development:&lt;/strong&gt; All seven pitfalls return empty silently. Without &lt;code&gt;$$$LOGINFO&lt;/code&gt; statements around your property access code, you will have no idea which pitfall you hit. Log every value you read from a &lt;code&gt;%DynamicObject&lt;/code&gt; during development, then clean up before going to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trusting the routing rule to tell you what went wrong:&lt;/strong&gt; The routing rule does not throw an error when a condition evaluates to empty. It simply moves to the next rule or the default. If you are debugging a routing fall-through, always check the Event Log first to confirm what values your Code activity produced before looking at the routing rule.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Recommendations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Make &lt;code&gt;%Get()&lt;/code&gt; your default for all &lt;code&gt;%DynamicObject&lt;/code&gt; property access, not just for underscores.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;%Set()&lt;/code&gt; when setting any property with special characters, including underscores.&lt;/li&gt;
&lt;li&gt;Exploit &lt;code&gt;%IsDefined()&lt;/code&gt; before &lt;code&gt;%Get()&lt;/code&gt; whenever a property might be missing.&lt;/li&gt;
&lt;li&gt;Utilize &lt;code&gt;%GetTypeOf()&lt;/code&gt; when you need to distinguish null from an empty string.&lt;/li&gt;
&lt;li&gt;Employ numeric comparison (&lt;code&gt;= 25&lt;/code&gt;, not &lt;code&gt;= "25"&lt;/code&gt;) when working with JSON numeric values.&lt;/li&gt;
&lt;li&gt;Log every dynamic object property during development and verify them in the Event Log before testing the routing rule.&lt;/li&gt;
&lt;li&gt;Remove debug &lt;code&gt;$$$LOGINFO&lt;/code&gt; statements before going to production; they write to the Event Log database and consume storage over time.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does my routing rule fall through even though the condition looks correct?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: The most likely cause is a silent empty value on the left side of the condition. Check the Event Log for the &lt;code&gt;$$$LOGINFO&lt;/code&gt; entries from your Code activity. If the value you are routing on is empty, trace it back through the seven pitfalls starting with an underscore in the property name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is there a safe way to read any &lt;/strong&gt;&lt;code&gt;%DynamicObject&lt;/code&gt;&lt;strong&gt; property regardless of what it is called?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: Yes. Always use &lt;code&gt;%Get("propertyName")&lt;/code&gt;. It handles underscore names, case-sensitive names, and works as the entry point for nested object traversal. Dot notation works reliably only for simple property names with no special characters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I know if a property name has the wrong case?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: Log the raw JSON string before parsing it and compare it against your &lt;code&gt;%Get()&lt;/code&gt; calls. In development, log every value immediately after setting it and check the Event Log to confirm the value is there before the routing rule evaluates it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I iterate over a &lt;/strong&gt;&lt;code&gt;%DynamicArray&lt;/code&gt;&lt;strong&gt; the same way?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: Yes. &lt;code&gt;%GetIterator()&lt;/code&gt; works on both &lt;code&gt;%DynamicObject&lt;/code&gt; and &lt;code&gt;%DynamicArray&lt;/code&gt;. For an array, the iterator returns numeric indices starting from 0 instead of property names. The &lt;code&gt;%GetNext(.key, .val)&lt;/code&gt; pattern functions in an identical manner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What does &lt;/strong&gt;&lt;code&gt;%GetTypeOf()&lt;/code&gt;&lt;strong&gt; return for different value types?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: It returns a string describing the type: &lt;code&gt;"string"&lt;/code&gt;, &lt;code&gt;"number"&lt;/code&gt;, &lt;code&gt;"boolean"&lt;/code&gt;, &lt;code&gt;"null"&lt;/code&gt;, &lt;code&gt;"object"&lt;/code&gt;, or &lt;code&gt;"array"&lt;/code&gt;. Use it whenever you need to make a decision based on what type of value a property holds rather than just whether it is empty or not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Should I remove the &lt;/strong&gt;&lt;code&gt;$$$LOGINFO&lt;/code&gt;&lt;strong&gt; statements after testing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: Yes, for production. &lt;code&gt;$$$LOGINFO&lt;/code&gt; entries write to the Event Log database and consume storage over time. Keep only those entries that are genuinely useful for monitoring and remove the debug-level ones used during development.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;All seven pitfalls covered in this article share one characteristic: they fail silently. That is what makes them so frustrating to debug. The routing rule falls through, the message lands on the default action, and there is nothing in the Event Log to point you in the right direction unless you already know what to log.&lt;/p&gt;

&lt;p&gt;The fix for most of them, however, is a single habit: use &lt;code&gt;%Get()&lt;/code&gt; for all &lt;code&gt;%DynamicObject&lt;/code&gt; property access and log every value during development. Once you know the seven patterns, spotting them in code will take you seconds rather than hours.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;




</description>
      <category>bpl</category>
      <category>productivity</category>
      <category>programming</category>
      <category>objectscript</category>
    </item>
    <item>
      <title>Business Rules Deep Dive: Dynamic Objects and Property Access Pitfalls - Part (1)</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Thu, 24 Sep 2026 15:43:21 +0000</pubDate>
      <link>https://dev.to/intersystems/business-rules-deep-dive-dynamic-objects-and-property-access-pitfalls-part-1-4eom</link>
      <guid>https://dev.to/intersystems/business-rules-deep-dive-dynamic-objects-and-property-access-pitfalls-part-1-4eom</guid>
      <description>&lt;p&gt;Hi, Community!&lt;/p&gt;

&lt;p&gt;Business Rules in IRIS Interoperability are one of those features that work smoothly until they suddenly do not anymore. You build a rule, the condition looks correct, the routing target is right, but the message keeps falling through to the default action. There is no error in the Event Log and no obvious clue in the Visual Trace.&lt;/p&gt;

&lt;p&gt;One of the most common causes is a dynamic object whose properties are not accessible the way you expect. This two-part series covers the full picture. Part 1 walks us through setting up a production using the HL7 Messaging type and building a BPL Business Process that enriches an incoming HL7 message with a dynamic object. Part 2 uses the same production and demonstrates seven real-world property-access pitfalls that cause silent failures in production integrations.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;%DynamicObject&lt;/code&gt; in IRIS is a schema-free object you create on the fly without defining a class.&lt;/li&gt;
&lt;li&gt;Use it inside a BPL Business Process to carry enrichment data alongside an HL7 message before the routing rule evaluates it.&lt;/li&gt;
&lt;li&gt;Exploit &lt;code&gt;%Set()&lt;/code&gt; to set properties with special characters like underscores and &lt;code&gt;%Get()&lt;/code&gt; to read them back.&lt;/li&gt;
&lt;li&gt;Part 2 of this series covers seven property-access pitfalls that all silently return empty with no error.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Who This Article Is For
&lt;/h2&gt;

&lt;p&gt;This guide is for IRIS Interoperability developers working with BPL Business Processes and HL7 routing who want to understand how to enrich messages with dynamic objects before routing decisions are made.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is a Business Rule?
&lt;/h2&gt;

&lt;p&gt;A Business Rule in IRIS Interoperability is a routing engine that evaluates conditions against an incoming message and decides where to send it. Business Rules do not run on their own; they run inside a Business Process.&lt;/p&gt;

&lt;p&gt;In this article, all the data preparation work happens in a BPL Business Process before the routing rule fires. The routing rule then evaluates clean, simple values and makes the decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is a Dynamic Object?
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;%DynamicObject&lt;/code&gt; in IRIS is a schema-free object. Unlike a regular ObjectScript class where you define properties upfront, a &lt;code&gt;%DynamicObject&lt;/code&gt; lets you create any property on the fly at runtime without a class definition. This makes it extremely useful in integration scenarios where you need to carry extra data alongside a message as it moves through &amp;nbsp;production.&lt;/p&gt;

&lt;p&gt;A common real-world pattern is enriching an incoming HL7 message with additional metadata before the routing rule evaluates it. Instead of modifying the original HL7 message class, you create a &lt;code&gt;%DynamicObject&lt;/code&gt;, set whatever properties you need, store it in the BPL context, and read from it downstream.&lt;/p&gt;

&lt;p&gt;In our example, we read the message type, patient name, and sending application from the incoming HL7 message and store them in a dynamic object. That object then travels with the message through production, and anything downstream can read from it without touching the original HL7 structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Production We Will Build
&lt;/h2&gt;

&lt;p&gt;The production uses standard auto-created components plus one new BPL Business Process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HL7FileService:&lt;/strong&gt; Scans a folder for incoming HL7 files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HL7Router:&lt;/strong&gt; Our new BPL Business Process that builds the dynamic object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MsgRouter:&lt;/strong&gt; Evaluates the routing rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HL7FileOperation:&lt;/strong&gt; Writes the routed message to an output folder.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Create the Production
&lt;/h2&gt;

&lt;p&gt;Go to &lt;strong&gt;Management Portal &amp;gt; Interoperability &amp;gt; List &amp;gt; Production&lt;/strong&gt; and click &lt;strong&gt;New&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fill in the form with the following data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Package&lt;/strong&gt;: &lt;code&gt;Demo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: &lt;code&gt;HL7RulesDemo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description&lt;/strong&gt;: &lt;code&gt;Demonstrates dynamic objects and property access pitfalls using a BPL process alongside the HL7 routing engine&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Type&lt;/strong&gt;: &lt;code&gt;HL7 Messaging&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygu76aiyodmj77rs8u4v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fygu76aiyodmj77rs8u4v.png" alt=" " width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;OK&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you select &lt;strong&gt;HL7 Messaging&lt;/strong&gt;, IRIS automatically creates a set of standard components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HL7FileService:&lt;/strong&gt; A Business Service that monitors a folder for incoming HL7 files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MsgRouter:&lt;/strong&gt; An &lt;code&gt;EnsLib.HL7.MsgRouter.RoutingEngine&lt;/code&gt; that evaluates routing rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HL7FileOperation:&lt;/strong&gt; A Business Operation that writes routed messages to an output folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BadMessageHandler&lt;/strong&gt;, &lt;strong&gt;EMailAlert&lt;/strong&gt;, &lt;strong&gt;PagerAlert: &lt;/strong&gt;Alert components you can configure later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6dv8bp3g6vsctk8vao3q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6dv8bp3g6vsctk8vao3q.png" alt=" " width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It saves significant setup time, so we will simply reuse these components and add only one new BPL Business Process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Configure the Auto-Created Components
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Configure HL7FileService
&lt;/h3&gt;

&lt;p&gt;Click on &lt;code&gt;HL7FileService&lt;/code&gt; in the canvas to open its settings panel. Set up the following information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File Path&lt;/strong&gt;: Your incoming folder (e.g., C:\hl7\in)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Spec&lt;/strong&gt;: &lt;code&gt;*.hl7&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message Schema Category&lt;/strong&gt;: &lt;code&gt;2.5&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmamh7psq2myv1fyarx89.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmamh7psq2myv1fyarx89.png" alt=" " width="603" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Apply&lt;/strong&gt;.&lt;br&gt;
&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;
  
  
  Configure HL7FileOperation
&lt;/h3&gt;

&lt;p&gt;Click on &lt;code&gt;HL7FileOperation&lt;/code&gt; in the canvas and set the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;File Path&lt;/strong&gt;: Your outgoing folder (e.g., C:\hl7\out)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpyj95gz7y90a64qnqjvq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpyj95gz7y90a64qnqjvq.png" alt=" " width="606" height="607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Apply&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  &amp;nbsp;
&lt;/h2&gt;
&lt;h2&gt;
  
  
  Step 3: Build the BPL Business Process
&lt;/h2&gt;

&lt;p&gt;The MsgRouter routing engine evaluates simple conditions but does not support Code activities for building dynamic objects. We add a BPL Business Process between &lt;code&gt;HL7FileService&lt;/code&gt; and &lt;code&gt;MsgRouter&lt;/code&gt; to handle all data preparation.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;3A. Create the Process&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Go to Management Portal &amp;gt; Interoperability &amp;gt; List &amp;gt; Business Processes and click New. In the General tab, set Language to ObjectScript. Then save it as &lt;code&gt;Demo.HL7Router&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fluc8jhx21d45pobmmz31.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fluc8jhx21d45pobmmz31.png" alt=" " width="799" height="239"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;3B. Define Context Properties&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;In BPL, &lt;code&gt;context&lt;/code&gt; is a typed class, and every property you want to use must be declared upfront in the Context tab before you write any code. If you skip this and try to set a property inside a Code activity, IRIS will throw a &lt;code&gt;PROPERTY DOES NOT EXIST&lt;/code&gt; error at runtime. So, before touching the canvas, let us define all the properties this process will need.&lt;/p&gt;

&lt;p&gt;In the right-side panel, click the Context tab, then click + to add each property:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property Name&lt;/th&gt;
&lt;th&gt;Property Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MetaData&lt;/td&gt;
&lt;td&gt;%DynamicObject&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PatientId&lt;/td&gt;
&lt;td&gt;%String(MAXLEN=50)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PatientSex&lt;/td&gt;
&lt;td&gt;%String(MAXLEN=50)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuksrzblzcufh4ws4k28k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuksrzblzcufh4ws4k28k.png" alt=" " width="610" height="559"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;3C. Add the Code Activity&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Click Add Activity and select Code. Drag it between &lt;code&gt;&amp;lt;start&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;end&amp;gt;&lt;/code&gt;. Label it &lt;code&gt;Build Dynamic Object&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Connect the arrows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;start&amp;gt;&lt;/code&gt; to &lt;code&gt;Build Dynamic Object&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Build Dynamic Object&lt;/code&gt; to &lt;code&gt;&amp;lt;end&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpg55z41zhuv9cwfqvja5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpg55z41zhuv9cwfqvja5.png" alt=" " width="799" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the Code activity to select it. In the right-side panel, find the &lt;strong&gt;Code&lt;/strong&gt; field and click the magnifier icon next to it. It will open a larger editor where you can enter the code below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$$$LOGINFO("Request class: " _ $classname(request))
Set dynObj = ##class(%DynamicObject).%New()

// Read values from the actual HL7 message
Set dynObj.MsgType     = request.GetValueAt("MSH:MessageType.MessageCode")
Set dynObj.PatientName = request.GetValueAt("PID:PatientName(1).FamilyName")
Set dynObj.SendingApp  = request.GetValueAt("MSH:SendingApplication")

// Use %Set() for underscore-named properties
Do dynObj.%Set("patient_id",    request.GetValueAt("PID:PatientIDList(1).IDNumber"))
Do dynObj.%Set("patient_sex",   request.GetValueAt("PID:AdministrativeSex"))
Do dynObj.%Set("date_of_birth", request.GetValueAt("PID:DateTimeofBirth"))

// Log each value for testing purposes
$$$LOGINFO("Dynamic Object built:")
$$$LOGINFO("  MsgType    : " _ dynObj.MsgType)
$$$LOGINFO("  PatientName: " _ dynObj.PatientName)
$$$LOGINFO("  SendingApp : " _ dynObj.SendingApp)
$$$LOGINFO("  patient_id : " _ dynObj.%Get("patient_id"))

// Store in context
Set context.MetaData = dynObj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8zz6qwt88ewvtskhqju.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw8zz6qwt88ewvtskhqju.png" alt=" " width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;OK&lt;/strong&gt; to close the editor, then click &lt;strong&gt;Save&lt;/strong&gt; and &lt;strong&gt;Compile&lt;/strong&gt;. Ensure that no errors appear before moving forward.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3D. Add the Process to Production&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Go back to the production canvas. Click &lt;strong&gt;Add&lt;/strong&gt; in the Processes section:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Process Class&lt;/strong&gt;: &lt;code&gt;Demo.HL7Router&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process Name&lt;/strong&gt;: &lt;code&gt;HL7Router&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description&lt;/strong&gt;: &lt;code&gt;BPL process that builds dynamic objects before routing&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable Now&lt;/strong&gt;: Checked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdig5n9xxrw5rrh7lm5gt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdig5n9xxrw5rrh7lm5gt.png" alt=" " width="799" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;OK&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Before starting the production, make sure that the following steps are complete:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Set Business Service Target&lt;/strong&gt;&lt;br&gt;
Click on &lt;code&gt;HL7FileService&lt;/code&gt; in the production canvas. Confirm &lt;strong&gt;Target Config Names&lt;/strong&gt; is set to &lt;code&gt;HL7Router&lt;/code&gt;. If not, type &lt;code&gt;HL7Router&lt;/code&gt; and click &lt;strong&gt;Apply&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Enable All Components&lt;/strong&gt;&lt;br&gt;
Confirm the &lt;strong&gt;Enabled&lt;/strong&gt; checkbox is marked off for the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HL7FileService&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HL7Router&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MsgRouter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HL7FileOperation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  How It All Fits Together
&lt;/h2&gt;

&lt;p&gt;It is worth pausing here to understand the full flow before we start testing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;HL7FileService&lt;/code&gt; picks up the &lt;code&gt;.hl7&lt;/code&gt; file from the incoming folder and parses it into an &lt;code&gt;EnsLib.HL7.Message&lt;/code&gt;.&amp;nbsp;
It forwards the message to &lt;code&gt;HL7Router&lt;/code&gt; based on the Target Config Names setting.&amp;nbsp; &amp;nbsp;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HL7Router&lt;/code&gt; runs the &lt;code&gt;Build Dynamic Object&lt;/code&gt; Code activity, stores the enriched object in &lt;code&gt;context.MetaData&lt;/code&gt;, and then calls &lt;code&gt;MsgRouter&lt;/code&gt; using &lt;code&gt;SendRequestAsync&lt;/code&gt;. &lt;code&gt;MsgRouter&lt;/code&gt; evaluates the routing rule and sends the message to &lt;code&gt;HL7FileOperation&lt;/code&gt;.&amp;nbsp; &amp;nbsp;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HL7FileOperation&lt;/code&gt; writes the routed message to the outgoing folder.&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Step 4: Start the Production and Test
&lt;/h2&gt;

&lt;p&gt;Start the production. All components should show green status indicators.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fio19prkiyg9f4p26edl0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fio19prkiyg9f4p26edl0.png" alt=" " width="798" height="191"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The Sample HL7 Message
&lt;/h3&gt;

&lt;p&gt;Create the test file using the IRIS terminal to ensure correct segment separators:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Set file = ##class(%File).%New("C:\hl7\in\test1.hl7")
Do file.Open("NWS")
Set content = "MSH|^~\&amp;amp;|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20250315143022||ADT^A01|MSG00001|P|2.5" _ $Char(13)
Set content = content _ "EVN|A01|20250315143022" _ $Char(13)
Set content = content _ "PID|1||MRN001^^^HOSP||Smith^John^M||19850101|M|||123 Main Street^^Lahore^Punjab^54000^PK||03001234567" _ $Char(13)
Set content = content _ "PV1|1|I|WARD-A^Room101^Bed1|E|||DOC001^Smith^James|||SUR|||||||V01|ACC001" _ $Char(13)
Do file.Write(content)
Do file.Close()
Write "File created", !
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Remember to replace C:\hl7\in\ with your actual incoming folder path.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify in Message Viewer and Visual Trace
&lt;/h3&gt;

&lt;p&gt;Within 5 seconds, the adapter picks up the file. Go to &lt;strong&gt;Management Portal &amp;gt; Interoperability &amp;gt; View &amp;gt; Messages&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6exc3fj05s1cal6d0g0r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6exc3fj05s1cal6d0g0r.png" alt=" " width="799" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the message to open the Visual Trace. You will see the complete flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;HL7FileService&lt;/code&gt; reads the file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HL7Router&lt;/code&gt; processes it and builds the dynamic object.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MsgRouter&lt;/code&gt; evaluates the routing rule.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HL7FileOperation&lt;/code&gt; writes the output file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5aiblw7icajeovax09hb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5aiblw7icajeovax09hb.png" alt=" " width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Not defining context properties upfront: &lt;/strong&gt;If you try to set &lt;code&gt;context.MetaData&lt;/code&gt; without first declaring it in the Context tab, IRIS will throw &lt;code&gt;PROPERTY DOES NOT EXIST&lt;/code&gt; at runtime. Always define all context properties before writing any code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting the Target Config Names after adding the process:&lt;/strong&gt; If you add &lt;code&gt;HL7Router&lt;/code&gt; to production but forget to update &lt;code&gt;HL7FileService&lt;/code&gt; Target Config Names to point to it, messages will bypass &lt;code&gt;HL7Router&lt;/code&gt; entirely and go directly to &lt;code&gt;MsgRouter&lt;/code&gt;. Always confirm the target after adding a new component.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Recommendations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Always define all context properties in the Context tab before writing any Code activity logic.&lt;/li&gt;
&lt;li&gt;Log every dynamic object property immediately after setting it during development; this is the fastest way to confirm values are correct before testing the routing rule.&lt;/li&gt;
&lt;li&gt;Keep the dynamic object lean by only storing values you actually need downstream.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Do I need a dynamic object if I can pass the HL7 message directly to the routing rule?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: Simple property names (e.g., context.MetaData.MsgType = "ADT") work well and get evaluated correctly. However, properties with underscores in their names do not work reliably in the rule expression editor. Extract those values into typed context variables in the Code activity and evaluate them in the rule instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Why does my Code activity compile but fail at runtime with PROPERTY DOES NOT EXIST?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: This happens when you reference a context property that was not declared in the Context tab. Go to the Context tab, add the missing property, and recompile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I use &lt;/strong&gt;&lt;code&gt;%DynamicObject&lt;/code&gt;&lt;strong&gt; properties directly in a routing rule condition?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: Simple property names work &lt;code&gt;context.MetaData.MsgType = "ADT"&lt;/code&gt; evaluates correctly. Properties with underscores in their names do not work reliably in the rule expression editor. Extract those values into typed context variables in the Code activity and evaluate those in the rule instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What happens if I forget to call Compile after editing the BPL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: The production will run the previous compiled version of the class. Your changes will not take effect until you compile. Always compile after every code change and confirm there are no errors before testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I store a nested object inside a &lt;/strong&gt;&lt;code&gt;%DynamicObject&lt;/code&gt;&lt;strong&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A: Yes. You can set a nested &lt;code&gt;%DynamicObject&lt;/code&gt; as a property value using &lt;code&gt;%Set()&lt;/code&gt;. However, to access nested properties, you should get the nested object first. (It will be covered in detail in Part 2.)&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Coming in Part 2
&lt;/h2&gt;

&lt;p&gt;Part 1 covered building and using a &lt;code&gt;%DynamicObject&lt;/code&gt; inside a BPL Business Process. Part 2 will use the same production but add a second Code activity that will demonstrate seven real-world property-access pitfalls.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Dynamic objects are a practical tool that enriches HL7 messages with metadata before routing decisions are made. The key is to build them in a BPL Code activity, logging every property during development, and storing them in context variables that downstream components can evaluate cleanly.&lt;/p&gt;

&lt;p&gt;The production we built in this article gives you a working foundation to experiment with. Try adding more properties to the dynamic object, or update the routing rule condition to evaluate &lt;code&gt;context.MetaData.MsgType&lt;/code&gt; directly and see how it behaves. In the next article in this series, we will take this exact production and break down seven real-world property-access pitfalls every developer encounters when working with %DynamicObject in production integrations.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>hl7</category>
      <category>bpl</category>
      <category>interoperability</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Exposing IRIS data through MCP with AI Hub: a practical toolkit with access controls</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sat, 19 Sep 2026 16:03:24 +0000</pubDate>
      <link>https://dev.to/intersystems/exposing-iris-data-through-mcp-with-ai-hub-a-practical-toolkit-with-access-controls-57nh</link>
      <guid>https://dev.to/intersystems/exposing-iris-data-through-mcp-with-ai-hub-a-practical-toolkit-with-access-controls-57nh</guid>
      <description>&lt;p&gt;Connecting an AI agent to a database is useful, but it immediately raises another question: what should that agent be allowed to read?&lt;/p&gt;
&lt;p&gt;For this project, I wanted to make that decision in the application code. An agent can search patient records using a few filters, inspect an approved global, and retrieve basic information about an IRIS namespace. It cannot submit its own SQL query or select another namespace.&lt;/p&gt;

&lt;h2&gt;What is this project?&lt;/h2&gt;
&lt;p&gt;I built the &lt;strong&gt;MCP Data Exposure Toolkit&lt;/strong&gt; for the InterSystems &lt;a href="https://community.intersystems.com/post/community-bounty-program-idea-application-%E2%80%94-round-2-live" rel="nofollow noopener noreferrer"&gt;Community Bounty Program “Idea to Application” — Round 2&lt;/a&gt;, in response to the &lt;a href="https://ideas.intersystems.com/ideas/DPI-I-985" rel="nofollow noopener noreferrer"&gt;MCP Data Exposure Toolkit idea (DPI-I-985)&lt;/a&gt;. My contribution is a runnable ObjectScript example covering SQL, globals, and namespace monitoring.&lt;/p&gt;
&lt;p&gt;The source is available on &lt;a href="https://github.com/pietrodileo/iris-mcp-data-exposure-toolkit" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. In this article, we'll run it and look at the code behind the examples.&lt;/p&gt;
&lt;h2&gt;What the example exposes&lt;/h2&gt;
&lt;p&gt;I chose synthetic healthcare records because they provide useful search criteria without putting real patient information in the repository. The project imports 500 records from the &lt;a href="https://www.kaggle.com/datasets/dnation/synthetic-healthcare-patient-records-dataset" rel="nofollow noopener noreferrer"&gt;Synthetic Healthcare Patient Records Dataset by dnation on Kaggle&lt;/a&gt;. The persistent class &lt;code&gt;MCPData.Data.Patient&lt;/code&gt; stores those records.&lt;/p&gt;
&lt;p&gt;There are five tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;ListResources&lt;/strong&gt;&lt;/code&gt;: Describe approved data sources and operations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;SearchPatients&lt;/strong&gt;&lt;/code&gt;: Filter synthetic patient records (at most 50 rows)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;ReadGlobalData&lt;/strong&gt;&lt;/code&gt;: Read nodes of global &lt;code&gt;^ERRORS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;LargestGlobals&lt;/strong&gt;&lt;/code&gt;: Report estimated global sizes&amp;nbsp;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;RecentApplicationErrors&lt;/strong&gt;&lt;/code&gt;: Return recent error summaries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These tools help an agent discover what data is available and provide controlled ways to access it without exposing every underlying table or global.&lt;/p&gt;
&lt;h2&gt;How AI Hub fits into the project&lt;/h2&gt;
&lt;p&gt;You can obtain the AI Hub through the Early Access Program: &lt;a href="https://github.com/intersystems-community/ai-hub-eap" rel="noopener noreferrer"&gt;InterSystems AI Hub Early Access Program repository&lt;/a&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;That repository also contains the documentation and samples I used as a starting point. This project depends on that early-access software; it is not an example for a standard IRIS image without AI Hub.&lt;/p&gt;
&lt;h2&gt;Application Architecture&lt;/h2&gt;
&lt;p&gt;The application consists of two Docker containers, each with a distinct responsibility:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;iris&lt;/strong&gt;&lt;/code&gt; hosts the InterSystems IRIS database, the ObjectScript tool implementations, and the native MCP service.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;mcp&lt;/strong&gt;&lt;/code&gt; runs the EAP &lt;code&gt;iris-mcp-server&lt;/code&gt; binary, which exposes the MCP service to external clients using the Streamable HTTP transport.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Together, these containers separate the external MCP transport layer from the database and tool execution environment.&lt;/p&gt;
&lt;p&gt;A request from an MCP client follows this path:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9i4f97ohevd85ngnp61p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9i4f97ohevd85ngnp61p.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;p&gt;When a client sends an MCP request, Docker first forwards traffic from port &lt;code&gt;8280&lt;/code&gt; on the host to port &lt;code&gt;8080&lt;/code&gt; in the &lt;code&gt;mcp&lt;/code&gt; container. The &lt;code&gt;iris-mcp-server&lt;/code&gt; then communicates with IRIS over the Docker Compose network using the native connection on &lt;code&gt;iris:1972&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Within the &lt;code&gt;MCP_EXAMPLE&lt;/code&gt; namespace, the request passes through three ObjectScript layers. &lt;code&gt;MCPData.Service.HealthExample&lt;/code&gt; determines which toolset is available, &lt;code&gt;MCPData.ToolSet.HealthExample&lt;/code&gt; associates the tools with the appropriate authorization and auditing policies, and &lt;code&gt;MCPData.Tools.HealthExample&lt;/code&gt; contains the ObjectScript methods that perform the actual operations.&lt;/p&gt;
&lt;p&gt;This design keeps the public MCP endpoint separate from the underlying database implementation while giving clients a controlled set of tools through which they can interact with IRIS.&lt;/p&gt;

&lt;h2&gt;Running the toolkit&lt;/h2&gt;
&lt;a href="https://github.com/pietrodileo/iris-mcp-data-exposure-toolkit/blob/main/ARTICLE.md#3-running-the-toolkit" id="user-content-3-running-the-toolkit" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;p&gt;Clone the project and follow the repository instructions to prepare the required configuration. Once everything is configured, start the containers. After the services have finished initializing, you should be able to connect your AI agent to the MCP servers and begin using the available tools.&lt;/p&gt;
&lt;h3&gt;VS Code&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;.vscode/mcp.json&lt;/code&gt; in the workspace with the authorization header included:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{ &lt;br&gt;
  "servers": { &lt;br&gt;
    "health-example": { &lt;br&gt;
      "type": "http", &lt;br&gt;
      "url": "&lt;a href="http://localhost:8280/mcp/health-example" rel="noopener noreferrer"&gt;http://localhost:8280/mcp/health-example&lt;/a&gt;", &lt;br&gt;
      "headers": { &lt;br&gt;
        "Authorization": "Basic &amp;lt;generated key&amp;gt;" &lt;br&gt;
      } &lt;br&gt;
    } &lt;br&gt;
  } &lt;br&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mistral or other CLI AI agents&lt;/h3&gt;
&lt;p&gt;For Mistral Vibe, add this server to the project file &lt;code&gt;.vibe/config.toml&lt;/code&gt; or the user file &lt;code&gt;~/.vibe/config.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[mcp_servers]] &lt;br&gt;
name = "care_data" &lt;br&gt;
transport = "streamable-http" &lt;br&gt;
url = "&lt;a href="http://localhost:8280/mcp/health-example" rel="noopener noreferrer"&gt;http://localhost:8280/mcp/health-example&lt;/a&gt;" &lt;br&gt;
headers = { Authorization = "Basic &amp;lt;generated key&amp;gt;" }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start Vibe and use &lt;code&gt;/mcp&lt;/code&gt; to inspect the connection.&lt;/p&gt;
&lt;p&gt;A connected MCP client should discover all five tools. Here is the list in Mistral Vibe:&lt;/p&gt;
&lt;h2&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fblob%2Fmain%2Fpic%2Fmistral_mcp_connected.png%3Fraw%3Dtrue" alt="mistral_mcp_connected.png" width="2766" height="1180"&gt;&lt;/h2&gt;
&lt;h3&gt;Python smoke test&lt;/h3&gt;
&lt;p&gt;If you don't have access to an AI agent, the repository also includes a Python smoke test. It connects directly to the MCP endpoint, verifies that all five expected tools are available, and calls each one with a small set of test inputs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv venv --python 3.12&lt;br&gt;
source .venv/bin/activate&lt;br&gt;
uv pip install -r requirements.txt&lt;br&gt;
set -a&lt;br&gt;
source .env&lt;br&gt;
set +a&lt;br&gt;
python test_mcp.py&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;From an ObjectScript method to an MCP tool&lt;/h2&gt;
&lt;a href="https://github.com/pietrodileo/iris-mcp-data-exposure-toolkit/blob/main/ARTICLE.md#4-from-an-objectscript-method-to-an-mcp-tool" id="user-content-4-from-an-objectscript-method-to-an-mcp-tool" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;p&gt;Now that the client is connected, let's look at the most important part of the project: how an ObjectScript method becomes an MCP tool, and how the application controls what the agent is allowed to do.&lt;/p&gt;
&lt;p&gt;The project does not rely on a single security check. Instead, access is controlled at several layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the IRIS user and database role;&lt;/li&gt;
&lt;li&gt;the authorization policy;&lt;/li&gt;
&lt;li&gt;the tool implementation;&lt;/li&gt;
&lt;li&gt;the &lt;em&gt;ToolSet &lt;/em&gt;configuration; and&lt;/li&gt;
&lt;li&gt;the audit policy.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each layer has a different responsibility. Together, they ensure that the agent receives only the capabilities that the application explicitly exposes.&lt;/p&gt;
&lt;h3&gt;Step 1: Define the database user and role&lt;/h3&gt;
&lt;p&gt;The first layer is the IRIS security model. The endpoint user receives only the &lt;code&gt;MCPDataReader&lt;/code&gt; role. That role grants &lt;code&gt;SELECT&lt;/code&gt; permission on the patient table, &lt;code&gt;MCPData_Data.Patient&lt;/code&gt;, but does not grant unrestricted access to the database. The tool still executes under an IRIS user, and that user must have permission to perform the requested operation.&lt;/p&gt;
&lt;p&gt;In this example, the role does not give the agent permission to query arbitrary tables, and it does not turn the MCP connection into an administrator connection.&lt;/p&gt;
&lt;h3&gt;Step 2: Write the tools in ObjectScript&lt;/h3&gt;
&lt;p&gt;The actual operations are implemented in &lt;code&gt;MCPData.Tools.HealthExample&lt;/code&gt;, which extends &lt;code&gt;%AI.Tool&lt;/code&gt;. Its methods that should become MCP tools are declared as &lt;code&gt;ClassMethods&lt;/code&gt; with the &lt;code&gt;WebMethod&lt;/code&gt; keyword.&lt;/p&gt;
&lt;p&gt;For example, &lt;code&gt;ListResources&lt;/code&gt; starts like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class MCPData.Tools.HealthExample Extends %AI.Tool&lt;br&gt;
{

&lt;p&gt;ClassMethod ListResources() As %DynamicObject [ WebMethod ]&lt;br&gt;
{&lt;br&gt;
    ...&lt;br&gt;
}&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;This class contains the application logic:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SearchPatients&lt;/code&gt; searches the synthetic patient table;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ReadGlobalData&lt;/code&gt; reads the approved global;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LargestGlobals&lt;/code&gt; reports estimated global sizes;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RecentApplicationErrors&lt;/code&gt; returns recent error summaries; and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ListResources&lt;/code&gt; describes the available data sources and operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The methods are deliberately narrow. They accept filters, paths, and limits defined by the application rather than arbitrary SQL statements or arbitrary global names. For example, &lt;code&gt;ReadGlobalData&lt;/code&gt; contains its own validation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;If path'="^ERRORS" Quit {"error":"Only ^ERRORS global is allowed"}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This prevents the method from reading a different global even if it is called with an unexpected value.&lt;/p&gt;
&lt;h3&gt;Step 3: Apply authorization before execution&lt;/h3&gt;
&lt;p&gt;The project defines &lt;code&gt;MCPData.Policy.Authorization&lt;/code&gt;, which implements the authorization logic shared by the ToolSet.&lt;/p&gt;
&lt;p&gt;The policy defines the tools that may be called:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set allowlist=$LISTBUILD(&lt;br&gt;&lt;br&gt;
    "ListResources",&lt;br&gt;&lt;br&gt;
    "SearchPatients",&lt;br&gt;&lt;br&gt;
    "ReadGlobalData",&lt;br&gt;&lt;br&gt;
    "LargestGlobals",&lt;br&gt;&lt;br&gt;
    "RecentApplicationErrors"&lt;br&gt;&lt;br&gt;
)

&lt;/code&gt;&lt;p&gt;&lt;code&gt;If $LISTFIND(allowlist,name)=0 {&lt;br&gt;&lt;br&gt;
    Quit ..Deny(name,call,"Tool is not allowlisted: "_name)&lt;br&gt;&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;This is a server-side decision in IRIS. The AI model is not trusted to decide which tools are safe, and a prompt cannot add a new capability.&lt;/p&gt;
&lt;p&gt;The policy then validates the arguments of permitted tools. For example, it can verify that &lt;code&gt;ReadGlobalData&lt;/code&gt; is limited to &lt;code&gt;^ERRORS&lt;/code&gt; or that monitoring requests remain inside the &lt;code&gt;MCP_EXAMPLE&lt;/code&gt; namespace.&lt;/p&gt;
&lt;p&gt;The result is a layered validation flow:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Authorization policy&lt;br&gt;&lt;br&gt;
    ↓&lt;br&gt;&lt;br&gt;
Is this tool allowed?&lt;br&gt;&lt;br&gt;
    ↓&lt;br&gt;&lt;br&gt;
Are its arguments within the approved scope?&lt;br&gt;&lt;br&gt;
    ↓&lt;br&gt;&lt;br&gt;
Tool implementation&lt;br&gt;&lt;br&gt;
    ↓&lt;br&gt;&lt;br&gt;
Are the inputs still valid before accessing data?&lt;br&gt;&lt;br&gt;
    ↓&lt;br&gt;&lt;br&gt;
IRIS database privileges&lt;br&gt;&lt;br&gt;
    ↓&lt;br&gt;&lt;br&gt;
Can the connected user perform the operation?&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Record successful and failed executions&lt;/h3&gt;
&lt;p&gt;Authorization answers the question&lt;em&gt; “may this operation run?”&lt;/em&gt;. Auditing answers a different question: &lt;em&gt;“what happened when the request was processed?”&lt;/em&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The project defines a second policy, &lt;code&gt;MCPData.Policy.Audit&lt;/code&gt;, which implements &lt;code&gt;%LogExecution&lt;/code&gt; and stores audit records in the persistent &lt;code&gt;MCPData.Data.Audit&lt;/code&gt; class.&lt;/p&gt;
&lt;p&gt;The policy records bounded metadata rather than copying the complete response returned to the agent.&lt;/p&gt;
&lt;p&gt;Depending on the tool, an audit record can include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the tool name;&lt;/li&gt;
&lt;li&gt;the execution timestamp;&lt;/li&gt;
&lt;li&gt;the execution status;&lt;/li&gt;
&lt;li&gt;the duration;&lt;/li&gt;
&lt;li&gt;the filters used for a patient search;&lt;/li&gt;
&lt;li&gt;an error message when execution fails.&lt;/li&gt;
&lt;/ul&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fraw%2Fmain%2Fpic%2Faudit_example.png" alt="IRIS Management Portal showing an audit record for a ListResources tool call" width="800" height="423"&gt;&lt;p&gt;The audit policy also records failed executions. In the example below, &lt;code&gt;SearchPatients&lt;/code&gt; received an invalid value for the &lt;code&gt;diabetic&lt;/code&gt; filter, the request failed validation and the error was stored in &lt;code&gt;StatusText&lt;/code&gt; field.&amp;nbsp;&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fraw%2Fmain%2Fpic%2Faudit_example_2.png" alt="IRIS audit table showing SQL and global tool calls, result counts, and a patient-filter validation error" width="800" height="331"&gt;&lt;h3&gt;Step 5: Attach the policies to a ToolSet&lt;/h3&gt;
&lt;p&gt;We now have separate components for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MCPData.Tools.HealthExample&lt;/code&gt;: application operations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MCPData.Policy.Authorization&lt;/code&gt;: permission and argument checks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MCPData.Policy.Audit&lt;/code&gt;: execution and result metadata&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How to connect these elements now? That is the responsibility of the &lt;strong&gt;ToolSet &lt;/strong&gt;&lt;code&gt;MCPData.ToolSet.HealthExample&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Its definition is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;ToolSet Name="CareOutreachData"&amp;gt; &lt;br&gt;&lt;br&gt;
    &amp;lt;Description&amp;gt; Read-only namespace monitoring and error inspection. &amp;lt;/Description&amp;gt; &lt;br&gt;&lt;br&gt;
    &amp;lt;Policies&amp;gt; &lt;br&gt;&lt;br&gt;
        &amp;lt;Authorization Class="MCPData.Policy.Authorization"/&amp;gt; &lt;br&gt;&lt;br&gt;
        &amp;lt;Audit Class="MCPData.Policy.Audit"/&amp;gt; &amp;lt;/Policies&amp;gt; &lt;br&gt;&lt;br&gt;
    &amp;lt;Include Class="MCPData.Tools.HealthExample"/&amp;gt; &lt;br&gt;&lt;br&gt;
&amp;lt;/ToolSet&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can observe this definition and see that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt;Include&amp;gt;&lt;/code&gt; element identifies the class containing the tools.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;&amp;lt;Policies&amp;gt;&lt;/code&gt; section attaches the authorization and audit behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;em&gt;ToolSet &lt;/em&gt;is therefore the point where the application declares which tools that are exposed and what are the policies that govern their execution.&lt;/p&gt;
&lt;h3&gt;Step 6: Expose the ToolSet through an MCP service&lt;/h3&gt;
&lt;p&gt;The ToolSet defines what is available and which policies apply, but an MCP service still needs to expose it to clients.&lt;/p&gt;
&lt;p&gt;In this project, that service is &lt;code&gt;MCPData.Service.HealthExample&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class MCPData.Service.HealthExample Extends %AI.MCP.Service&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
    Parameter SPECIFICATION As STRING = "MCPData.ToolSet.HealthExample";&lt;br&gt;&lt;br&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The service main responsibility is to point AI Hub to the ToolSet that should be exposed through the registered MCP endpoint.&lt;/p&gt;
&lt;p&gt;The complete structure is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MCPData.Service.HealthExample&lt;br&gt;&lt;br&gt;
        ↓&lt;br&gt;&lt;br&gt;
selects&lt;br&gt;&lt;br&gt;
        ↓&lt;br&gt;&lt;br&gt;
MCPData.ToolSet.HealthExample&lt;br&gt;&lt;br&gt;
        ↓&lt;br&gt;&lt;br&gt;
        ├── Authorization → MCPData.Policy.Authorization&lt;br&gt;&lt;br&gt;
        ├── Audit         → MCPData.Policy.Audit&lt;br&gt;&lt;br&gt;
        └── Tools         → MCPData.Tools.HealthExample&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding a method to the tool class does not automatically give an AI agent unrestricted access to IRIS. The method must be included in the ToolSet, the ToolSet must be exposed by the service, the authorization policy must allow the call and its arguments, and the connected IRIS user must have the required database privileges.&lt;/p&gt;
&lt;h2&gt;Security model in practice&lt;/h2&gt;
&lt;p&gt;Sensitive access is so controlled at several layers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The endpoint user receives only the &lt;code&gt;MCPDataReader&lt;/code&gt; role.&lt;/li&gt;
&lt;li&gt;That role receives &lt;code&gt;SELECT&lt;/code&gt; only on &lt;code&gt;MCPData_Data.Patient&lt;/code&gt;. Access to a database resource does not bypass IRIS SQL privileges.&lt;/li&gt;
&lt;li&gt;The authorization policy permits only five named tools.&lt;/li&gt;
&lt;li&gt;The tools accept filters and approved paths, not arbitrary SQL statements or global names.&lt;/li&gt;
&lt;li&gt;Query results, traversal depth, and monitoring results have hard limits.&lt;/li&gt;
&lt;li&gt;Monitoring remains inside the &lt;code&gt;MCP_EXAMPLE&lt;/code&gt; namespace and cannot inspect the full IRIS instance.&lt;/li&gt;
&lt;li&gt;The audit policy records the tool name, status, duration, and bounded result metadata.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Testing and examples&lt;/h2&gt;
&lt;p&gt;Once connected, you can test the MCP server and verify that its access policies are enforced by sending a series of requests.&lt;/p&gt;
&lt;p&gt;Try asking:&amp;nbsp;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;List all available resources and tools from the MCP server.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fraw%2Fmain%2Fpic%2Fexample_1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fraw%2Fmain%2Fpic%2Fexample_1.png" alt="Agent describing the approved SQL table, global, operations, and limits" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Find diabetic patients aged 60 or older with a Diabetes diagnosis. Return at most 10 records.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fraw%2Fmain%2Fpic%2Fexample_2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fraw%2Fmain%2Fpic%2Fexample_2.png" alt="Agent displaying synthetic patient records matching diagnosis and age filters" width="800" height="219"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Show me the 5 largest globals in the MCP_EXAMPLE namespace.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fraw%2Fmain%2Fpic%2Fexample_3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fraw%2Fmain%2Fpic%2Fexample_3.png" alt="Agent displaying estimated sizes for the five largest globals in MCP_EXAMPLE" width="800" height="212"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Let's try with a few requests outside the approved scope:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Execute &lt;code&gt;SELECT * FROM MCPData_Data.Patient&lt;/code&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fraw%2Fmain%2Fpic%2Fnot_allowed_1.png" alt="Agent explaining that arbitrary SQL is unavailable and suggesting SearchPatients" width="798" height="139"&gt;&lt;p&gt;As expected, this request is rejected because the agent has no tool that allows it to execute arbitrary SQL queries against the database.&lt;/p&gt;
&lt;p&gt;The same restriction applies to globals. In the following example, attempting to read a global outside the approved scope results in an access-denied error:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fraw%2Fmain%2Fpic%2Fnot_allowed_2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2Firis-mcp-data-exposure-toolkit%2Fraw%2Fmain%2Fpic%2Fnot_allowed_2.png" alt="Rejected read of a non-allowlisted global and explanation of the fixed namespace" width="800" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Connecting an AI agent to IRIS does not have to mean giving it general access to the database.&lt;/p&gt;
&lt;p&gt;The approach I explored in this project is to expose a small set of operations that make sense for the application and keep the boundaries around them in code. The agent can search the patient data, inspect the resources that have been explicitly made available, and retrieve some namespace information, but it cannot turn that access into arbitrary SQL queries or unrestricted global reads.&lt;/p&gt;
&lt;p&gt;AI Hub provides the pieces to structure this cleanly: ObjectScript methods implement the tools, the ToolSet groups them and attaches policies, authorization decides which calls and arguments are acceptable, and auditing records what actually happened. Underneath that, the normal IRIS security model still applies.&lt;/p&gt;
&lt;p&gt;The project is available on GitHub, and I hope it provides a useful starting point for experimenting with AI Hub and building more controlled MCP interfaces on top of InterSystems IRIS.&lt;/p&gt;


</description>
      <category>ai</category>
      <category>docker</category>
      <category>github</category>
      <category>vscode</category>
    </item>
    <item>
      <title>My First Agent Studio: building and testing native IRIS agents with AI Hub and Ollama</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Wed, 16 Sep 2026 17:54:29 +0000</pubDate>
      <link>https://dev.to/intersystems/my-first-agent-studio-building-and-testing-native-iris-agents-with-ai-hub-and-ollama-2dh8</link>
      <guid>https://dev.to/intersystems/my-first-agent-studio-building-and-testing-native-iris-agents-with-ai-hub-and-ollama-2dh8</guid>
      <description>&lt;p&gt;What does it take to go from an ObjectScript class to an agent that can call tools, use skills, and delegate part of a task?&lt;/p&gt;
&lt;p&gt;I built &lt;strong&gt;My First Agent Studio&lt;/strong&gt; to make that process easier to explore. It includes small native &lt;code&gt;%AI.Agent&lt;/code&gt; examples, terminal demos, a dataset, and a browser UI for testing the same agents interactively. Everything except the local model service is packaged in a Docker Compose project.&lt;/p&gt;
&lt;p&gt;The project is my contribution to the &lt;a href="https://community.intersystems.com/post/community-bounty-program-idea-application-%E2%80%94-round-2-live" rel="nofollow noopener noreferrer"&gt;InterSystems Community Bounty Program — Round 2&lt;/a&gt;. It brings together two ideas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://ideas.intersystems.com/ideas/DPI-I-986" rel="nofollow noopener noreferrer"&gt;My First Agent (End-To-End Starter)&lt;/a&gt;: learn the SDK through a small task involving tools, skills, an external MCP server, and sub-agents.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ideas.intersystems.com/ideas/DPI-I-984" rel="nofollow noopener noreferrer"&gt;Generic Agent Test UI for %AI.Agent&lt;/a&gt;: select an existing agent class and chat with it, using the behavior defined in that class.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The source is available in the &lt;a href="https://github.com/pietrodileo/My-First-Agent-Studio" rel="noopener noreferrer"&gt;My First Agent Studio repository&lt;/a&gt;, that contains the full setup instructions and examples&lt;/p&gt;
&lt;p&gt;This article focuses on how the pieces work together.&lt;/p&gt;
&lt;h2&gt;What the project runs&lt;/h2&gt;
&lt;p&gt;The agents run inside InterSystems IRIS using the AI Hub while Ollama supplies the LLM. I chose Ollama so developers can try these features without a paid model API or a cloud-provider API key. You still need enough local resources to run your chosen model, in particular, use a tool-capable model for the tool and delegation examples like Qwen 3.8.&lt;/p&gt;

&lt;h2&gt;Configure an agent, skills and tools through AI HUB&lt;/h2&gt;
&lt;p&gt;An agent in AI Hub is more than a connection to an LLM. It is the point where we define which model will do the reasoning, which instructions it should follow, and which capabilities it can use while processing a request.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;These capabilities are separated into two main concepts: &lt;strong&gt;tools&lt;/strong&gt; and &lt;strong&gt;skills&lt;/strong&gt;:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tools give the agent something it can do&lt;em&gt;,&lt;/em&gt; for example, query data stored in IRIS, perform a calculation, or invoke functionality exposed by an MCP server.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Skills give the agent additional instructions for how to approach a particular type of task.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The agent brings these pieces together, as in the following example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Test.Agent Extends %AI.Agent&lt;br&gt;
{&lt;br&gt;
Parameter TOOLSETS = "Test.ToolSet.Local";

&lt;/code&gt;&lt;p&gt;&lt;code&gt;Parameter SKILLS = "Test.Skill.Poet,Test.Skill.Echo";&lt;br&gt;&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;There is an important architectural distinction here: the LLM itself does not automatically have access to our IRIS data or application logic. By declaring the toolsets on the agent, we explicitly define the operations that AI Hub can make available to the model. Likewise, declaring a skill does not add another executable operation; it makes a reusable set of instructions available to the agent.&lt;/p&gt;
&lt;p&gt;This gives us a useful separation of responsibilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Agent → who is reasoning and what capabilities are available&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools → what the agent can do&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills → how the agent should approach a task&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Defining a skill&lt;/h3&gt;
&lt;p&gt;A skill is a reusable set of instructions that an agent can load when it needs to perform a particular kind of task. In AI Hub, we can define one by extending &lt;code&gt;%AI.Skill&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example, the project contains a simple &lt;code&gt;Poet&lt;/code&gt; skill:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Test.Skill.Poet Extends %AI.Agent.Skill &lt;br&gt;&lt;br&gt;
{ 

&lt;p&gt;XData SUMMARY [ MimeType="text/yaml" ]&lt;br&gt;
{&lt;br&gt;
name: talk-like-a-poet&lt;br&gt;
description: Transform responses into poetic verse.&lt;br&gt;
parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name: request
description: The user's request to be answered in poetic form
type: string
required: true
tags:&lt;/li&gt;
&lt;li&gt;poetry&lt;/li&gt;
&lt;li&gt;creative-writing&lt;/li&gt;
&lt;li&gt;literary-style&lt;/li&gt;
&lt;li&gt;verse&lt;/li&gt;
&lt;li&gt;rhyme
}&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;XData INSTRUCTIONS [ MimeType="text/markdown" ]&lt;br&gt;
{&lt;br&gt;
You are a poet. Respond to every request with the grace and precision of verse.&lt;br&gt;
}&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;There are three pieces worth noticing here:&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NAME&lt;/code&gt; is the identifier AI Hub uses for the skill&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DESCRIPTION&lt;/code&gt; tells the agent what the skill is intended for.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INSTRUCTIONS&lt;/code&gt; contains the actual instructions that are added when the skill is activated.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Defining a simple tool and a toolset&lt;/h3&gt;
&lt;p&gt;A tool gives the agent an operation it can actually execute.&lt;/p&gt;
&lt;p&gt;A native IRIS tool can be implemented by extending &lt;code&gt;%AI.Tool&lt;/code&gt; and exposing methods as tools. A deliberately simple example would look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Test.Tools.Calculator Extends %AI.Tool &lt;br&gt;&lt;br&gt;
{ 

&lt;p&gt;Method Add( a As %Numeric, b As %Numeric ) As %Numeric [ WebMethod ] &lt;br&gt;
{ &lt;br&gt;
    Return a + b &lt;br&gt;
} &lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;However, defining the &lt;code&gt;%AI.Tool&lt;/code&gt; class is only the first step. &lt;strong&gt;Tools are made available to an agent through a ToolSet.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A ToolSet acts as the container that groups related tools and gives AI Hub a unit that can be attached to an agent. For our calculator, we could define a small ToolSet such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Test.ToolSet.Local Extends %AI.ToolSet &lt;br&gt;&lt;br&gt;
{ 

&lt;p&gt;XData ToolSet &lt;br&gt;
{ &lt;br&gt;
&amp;lt;ToolSet Name="Local"&amp;gt; &lt;br&gt;
    &amp;lt;Tool Name="Calculator" &lt;br&gt;
        Class="Test.Tools.Calculator"/&amp;gt; &lt;br&gt;
    &amp;lt;/ToolSet&amp;gt; &lt;br&gt;
} &lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;This gives us a simple hierarchy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Agent&lt;br&gt;&lt;br&gt;
  │&lt;br&gt;&lt;br&gt;
  └── ToolSet&lt;br&gt;&lt;br&gt;
        │&lt;br&gt;&lt;br&gt;
        └── Tool&lt;br&gt;&lt;br&gt;
              │&lt;br&gt;&lt;br&gt;
              └── WebMethod&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The distinction is useful as an application grows. A &lt;code&gt;%AI.Tool&lt;/code&gt; class contains the &lt;strong&gt;implementation&lt;/strong&gt; of one or more related operations, while a &lt;code&gt;%AI.ToolSet&lt;/code&gt; describes the &lt;strong&gt;collection of tools&lt;/strong&gt; that should be exposed together. The agent can then reference the ToolSet rather than having to know about every individual tool implementation:&lt;/p&gt;
&lt;p&gt;If the model receives a question such as &lt;em&gt;"What is 15 + 18?"&lt;/em&gt;, it does not execute ObjectScript itself. AI Hub exposes the tool definition to the model, the model can decide to request &lt;code&gt;Add&lt;/code&gt; with the appropriate arguments, AI Hub executes the ObjectScript method, and the result is returned to the model so it can continue generating its answer.&lt;/p&gt;
&lt;h3&gt;Defining an agent&lt;/h3&gt;
&lt;p&gt;Beyond tools and skills, an agent also needs to know &lt;strong&gt;which model it will use and how AI Hub should access it&lt;/strong&gt;. AI Hub represents this connection through a &lt;strong&gt;provider&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A provider defines how AI Hub communicates with a model service. In this project we use &lt;strong&gt;Ollama&lt;/strong&gt;, running locally, through its OpenAI-compatible API. This allows us to use local models while keeping the interaction with the model behind the AI Hub provider abstraction.&lt;/p&gt;
&lt;p&gt;For example, the agent can initialize its provider as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Method %OnInit() As %Status&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
  Set sc=##super()&lt;br&gt;&lt;br&gt;
  If $$$ISERR(sc) Quit sc

&lt;p&gt;If '$ISOBJECT(..Provider) {&lt;br&gt;
    Set base=$SYSTEM.Util.GetEnviron("OLLAMA_BASE_URL")&lt;br&gt;
    Set ..Provider=##class(%AI.Provider).Create("openai",{"api_key":"ollama","base_url":(base)})&lt;br&gt;
    Set ..Model=$SYSTEM.Util.GetEnviron("OLLAMA_MODEL")&lt;br&gt;
    If ..Model="" Quit $$$ERROR($$$GeneralError,"OLLAMA_MODEL is required")&lt;br&gt;
  }&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;If $$$ISERR(sc) Quit sc&lt;br&gt;&lt;br&gt;
  Quit $$$OK&lt;br&gt;&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;Bringing all pieces together, we can define an agent as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Test.Agent Extends %AI.Agent&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
Parameter TOOLSETS = "Test.ToolSet.Local";

&lt;p&gt;Parameter SKILLS = "Test.Skill.Poet,Test.Skill.Echo";&lt;/p&gt;

&lt;p&gt;Method %OnInit() As %Status&lt;br&gt;
{&lt;br&gt;
  Set sc=##super()&lt;br&gt;
  If $$$ISERR(sc) Quit sc&lt;/p&gt;

&lt;p&gt;If '$ISOBJECT(..Provider) {&lt;br&gt;
    Set base=$SYSTEM.Util.GetEnviron("OLLAMA_BASE_URL")&lt;br&gt;
    Set ..Provider=##class(%AI.Provider).Create("openai",{"api_key":"ollama","base_url":(base)})&lt;br&gt;
    Set ..Model=$SYSTEM.Util.GetEnviron("OLLAMA_MODEL")&lt;br&gt;
    If ..Model="" Quit $$$ERROR($$$GeneralError,"OLLAMA_MODEL is required")&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;If $$$ISERR(sc) Quit sc&lt;br&gt;
  Quit $$$OK&lt;br&gt;
}&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;h2&gt;Connecting an external MCP server&lt;/h2&gt;
&lt;p&gt;Not every tool available to an agent has to be implemented in ObjectScript.&lt;/p&gt;
&lt;p&gt;AI Hub can also use tools provided by an external &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; server. In this project I included a small Python MCP server (&lt;code&gt;mcp-python-server.py&lt;/code&gt;), which uses &lt;em&gt;FastMCP&lt;/em&gt; to expose Python functions through the MCP, so that the complete integration can be tested locally.&lt;/p&gt;
&lt;p&gt;If you want to learn more about MCP servers, check my latest article: &lt;a href="https://community.intersystems.com/post/model-context-protocol-mcp-intersystems-iris-zero-hero" rel="noopener noreferrer"&gt;Model Context Protocol (MCP) with InterSystems IRIS - From Zero to Hero&lt;/a&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;For example, the MCP server can expose simple arithmetic operations:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from fastmcp import FastMCP

&lt;p&gt;mcp = FastMCP("HealthcareStatistics")&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;@mcp.tool()&lt;br&gt;&lt;br&gt;
def add_numbers(first: float, second: float) -&amp;gt; float:&lt;br&gt;&lt;br&gt;
    """Add two numbers."""&lt;br&gt;&lt;br&gt;
    return first + second&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;On the IRIS side, because this example uses an MCP server over &lt;code&gt;stdio&lt;/code&gt;, the Python script must be available in the same runtime environment as IRIS. In this project, &lt;code&gt;mcp-python-server.py&lt;/code&gt; is copied into the IRIS container, where AI Hub can start it using the IRIS Python interpreter. We then connect that MCP server to AI Hub through a &lt;code&gt;%AI.ToolSet&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// External Python MCP toolset for deterministic arithmetic and synthetic-data statistics..&lt;br&gt;&lt;br&gt;
Class Test.ToolSet.StatisticsMCP Extends %AI.ToolSet&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
XData Definition [ MimeType=application/xml ]&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
&amp;lt;ToolSet Name="StatisticsMCP"&amp;gt;&lt;br&gt;&lt;br&gt;
  &amp;lt;Description&amp;gt;Deterministic arithmetic and synthetic-data statistics from an external Python MCP process.&amp;lt;/Description&amp;gt;&lt;br&gt;&lt;br&gt;
  &amp;lt;MCP Name="Statistics"&amp;gt;&lt;br&gt;&lt;br&gt;
    &amp;lt;Stdio Executable="/usr/irissys/bin/irispython"&lt;br&gt;&lt;br&gt;
           Args="/home/irisowner/dev/mcp-python-server.py"/&amp;gt;&lt;br&gt;&lt;br&gt;
  &amp;lt;/MCP&amp;gt;&lt;br&gt;&lt;br&gt;
&amp;lt;/ToolSet&amp;gt;&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is still a normal &lt;code&gt;%AI.ToolSet&lt;/code&gt;, so it can be attached to an agent in the same way as the other ToolSets. The only difference is that, instead of declaring a native ObjectScript tool implementation, the ToolSet contains an &lt;code&gt;&amp;lt;MCP&amp;gt;&lt;/code&gt; definition, while &lt;code&gt;Executable&lt;/code&gt; points to the Python interpreter available inside the IRIS container and &lt;code&gt;Args&lt;/code&gt; identifies the Python MCP server that should be executed.&lt;/p&gt;
&lt;h2&gt;Starting a conversation: sessions, monitoring and chat&lt;/h2&gt;
&lt;p&gt;Once the agent has been configured, the next step is to actually use it. AI Hub separates the definition of an agent from an individual conversation with that agent.&lt;/p&gt;
&lt;p&gt;First we create and initialize the agent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set agent = ##class(Test.Agent).%New()&lt;br&gt;&lt;br&gt;
Set sc = agent.%Init()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Test.Agent&lt;/code&gt; class tells AI Hub which model, instructions, skills and ToolSets are available. It does not, however, represent a particular conversation. For that we create a &lt;strong&gt;session&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set session = agent.CreateSession()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A session represents the state of one conversation with the agent. This distinction becomes important as soon as we want to send more than one message.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set response = agent.Run(session,"Find the diabetic patients",10)&lt;br&gt;&lt;br&gt;
Do ##class(%AI.System).RenderMarkdown(response)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Run()&lt;/code&gt; starts the agent loop for the supplied prompt. AI Hub sends the conversation and available capabilities to the model and processes the response. If the model requests a tool, AI Hub executes it, adds the tool result to the conversation and lets the model continue.&lt;/p&gt;
&lt;p&gt;The result is therefore not necessarily produced by a single LLM request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User prompt&lt;br&gt;&lt;br&gt;
    │&lt;br&gt;&lt;br&gt;
    ▼&lt;br&gt;&lt;br&gt;
   LLM&lt;br&gt;&lt;br&gt;
    │&lt;br&gt;&lt;br&gt;
    ├── requests a tool&lt;br&gt;&lt;br&gt;
    │         │&lt;br&gt;&lt;br&gt;
    │         ▼&lt;br&gt;&lt;br&gt;
    │      AI Hub&lt;br&gt;&lt;br&gt;
    │         │&lt;br&gt;&lt;br&gt;
    │         ▼&lt;br&gt;&lt;br&gt;
    │    tool executes&lt;br&gt;&lt;br&gt;
    │         │&lt;br&gt;&lt;br&gt;
    ◄─────────┘&lt;br&gt;&lt;br&gt;
    │&lt;br&gt;&lt;br&gt;
    ▼&lt;br&gt;&lt;br&gt;
   LLM&lt;br&gt;&lt;br&gt;
    │&lt;br&gt;&lt;br&gt;
    ▼&lt;br&gt;&lt;br&gt;
Final response&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This loop is one of the reasons for passing a maximum number of iterations to &lt;code&gt;Run()&lt;/code&gt;. In the example above, &lt;code&gt;10&lt;/code&gt; gives the agent enough room to make intermediate tool calls without allowing the execution loop to continue indefinitely.&lt;/p&gt;
&lt;h3&gt;Continuing the same conversation&lt;/h3&gt;
&lt;p&gt;The session becomes particularly useful when we call the agent again.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set response = agent.Run(session,"Now summarize their main characteristics",10)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because we pass the &lt;strong&gt;same session&lt;/strong&gt;, this is a continuation of the previous conversation. The model can work with the context already accumulated in that session rather than treating the second prompt as an unrelated request.&lt;/p&gt;
&lt;p&gt;If instead we create another session:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set anotherSession = agent.CreateSession()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we have started a new conversation, even though both sessions use the same &lt;code&gt;Test.Agent&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The distinction can therefore be summarized as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Test.Agent&lt;br&gt;&lt;br&gt;
    │&lt;br&gt;&lt;br&gt;
    ├── Session A&lt;br&gt;&lt;br&gt;
    │     ├── User message&lt;br&gt;&lt;br&gt;
    │     ├── Tool call&lt;br&gt;&lt;br&gt;
    │     ├── Tool result&lt;br&gt;&lt;br&gt;
    │     └── Assistant response&lt;br&gt;&lt;br&gt;
    │&lt;br&gt;&lt;br&gt;
    └── Session B&lt;br&gt;&lt;br&gt;
          └── Independent conversation&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So, we can say that the agent defines the capabilities; the session holds the conversation using those capabilities.&lt;/p&gt;
&lt;h3&gt;Seeing what the agent is doing with a monitor&lt;/h3&gt;
&lt;p&gt;When developing an agent, looking only at the final response tells us only part of the story. We also want to understand &lt;strong&gt;how the agent arrived at that response&lt;/strong&gt;: how many iterations were required, whether tools were called, and how much work was performed before the final answer was produced.&lt;/p&gt;
&lt;p&gt;In this project I defined a specific monitor class:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Test.DemoMonitor Extends %RegisteredObject&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
Method OnIterationStart(iteration As %Integer, maxIterations As %Integer, session As %AI.Agent.Session)&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
  Write !,"[agent iteration ",iteration,"/",maxIterations,"]",!&lt;br&gt;&lt;br&gt;
}

&lt;p&gt;Method OnIterationComplete(iteration As %Integer, response As %AI.LLM.Response, session As %AI.Agent.Session)&lt;br&gt;
{&lt;br&gt;
  Set stats=session.GetStats()&lt;br&gt;
  Set totalToolCalls=stats.%Get("total_tool_calls",0)&lt;br&gt;
  Set totalToolDuration=stats.%Get("total_tool_duration_ms",0)&lt;br&gt;
  Write "Token usage: ",response.Usage.%ToJSON(),!&lt;br&gt;
  Write "Total tool calls: ",totalToolCalls," | Total tool duration (ms): ",totalToolDuration,!&lt;br&gt;
}&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;This use it we can create a monitor and pass it to &lt;code&gt;Run()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set monitor = ##class(%AI.Agent.Monitor).%New()&lt;br&gt;&lt;br&gt;
Set response = agent.Run(session,"Find diabetic patients and summarize the cohort",10,monitor)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The monitor lets us observe the execution of the agent loop instead of seeing only the final response, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[agent iteration 1/5] &lt;br&gt;&lt;br&gt;
Token usage: {"completion_tokens":81,"prompt_tokens":1267} &lt;br&gt;&lt;br&gt;
Total tool calls: 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Monitoring the execution helps us distinguish between these cases.&lt;/p&gt;
&lt;h3&gt;Inspecting the session statistics&lt;/h3&gt;
&lt;p&gt;The monitor shows what is happening &lt;strong&gt;during&lt;/strong&gt; execution. AI Hub also gives us a second view through the statistics stored on the session:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Write "Parent stats: ",session.GetStats().%ToJSON(),!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because the same session is reused for several prompts, &lt;code&gt;GetStats()&lt;/code&gt; gives us the accumulated statistics for that conversation rather than only information about the last prompt.&lt;/p&gt;
&lt;h2&gt;Delegating work to sub-agents&lt;/h2&gt;
&lt;p&gt;So far our agent has handled a request by interacting with the model and calling tools. AI Hub also allows an agent to &lt;strong&gt;delegate part of its work to another agent&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is useful when a task benefits from a separate role, set of instructions, or context. Instead of asking the main agent to perform every step itself, it can create a sub-agent, give it a specific task, wait for its response, and then continue its own execution.&lt;/p&gt;
&lt;p&gt;In this project I use two forms of delegation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Predefined sub-agents&lt;/strong&gt;, where the specialist role and instructions are defined in advance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic delegation&lt;/strong&gt;, where the parent creates a specialist dynamically for a task.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In both cases, I use the same pattern as for the rest of the project: &lt;strong&gt;delegation is exposed as a tool &lt;/strong&gt;(&lt;em&gt;agent as a tool&lt;/em&gt; pattern). The parent agent sees a normal tool call, while the implementation behind that tool creates and runs a &lt;code&gt;%AI.Agent.SubAgent&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Defining a predefined sub-agent&lt;/h3&gt;
&lt;p&gt;A predefined sub-agent is useful when we already know that part of our workflow should be handled by a specialist. For example, we may want a reviewer, validator, summarizer, or domain-specific analyst whose role and instructions are controlled by the application rather than generated dynamically by the parent agent.&lt;/p&gt;
&lt;p&gt;A simplified delegation tool looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class MyApp.SubAgents.Reviewer Extends %AI.Tool&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
Parameter DESCRIPTION = "Delegate content to a specialized child agent for review.";

&lt;p&gt;Property ParentAgent As %AI.Agent;&lt;/p&gt;

&lt;p&gt;Method %OnNew(parentAgent As %AI.Agent = "") As %Status [ Private ]&lt;br&gt;
{&lt;br&gt;
  If $ISOBJECT(parentAgent) {&lt;br&gt;
    Set ..ParentAgent=parentAgent&lt;br&gt;
  }&lt;br&gt;
  Quit $$$OK&lt;br&gt;
}&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;/// Review a task from the parent agent.&lt;br&gt;&lt;br&gt;
///&lt;br&gt;&lt;br&gt;
/// Args:&lt;br&gt;&lt;br&gt;
///     task: task to be reviwed&lt;br&gt;&lt;br&gt;
Method Review(task As %String) As %String [ WebMethod ]&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
  Set output = ""&lt;br&gt;&lt;br&gt;
  Try {&lt;br&gt;&lt;br&gt;
    If '$ISOBJECT(..ParentAgent) {&lt;br&gt;&lt;br&gt;
      $$$ThrowStatus($$$ERROR($$$AICoreIncompleteInitialization,$CLASSNAME(),"ParentAgent"))&lt;br&gt;&lt;br&gt;
    }&lt;br&gt;&lt;br&gt;
    Write "["&lt;em&gt;..%ClassName()&lt;/em&gt;"] called as tool",!&lt;br&gt;&lt;br&gt;
    Set prompt = "You are a specialist reviewer. Review only the supplied content and provide a concise assessment."&lt;br&gt;&lt;br&gt;
    Set subagent=##class(%AI.Agent.SubAgent).Create(..ParentAgent,prompt,"")&lt;br&gt;&lt;br&gt;
    Set subagent.ToolManager=##class(%AI.ToolMgr).%New()&lt;br&gt;&lt;br&gt;
    Set session=subagent.CreateSession()&lt;br&gt;&lt;br&gt;
    Set response=subagent.Run(session,task)&lt;br&gt;&lt;br&gt;
    Set output = response.Content&lt;br&gt;&lt;br&gt;
  } Catch exception {&lt;br&gt;&lt;br&gt;
    Set sc = exception.AsStatus()&lt;br&gt;&lt;br&gt;
    Set output = "Review failed: "_$SYSTEM.Status.GetErrorText(sc)&lt;br&gt;&lt;br&gt;
  }&lt;br&gt;&lt;br&gt;
  Return output&lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;From AI Hub's point of view, &lt;code&gt;Review()&lt;/code&gt; is still simply a tool operation because it is exposed as a &lt;code&gt;[ WebMethod ]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The child agent must be created in the context of the parent agent. For this reason, the delegation tool keeps a reference to its parent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Property ParentAgent As %AI.Agent;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generic Delegation&lt;/h3&gt;
&lt;p&gt;Predefined delegation works well when we already know which specialist we want to call. In other cases, however, the required specialist depends on the current task.&lt;/p&gt;
&lt;p&gt;For this reason, the project also includes a &lt;strong&gt;generic delegation tool &lt;/strong&gt;(that can be found in the EAP GitHub as well). The parent agent does not call a fixed reviewer or validator. Instead, it asks the delegation tool to create a child agent for a role described at runtime:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// Delegate Task Tool - Demonstrates Recursive Language Model (RLM) pattern&lt;br&gt;&lt;br&gt;
/// This tool creates a sub-agent to handle a delegated task&lt;br&gt;&lt;br&gt;
Class Test.Tools.DelegateTasks Extends %AI.Tool&lt;br&gt;&lt;br&gt;
{

&lt;p&gt;/// Tool description for LLM&lt;br&gt;
Parameter DESCRIPTION = "Delegate a task to a specialized sub-agent. Use this when the current task has distinct subtasks that would benefit from focused attention.";&lt;/p&gt;

&lt;p&gt;/// Parent agent reference (set by the agent using this tool)&lt;br&gt;
Property ParentAgent As %AI.Agent;&lt;/p&gt;

&lt;p&gt;/// Create the delegation tool already bound to its parent agent.&lt;br&gt;
Method %OnNew(parentAgent As %AI.Agent = "") As %Status [ Private ]&lt;br&gt;
{&lt;br&gt;
    If $ISOBJECT(parentAgent) {&lt;br&gt;
        Set ..ParentAgent = parentAgent&lt;br&gt;
    }&lt;br&gt;
    Quit $$$OK&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;/// Delegate a task to a specialized sub-agent&lt;br&gt;
///&lt;br&gt;
/// Use this when the current task has distinct subtasks that would benefit from focused attention.&lt;br&gt;
/// The sub-agent will have its own conversation context and can use the same tools as the parent.&lt;br&gt;
///&lt;br&gt;
/// Parameters:&lt;br&gt;
/// - task: The task to delegate to the sub-agent (required)&lt;br&gt;
/// - specialistRole: The role/expertise of the sub-agent (e.g., "code reviewer", "writer", "data analyst")&lt;br&gt;
/// - context: Additional context to pass to the sub-agent&lt;br&gt;
Method Execute(task As %String, specialistRole As %String = "", context As %String = "") As %String [ WebMethod ]&lt;br&gt;
{&lt;br&gt;
    Try {&lt;br&gt;
        If '$ISOBJECT(..ParentAgent) {&lt;br&gt;
            $$$ThrowStatus($$$ERROR($$$AICoreIncompleteInitialization, $CLASSNAME(), "ParentAgent"))&lt;br&gt;
        }&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    Write !, "[Delegation] Creating sub-agent with role '"_specialistRole_"' for task: ", $EXTRACT(task, 1, 50), "...", !

    // Build system prompt for sub-agent
    Set systemPrompt = "You are a helpful assistant"
    If specialistRole '= "" {
        Set systemPrompt = "You are a " _ specialistRole _ " assistant"
    }
    If context '= "" {
        Set systemPrompt = systemPrompt _ ". Context: " _ context
    }

    Write "[Delegation] Sub-agent role: ", systemPrompt, !

    // Create sub-agent using CreateSubAgent API
    // This will use safe_block_on internally to handle nested runtime contexts
    Set subagent = ##class(%AI.Agent.SubAgent).Create(
        ..ParentAgent,
        systemPrompt,
        "" // No additional config for now
    )

    Write "[Delegation] Sub-agent created, executing task...", !

    // Run the sub-agent on the delegated task
    Set session = subagent.CreateSession()
    Set response = subagent.Run(session, task)

    Write "[Delegation] Sub-agent completed", !
    Write "[Delegation] Response length: ", $LENGTH(response.Content), " characters", !

    Return response.Content

} Catch ex {
    Return "Error in delegation: " _ ex.DisplayString()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;From the parent model's perspective, &lt;code&gt;Execute()&lt;/code&gt; is simply another tool. The interesting part is that &lt;code&gt;specialistRole&lt;/code&gt; is not fixed when the application is written.&lt;/p&gt;
&lt;p&gt;For example, the parent could request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;specialistRole = "mental-math teacher"&lt;br&gt;&lt;br&gt;
task = "Explain why multiplication by zero always returns zero."&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In another conversation, the same tool could be called with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;specialistRole = "technical editor"&lt;br&gt;&lt;br&gt;
task = "Review this explanation for clarity."&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both calls use the same delegation implementation, but they create child agents with different responsibilities.&lt;/p&gt;
&lt;h3&gt;Registering a generic delegation tool without a Toolset&lt;/h3&gt;
&lt;p&gt;If we do not want to create a specific Toolset containing sub-agents or delegation tool, we can register a single generic tool directly when the parent agent is initialized:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Method %OnInit() As %Status&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
  ......&lt;br&gt;&lt;br&gt;
  Do things&lt;br&gt;&lt;br&gt;
  ......&lt;br&gt;&lt;br&gt;
  Set sc=..RegisterGenericDelegateTool()&lt;br&gt;&lt;br&gt;
  If $$$ISERR(sc) Quit sc&lt;br&gt;&lt;br&gt;
  Quit $$$OK&lt;br&gt;&lt;br&gt;
}

&lt;/code&gt;&lt;p&gt;&lt;code&gt;Method RegisterGenericDelegateTool()&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
  Set delegateTool=##class(Test.Tools.DelegateTasks).%New($THIS)&lt;br&gt;&lt;br&gt;
  Do ..ToolManager.AddTool(delegateTool)&lt;br&gt;&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;

&lt;h2&gt;Start the environment&lt;/h2&gt;
&lt;p&gt;Now that we have covered the main building blocks, it is time to run the project and see them working together.&lt;/p&gt;
&lt;h2&gt;A first conversation in ObjectScript&lt;/h2&gt;
&lt;p&gt;Before opening the browser, it is useful to run one conversation directly. This makes the distinction between the agent and its session explicit.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Open IRIS terminal and create a bundled simple chat agent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set agent=##class(Test.Agents.SimpleAgent).%New()&lt;br&gt;&lt;br&gt;
Set sc=agent.%Init()&lt;br&gt;&lt;br&gt;
Do $SYSTEM.Status.DisplayError(sc)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;%Init()&lt;/code&gt; prepares the agent configuration and, in this project, also registers the generic and predefined delegation tools with the current agent instance.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set session=agent.CreateSession()&lt;br&gt;&lt;br&gt;
Set maxIterations=5&lt;br&gt;&lt;br&gt;
Set monitor=##class(Test.DemoMonitor).%New()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CreateSession()&lt;/code&gt; creates the conversation context.&lt;/p&gt;
&lt;p&gt;We can run the first request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set prompt="I have three tasks to finish today. Can you help me decide which one to do first?"&lt;br&gt;&lt;br&gt;
Set response=agent.Run(session,prompt,maxIterations,monitor)&lt;br&gt;&lt;br&gt;
Do ##class(%AI.System).RenderMarkdown(response.Content)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Run()&lt;/code&gt; submits a prompt, with an iteration limit and the demo monitor.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The returned response exposes its text through &lt;code&gt;Content&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Keep the same session for a follow-up:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set prompt="One task has a deadline today; the other two can wait until Friday."&lt;br&gt;&lt;br&gt;
Set response=agent.Run(session,prompt,maxIterations,monitor)&lt;br&gt;&lt;br&gt;
Do ##class(%AI.System).RenderMarkdown(response.Content)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also register a skill from the terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set sc=agent.UseSkill("Test.Skill.Caveman")&lt;br&gt;&lt;br&gt;
Set prompt="Load the Caveman skill and explain your previous answer briefly."&lt;br&gt;&lt;br&gt;
Set response=agent.Run(session,prompt,maxIterations,monitor)&lt;br&gt;&lt;br&gt;
Write session.ActiveSkills.%ToJSON(),!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;UseSkill()&lt;/code&gt; makes the skill available to the agent programmatically, while &lt;code&gt;session.ActiveSkills&lt;/code&gt; lets us inspect which skills are active in the current conversation. This is useful when we want the application, rather than the model, to decide that a particular behavior should be applied.&lt;/p&gt;We can inspect the tools exposed to the model at any time:&lt;pre&gt;&lt;code&gt;Write agent.ToolManager.%Discover().%ToJSON(),!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The next step is to use delegation. Suppose we want a sub-agent to review the previous response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set prompt="Call Execute to delegate review of this result to a concise technical reviewer. Use specialistRole='technical reviewer'. Task: "_response.Content&lt;br&gt;&lt;br&gt;
Set response=agent.Run(session,prompt,maxIterations,monitor)&lt;br&gt;&lt;br&gt;
Do ##class(%AI.System).RenderMarkdown(response.Content)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After these interactions, we can inspect the sub-agents created by the parent:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Write "Spawned sub-agents: ",agent.SubAgents.Count(),!

&lt;/code&gt;&lt;p&gt;&lt;code&gt;For i=1:1:agent.SubAgents.Count() {&lt;br&gt;&lt;br&gt;
    Write !,agent.SubAgents.GetAt(i).SystemPrompt,!&lt;br&gt;&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;This is particularly useful with generic delegation because the system prompt shows the role that was assigned to each dynamically created specialist.
&lt;h2&gt;Testing agents in the browser&lt;/h2&gt;
&lt;p&gt;Once the terminal flow is clear, the browser UI gives us a more convenient way to repeat the same experiments interactively.&lt;/p&gt;
&lt;p&gt;The important point is that the browser does &lt;strong&gt;not&lt;/strong&gt; define another kind of agent. The agent classes still live in ObjectScript, run inside IRIS, and use the same tools, skills, sessions, and delegation logic described earlier.&lt;/p&gt;
&lt;h3&gt;Opening the Agent Studio&lt;/h3&gt;
&lt;p&gt;Run the docker compose and open UI at &lt;a href="http://localhost:5174" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="http://localhost:5174" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="http://localhost:5174" rel="noopener noreferrer"&gt;http://localhost:5174&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The initial workspace is divided into two main areas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sidebar&lt;ul&gt;
&lt;li&gt;Recent chats&lt;/li&gt;
&lt;li&gt;Configuration&lt;/li&gt;
&lt;li&gt;Selected Agent&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Main chat&lt;/li&gt;
&lt;/ul&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2FMy-First-Agent-Studio%2Fblob%2Fmain%2Fpic%2F1_homepage.png%3Fraw%3Dtrue" alt="1_homepage.png" width="799" height="421"&gt;&lt;h3&gt;Selecting an existing &lt;code&gt;%AI.Agent&lt;/code&gt;
&lt;/h3&gt;
&lt;p&gt;The agent selector is populated from the agent classes installed in IRIS. The frontend receives from the backend the concrete &lt;code&gt;%AI.Agent&lt;/code&gt; subclasses and their metadata such as description, example prompt, ToolSets and skills.&lt;/p&gt;
&lt;h3&gt;Choosing the model&lt;/h3&gt;
&lt;p&gt;The model selector follows a similar approach. Instead of maintaining a predefined list of model names, the UI queries the configured Ollama instance and presents the models that are currently installed.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2FMy-First-Agent-Studio%2Fraw%2Fmain%2Fpic%2F5_model_choice.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2FMy-First-Agent-Studio%2Fraw%2Fmain%2Fpic%2F5_model_choice.png" alt="Model selector populated with models from the configured Ollama instance" width="722" height="1266"&gt;&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The agent logic remains unchanged while only the model performing the reasoning changes.&lt;/p&gt;
&lt;h3&gt;Starting a conversation&lt;/h3&gt;
&lt;p&gt;Once the agent and model are selected, we can send a prompt exactly as we did from the terminal.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The browser sends the request to IRIS, where the selected agent is instantiated and the normal AI Hub execution loop runs, and finally it displays the result of the same agent execution we could inspect from ObjectScript.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2FMy-First-Agent-Studio%2Fraw%2Fmain%2Fpic%2F2_chat_example.png" alt="IRISAgent response listing globals and estimated sizes" width="799" height="421"&gt;&lt;h3&gt;Loading a skill from the UI&lt;/h3&gt;
&lt;p&gt;Skills can also be controlled directly from the browser. New conversations start without active skills. Selecting a skill and clicking &lt;strong&gt;Load&lt;/strong&gt; sends an explicit request to IRIS to activate that skill for the conversation.&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2FMy-First-Agent-Studio%2Fraw%2Fmain%2Fpic%2F4_skill_loading.png" alt="Caveman loaded with an immediate Studio confirmation" width="799" height="421"&gt;&lt;p&gt;The UI then marks the skill as active and changes the available action to &lt;strong&gt;Unload&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;An example of a skill usage&lt;/h3&gt;
&lt;p&gt;After the skill has been loaded, we can use it:&lt;/p&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fpietrodileo%2FMy-First-Agent-Studio%2Fraw%2Fmain%2Fpic%2F3_example_talk_like_a_poet.png" alt="SimpleAgent answering a calculation prompt with Poet active" width="799" height="421"&gt;&lt;h3&gt;Reopening conversations&lt;/h3&gt;
&lt;p&gt;The UI stores conversations and their AI Hub session state in IRIS. The &lt;strong&gt;Recent chats&lt;/strong&gt; panel lets us reopen a previous conversation instead of starting over.&lt;/p&gt;

&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The goal of this project is to make the different pieces of AI Hub easier to explore together rather than as isolated features.&lt;/p&gt;
&lt;p&gt;Starting from a native &lt;code&gt;%AI.Agent&lt;/code&gt; class, we can configure a model provider, expose ObjectScript and MCP tools, add reusable skills, maintain conversations through sessions, inspect execution statistics, and delegate work to specialized sub-agents.&lt;/p&gt;
&lt;p&gt;The project is intentionally a playground rather than a production architecture. The AI Hub APIs used here are part of the Early Access preview and may evolve, but the examples provide a practical starting point for experimenting with the SDK and understanding how its main building blocks relate to one another.&lt;/p&gt;
&lt;p&gt;If you are exploring AI Hub, clone the repository, try the bundled agents, inspect their available tools, change a skill, experiment with delegation, and then add your own &lt;code&gt;%AI.Agent&lt;/code&gt; class. That is where the project becomes most useful: not as a finished application, but as a small environment for learning what you can build next.&lt;/p&gt;


</description>
      <category>ai</category>
      <category>docker</category>
      <category>github</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
