<?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</title>
    <description>The latest articles on DEV Community by InterSystems (intersystems).</description>
    <link>https://dev.to/intersystems</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%2Forganization%2Fprofile_image%2F2450%2F5c611adb-602d-4948-b84b-5fe47046fd5c.png</url>
      <title>DEV Community: InterSystems</title>
      <link>https://dev.to/intersystems</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/intersystems"/>
    <language>en</language>
    <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>
    <item>
      <title>Working with Dynamic Objects in IRIS: doubts that only come up when you need to deploy by tomorrow morning and it's 11 P.M.</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 30 Aug 2026 07:37:14 +0000</pubDate>
      <link>https://dev.to/intersystems/working-with-dynamic-objects-in-iris-doubts-that-only-come-up-when-you-need-to-deploy-by-tomorrow-4an0</link>
      <guid>https://dev.to/intersystems/working-with-dynamic-objects-in-iris-doubts-that-only-come-up-when-you-need-to-deploy-by-tomorrow-4an0</guid>
      <description>&lt;p&gt;If you have spent any time on the Developer Community, you have seen the many questions return in different costumes: &lt;em&gt;How do I turn a persistent object into JSON? How do I loop over a JSON payload whose shape I don't know? Why does &lt;/em&gt;&lt;code&gt;&lt;em&gt;Required&lt;/em&gt;&lt;/code&gt;&lt;em&gt; do nothing on my &lt;/em&gt;&lt;code&gt;&lt;em&gt;%DynamicArray&lt;/em&gt;&lt;/code&gt;&lt;em&gt; property? Why is my date coming back as &lt;/em&gt;&lt;code&gt;&lt;em&gt;31390&lt;/em&gt;&lt;/code&gt;&lt;em&gt;?&lt;/em&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Noticing that, we decided to write an article summarizing all the questions that come with something that the documentation alone couldn't provide: practice.&lt;/p&gt;
&lt;h2&gt;Let's start with the foundation&lt;/h2&gt;
&lt;p&gt;InterSystems IRIS gives you two classes for schema-less data: &lt;code&gt;%DynamicObject&lt;/code&gt; and &lt;code&gt;%DynamicArray&lt;/code&gt;. Both inherit from &lt;code&gt;%DynamicAbstractObject&lt;/code&gt;, and instances of either are called &lt;em&gt;dynamic entities&lt;/em&gt;. They map cleanly onto JSON: an object is a set of key/value pairs, an array is an ordered list. Unlike a persistent or registered class, a dynamic object keeps no predefined list of valid property names — every string key is legal, and you add or remove members at runtime.&lt;/p&gt;
&lt;p&gt;The most pleasant part is the literal syntax, which will look familiar to anyone coming from JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set person = {"name":"Ada","active":true,"roles":["admin","dev"]}
Set scores = [90, 85, 77]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also build them field by field. &lt;code&gt;%Set()&lt;/code&gt; returns the entity it modified, so calls chain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set obj = {}.%Set("a",1).%Set("b",2) // now obj is {"a":1,"b":2}

Do scores.%Push(100)                 // now scores is [90, 85, 77, 100]

Set last = scores.%Pop()             // now scores is [90, 85, 77] and last is 100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;%Push()&lt;/code&gt; and &lt;code&gt;%Pop()&lt;/code&gt; exist only on arrays, but everything else works on both. To turn text into an entity, use &lt;code&gt;%FromJSON()&lt;/code&gt; and to serialize back, use &lt;code&gt;%ToJSON()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set jsonString = "{""field"": ""value""}"
Set obj = {}.%FromJSON(jsonString) // now obj is {"field": "value"}

Write obj.%ToJSON()                // this outputs "{""field"": ""value""}"&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;%FromJSON()&lt;/code&gt; also accepts a stream, and &lt;code&gt;%FromJSONFile()&lt;/code&gt; reads straight from a filename (note: a filename string, rather than a &lt;code&gt;%File&lt;/code&gt; object — a common trip-up).&lt;/p&gt;
&lt;h2&gt;Iterating over structure you don't control&lt;/h2&gt;
&lt;p&gt;When a payload arrives from another system, you often don't know how many elements it has or what they're called. Don't reach for a &lt;code&gt;for&lt;/code&gt; loop by index. Dynamic arrays can be sparse, which means that an element can exist positionally without ever having been assigned, and a &lt;code&gt;for&lt;/code&gt; loop will happily hand you those empty slots. The correct tool is &lt;code&gt;%GetIterator()&lt;/code&gt;, which returns a &lt;code&gt;%Iterator.Object&lt;/code&gt; or &lt;code&gt;%Iterator.Array&lt;/code&gt;, driven by &lt;code&gt;%GetNext()&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set iter = obj.%GetIterator()

While iter.%GetNext(.key, .value, .type) 
{
    Write !, key, " = ", value, " (", type, ")"
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;%GetNext()&lt;/code&gt; skips unassigned elements automatically, which is exactly why it is preferred. For an object, &lt;code&gt;key&lt;/code&gt; is the property name. For an array, &lt;code&gt;key&lt;/code&gt; is the index. To walk a nested structure, recurse whenever &lt;code&gt;$IsObject(value)&lt;/code&gt; is true, as that will return true for both sub-objects and sub-arrays.&lt;/p&gt;
&lt;p&gt;That third argument, &lt;code&gt;.type&lt;/code&gt;, is easy to ignore, but worth understanding. When present it does two useful things. First, it returns the element's original JSON datatype as a string. Second — and this is the practical part — it changes the conversion rules so you avoid &lt;code&gt;&amp;lt;MAXSTRING&amp;gt;&lt;/code&gt; errors: a very long JSON string is handed back as a read-only stream object instead of being force-fit into an ObjectScript string, and JSON numbers are returned in their original textual form rather than being coerced. If you're processing arbitrary external JSON, passing &lt;code&gt;.type&lt;/code&gt; is cheap insurance.&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%2Fiyzrzoat3xl0ld0dcdfn.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%2Fiyzrzoat3xl0ld0dcdfn.png" alt=" " width="799" height="135"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;Type discovery inside a dynamic entity&lt;/h2&gt;
&lt;p&gt;Because dynamic entities are untyped containers, IRIS gives you tools to interrogate them. &lt;code&gt;%GetTypeOf(key)&lt;/code&gt; reports what a value actually is: &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;object&lt;/code&gt;, &lt;code&gt;array&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;oref&lt;/code&gt;, or &lt;code&gt;unassigned&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set a = [1, "test", true, {"v":1}, [1,2,3]]&lt;br&gt;
// the indexes are:&lt;br&gt;
//       0,      1,    2,       3,       4

&lt;/code&gt;&lt;p&gt;&lt;code&gt;Write a.%GetTypeOf(2)   // true: boolean&lt;br&gt;&lt;br&gt;
Write a.%GetTypeOf(3)   // {"v":1}: object&lt;br&gt;&lt;br&gt;
Write a.%GetTypeOf(9)   // unassigned - it finishes at index 4&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;This matters because ObjectScript flattens JSON's richer type system on the way in. JSON &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;, and &lt;code&gt;null&lt;/code&gt; all become ObjectScript-friendly values &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, and &lt;code&gt;""&lt;/code&gt; when you read them with dot syntax or &lt;code&gt;%Get()&lt;/code&gt;. If you need to tell a genuine &lt;code&gt;null&lt;/code&gt; apart from an empty string apart from a key that was never set, &lt;code&gt;%GetTypeOf()&lt;/code&gt; is the reliable discriminator. There is also &lt;code&gt;%IsDefined()&lt;/code&gt;, but it returns false for unassigned members and true for both &lt;code&gt;""&lt;/code&gt; and &amp;nbsp;&lt;code&gt;null&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%2Ft9nxsmdy6ndpph29triz.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%2Ft9nxsmdy6ndpph29triz.png" alt=" " width="463" height="183"&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%2Fp4jhf0ls9hk7gn9uwchx.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%2Fp4jhf0ls9hk7gn9uwchx.png" alt=" " width="387" height="167"&gt;&lt;/a&gt;&lt;br&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%2Fyvqmadnf8atqn423j41k.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%2Fyvqmadnf8atqn423j41k.png" alt=" " width="500" height="377"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;The date gotcha&lt;/h3&gt;
&lt;p&gt;Type flattening is behind one of the community's recurring puzzles. Export a persistent object whose &lt;code&gt;DOB&lt;/code&gt; is &lt;code&gt;1926-12-11&lt;/code&gt; and you may see &lt;code&gt;"DOB":31390&lt;/code&gt; in the result — that &lt;code&gt;31390&lt;/code&gt; is the internal &lt;code&gt;$HOROLOG&lt;/code&gt; day count, not a corrupted value. The same logic bites the other direction in dynamic SQL: if you pass a query a literal like &lt;code&gt;'1926-12-11'&lt;/code&gt; and get zero rows, it's usually because the column expects &lt;code&gt;$HOROLOG&lt;/code&gt; internal format. The fix is to convert on the way in with &lt;code&gt;$ZDATEH("1926-12-11", 3)&lt;/code&gt;. Whenever a date crosses the boundary between JSON, SQL, and stored objects, ask which representation each side expects.&lt;/p&gt;
&lt;h2&gt;Two things both called "array"&lt;/h2&gt;
&lt;p&gt;Here is a distinction that quietly causes bugs. &lt;code&gt;%DynamicArray&lt;/code&gt; is a &lt;em&gt;positional&lt;/em&gt; list, indexed from 0. But ObjectScript also has typed &lt;strong&gt;collection&lt;/strong&gt; properties, and the &lt;code&gt;array of&lt;/code&gt; collection is not a positional array at all — it's a &lt;strong&gt;dictionary&lt;/strong&gt; (a keyed map). Compare:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Property Tags As array of %String;   // a dictionary: key -&amp;gt; value&lt;br&gt;
Property Notes As list of %String;   // an ordered, positional list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You access an &lt;code&gt;array of&lt;/code&gt; collection by key, not by position:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Do obj.Tags.SetAt("high", "priority")

&lt;p&gt;Write obj.Tags.GetAt("priority")     // high&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;Set key = ""&lt;br&gt;&lt;br&gt;
For &lt;br&gt;&lt;br&gt;
{ &lt;br&gt;&lt;br&gt;
    Set value = obj.Tags.GetNext(.key)  Quit:key=""&lt;br&gt;&lt;br&gt;
    Write !, key, ": ", value &lt;br&gt;&lt;br&gt;
}&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;When a class using &lt;code&gt;%JSON.Adapter&lt;/code&gt; serializes an &lt;code&gt;array of&lt;/code&gt; property, it comes out as a &lt;strong&gt;JSON object&lt;/strong&gt;&lt;code&gt;{"priority":"high"}&lt;/code&gt;, whereas a &lt;code&gt;list of&lt;/code&gt; comes out as a JSON array &lt;code&gt;["high"]&lt;/code&gt;. So "typed array" can mean two very different shapes on the wire depending on which collection you chose. If you want positional JSON, use &lt;code&gt;list of&lt;/code&gt; (or a &lt;code&gt;%DynamicArray&lt;/code&gt;); if you genuinely want a keyed lookup, &lt;code&gt;array of&lt;/code&gt; is your dictionary.&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;Bridging persistent objects and dynamic objects&lt;/h2&gt;
&lt;p&gt;A frequent need is converting a stored object into a free-form dynamic one — for instance, to trim fields before returning them from a REST method. If your class extends &lt;code&gt;%JSON.Adapter&lt;/code&gt;, the clean, non-deprecated path is a two-step that fits on one line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set sc = person.%JSONExportToString(.json)&lt;br&gt;&lt;br&gt;
Set dynObj = {}.%FromJSON(json)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(For large objects, swap in &lt;code&gt;%JSONExportToStream()&lt;/code&gt; so you never hit the string length limit.) Two alternatives are worth knowing. Embedded SQL's &lt;code&gt;JSON_OBJECT()&lt;/code&gt; lets you cherry-pick and rename columns when you only want a subset:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;sql(SELECT JSON_OBJECT('name':Name,'dob':DOB) INTO :json WHERE ID = 1)&lt;br&gt;&lt;br&gt;
Set dynObj = {}.%FromJSON(json)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And going the other way, &lt;code&gt;%JSONImport()&lt;/code&gt; populates a persistent object &lt;em&gt;from&lt;/em&gt; a dynamic one.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;Where dynamic freedom ends: validation&lt;/h2&gt;
&lt;p&gt;Finally, the caveat that surprises people building JSON request validators. The &lt;code&gt;Required&lt;/code&gt; property keyword works for literals, collections, streams, and object-valued properties — but it is silently ignored for &lt;code&gt;%DynamicArray&lt;/code&gt; and &lt;code&gt;%DynamicObject&lt;/code&gt; properties. The reason is mechanical: the generated getter defaults these to &lt;code&gt;[]&lt;/code&gt; and &lt;code&gt;{}&lt;/code&gt;, so even assigning &lt;code&gt;""&lt;/code&gt; gets overwritten with a non-empty default, and &lt;code&gt;%ValidateObject()&lt;/code&gt; never sees a missing value. If you need to enforce presence or shape on dynamic properties, don't rely on &lt;code&gt;Required&lt;/code&gt; — implement a &lt;code&gt;%OnValidateObject()&lt;/code&gt; callback and check them yourself:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Method %OnValidateObject() As %Status&lt;br&gt;&lt;br&gt;
{&lt;br&gt;&lt;br&gt;
    If ..fieldOptions.%Size() = 0 &lt;br&gt;&lt;br&gt;
    {&lt;br&gt;&lt;br&gt;
        Return $$ERROR($$GeneralError, "fieldOptions is required")&lt;br&gt;&lt;br&gt;
    }

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Return $$OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;}&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;Choosing well&lt;/h2&gt;
&lt;p&gt;Dynamic entities are the right tool when structure is unknown, external, or genuinely fluid — parsing payloads, assembling responses, staging data. Typed persistent classes remain the right tool when you want the database, indexes, and validation to enforce a contract. Most real systems use both, meeting at the &lt;code&gt;%JSON.Adapter&lt;/code&gt; boundary. Keep three habits and you'll avoid the classic pitfalls: iterate with &lt;code&gt;%GetNext()&lt;/code&gt; rather than by index, reach for &lt;code&gt;%GetTypeOf()&lt;/code&gt; whenever a value's type actually matters, and remember that &lt;code&gt;array of&lt;/code&gt; is a dictionary, not a list.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;em&gt;Disclaimer: this article has been written by human hands, but reviewed with Claude AI for (1) enhancing my English (not my first language) and (2) reminding me of topics I hadn't covered in the initial versions. The search and studies for sources and practice were all made by a human (me :D), and the agent used to review is trained only on my own articles, so that it copies my tone without plagiarism, with respect to my fellow community members. The final revision was done completely without AI.&amp;nbsp;&lt;/em&gt;&lt;/span&gt;&lt;/p&gt;


</description>
      <category>sql</category>
      <category>tooling</category>
      <category>json</category>
      <category>javascript</category>
    </item>
    <item>
      <title>InterSystems for dummies – IRIS Vector Search (Part II)</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 30 Aug 2026 07:30:53 +0000</pubDate>
      <link>https://dev.to/intersystems/intersystems-for-dummies-iris-vector-search-part-ii-3l0o</link>
      <guid>https://dev.to/intersystems/intersystems-for-dummies-iris-vector-search-part-ii-3l0o</guid>
      <description>&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%2Fimukpqyxi2f37vaw87wj.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%2Fimukpqyxi2f37vaw87wj.png" alt=" " width="500" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After an extensive math lesson, we are going to put our new knowledge into practice and create one of the best context-related databases.&lt;/p&gt;

&lt;p&gt;Get your magnifying glasses and hats ready, because Vector Holmes is back.&lt;/p&gt;

&lt;h1&gt;
  
  
  Vector Calculation
&lt;/h1&gt;

&lt;p&gt;To calculate the vector associated with a text, image, or sound, we will use a Python library called sentence-transformers, which allows us to transform content into a vector.&lt;br&gt;
To implement this, we should create the following function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ClassMethod Embedding(Text) [ Language = python ]
{
    from sentence_transformers import SentenceTransformer

    model_name = 'sentence-transformers/all-MiniLM-L6-v2'

    # Cache in global variables of the embedded Python module (persistent per process).
    global _cached_embedding_model
    if '_cached_embedding_model' not in globals():
        _cached_embedding_model = SentenceTransformer(model_name)

    vector = _cached_embedding_model.encode(
        [Text],
        normalize_embeddings=True,
        convert_to_numpy=True,
        show_progress_bar=False
    )

    return str(vector[0].tolist())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method "vectorizes" the content of our text using a pre-trained library called "all-MiniLM-L6-v2". However, if you wish, you can modify it and utilize your own trained library.&lt;/p&gt;

&lt;h1&gt;
  
  
  First Steps
&lt;/h1&gt;




&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; To access the terminal of our Docker instance, use the following command:&lt;br&gt;
&lt;code&gt;Docker-compose exec iris iris session iris&lt;/code&gt;&lt;/p&gt;



&lt;p&gt;Let's use an example of a vectorized search. For this, we will utilize a table called &lt;em&gt;St.vectorsearch.Feeling&lt;/em&gt; that has the following fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;td&gt;%String&lt;/td&gt;
&lt;td&gt;Text about how I feel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value&lt;/td&gt;
&lt;td&gt;%Integer&lt;/td&gt;
&lt;td&gt;Identifier of my feeling (See attached table)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector&lt;/td&gt;
&lt;td&gt;%Vector&lt;/td&gt;
&lt;td&gt;Text vector value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can create the data by running the following command:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Do ##class(St.vectorsearch.Data).Init()&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Next, we are going to load the sentiment data from the &lt;em&gt;/opt/irisbuild/data/training.csv&lt;/em&gt; directory, using the Populate command from the &lt;em&gt;St.vectorsearch.Data&lt;/em&gt; class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USER&amp;gt;do ##class(St.vectorsearch.Data).Populate()
Truncating table St_vectorsearch.Feeling
Preparing to load data from file training.csv
Loading data from file training.csv
Total records loaded: 2000
Calculating vectors for records...
Processing 496 of 2000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be patient, since it will create a vector for each record, and it might take quite a while.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Populate will initialize a data model of 2000 records. There is another file with 5410 records if you want more information in the data model. If you wish to work with that one, use the PopulateFull() command instead.&lt;/p&gt;




&lt;p&gt;The values of feelings are as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feeling&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sadness&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;joy&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;love&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anger&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fear&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“I left with my bouquet of red and yellow tulips under my arm, feeling slightly more optimistic than when I arrived.”&lt;/em&gt; It has a value of 1 (Joy).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“I can’t walk into a shop anywhere where I do not feel comfortable.”&lt;/em&gt; It has a value of 4 (Fear).&lt;/p&gt;

&lt;p&gt;If we use this function to create vectors, the first text we pass will return the next vector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.034666668623685836791, .012147962115705013276, .020678628236055374146, 
.043785430490970611572, .030321707949042320251, -.0081106657162308692932, 
-.028869708999991416931, -.059950094670057296752, .067945346236228942871, 
-.070874534547328948974, -.060159780085086822509, .016239311546087265014, 
-.034665744751691818237, -.011642633005976676941, .080176420509815216064, 
………
.0099751437082886695861, .0098187308758497238159, .00016082286310847848653, .013545278459787368774, -.0049553057178854942321, .054155148565769195556, .025806473568081855773, -.038503900170326232911, .039657127112150192261, 
-.073851920664310455322, -.070615962147712707519, .066068030893802642822, 
-.082378372550010681152, -.043505564332008361816, -.0054294602014124393463
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The vector value has been reduced to keep the text from being too long; the vector actually has 384 values.&lt;/p&gt;




&lt;p&gt;If we look for another cheerful text when creating the vector, it will give us the following value.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Today, I’m very happy.”&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 0.08268272876739502, 0.004975424613803625, -0.062008269131183624, 
……
0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This data does not make sense right now because we are not going to make a value-by-value comparison. That means we are not going to look for a record with exactly the same value as the vector because each vector is different. For that, we will use the formulas explained in the first part.&lt;/p&gt;

&lt;p&gt;We are going to use the following SQL commands to see which vectors are the closest to the vector we have calculated.&lt;/p&gt;

&lt;h1&gt;
  
  
  &amp;nbsp;
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Dot Product Search
&lt;/h1&gt;

&lt;p&gt;As we indicated in the first part, the dot product search tells us how aligned two different vectors are. That means the closer it is to 1, the closer, or rather more similar, two vectors are.&lt;/p&gt;

&lt;p&gt;To perform our search, we will use the condition &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_vectordotproduct" rel="noopener noreferrer"&gt;“VECTOR_DOT_PRODUCT”&lt;/a&gt; that compares two vectors to determine their alignment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="nb"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;
 &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;St_vectorsearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Feeling&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;VECTOR_DOT_PRODUCT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 
......
-0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This search gives us the following results:&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%2F0pb2n3m4axskljucjq55.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%2F0pb2n3m4axskljucjq55.png" alt=" " width="776" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So… &lt;em&gt;“Today, I’m very happy”&lt;/em&gt; returns most results with a value of 1, joy.&lt;/p&gt;

&lt;p&gt;But… what is the percentage of proximity our text has regarding the rest of the retrieved values?&lt;/p&gt;

&lt;p&gt;We can find that out by comparing it with the current vector and displaying the value in a percentage format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="nb"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTOR_DOT_PRODUCT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 
......
 -0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'990.99%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;Percentage&lt;/span&gt;
 &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;St_vectorsearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Feeling&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;VECTOR_DOT_PRODUCT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 
......
-0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This search gives us the following results:&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%2Ff9qatwpd2253e3n0sahj.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%2Ff9qatwpd2253e3n0sahj.png" alt=" " width="800" height="162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The percentage of similarity to the most matching text is 65%. Our mind knows that, among all the texts, the meaning of &lt;em&gt;“Today, I’m very happy”&lt;/em&gt; is a feeling of joy, meaning it should be closer to 100% comparing to the text &lt;em&gt;“I am feeling so happy”&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Cosine Similarity Search
&lt;/h1&gt;

&lt;p&gt;The cosine similarity search is performed using the cosine of the angle between the vectors being compared.&lt;/p&gt;

&lt;p&gt;If we want to perform vector search with cosine similarity, we would have to utilize the condition &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_vectorcosine" rel="noopener noreferrer"&gt;“VECTOR_COSINE”&lt;/a&gt;, just as we have previously done it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="nb"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTOR_COSINE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 
......
 -0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'990.99%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;Percentage&lt;/span&gt;
 &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;St_vectorsearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Feeling&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;VECTOR_COSINE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[-0.007015716750174761, 0.05171322077512741, 0.0045058708637952805, -0.04008815810084343, 0.009171668440103531, -0.04867621883749962, 
......
-0.036987580358982086, 0.027985544875264168, 0.05382953956723213, 0.031146947294473648, -0.011256292462348938, 0.005441852379590273]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oh… surprise!! It gave us back the same results:&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%2Fdutn4cpehtdroscqj7wk.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%2Fdutn4cpehtdroscqj7wk.png" alt=" " width="800" height="162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As mentioned in the first part, this type of search is more advisable for comparing more than one parameter, such as movie genre, lead actor, etc. In other words, this is how the movie recommendation system of Netflix or HBO works.&lt;/p&gt;

&lt;h1&gt;
  
  
  Does it Support Multiple Languages?
&lt;/h1&gt;

&lt;p&gt;What will happen if, instead of using the text &lt;em&gt;“Today, I’m very happy,”&lt;/em&gt; we do it in Spanish?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Hoy, estoy muy feliz.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The created vector (reducing the text for obvious reasons) will resemble 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;[-0.020053215324878693, 0.08766797184944153, 0.04118209332227707, 0.027171766385436058, -0.026769554242491722, -0.045022152364254,
…..
 0.038541924208402634, 0.018977241590619087, 0.03448185324668884, 0.10012426972389221, 0.08426760882139206, -0.09713190793991089]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if we use it to perform the search as we did previously, we will get the result 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%2F8yv2fdkgysnbb8bm2vv8.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%2F8yv2fdkgysnbb8bm2vv8.png" alt=" " width="552" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We got a combination of love (2), fear (4), and joy (1), when the meaning is identical to the English text.&lt;/p&gt;

&lt;p&gt;This rather diverse result appeared because the word &lt;em&gt;"feliz"&lt;/em&gt; is closer to &lt;em&gt;"feel"&lt;/em&gt; than &lt;em&gt;"happy"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;As you have noticed, the creation of vectors is closer to the phonetics of the texts than to their meanings.&lt;/p&gt;

&lt;p&gt;I recommend testing the same text in different languages.&lt;/p&gt;

&lt;p&gt;Here you have the result of searching for it in French &lt;em&gt;“Aujourd'hui, je suis très heureux”&lt;/em&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%2Fz0dvqoph06lum7munjn6.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%2Fz0dvqoph06lum7munjn6.png" alt=" " width="799" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Therefore, if we want to use this system to search for text in a document that we have fully "tokenized", it can be solved in a very simple way.&lt;/p&gt;

&lt;p&gt;In a nutshell, we are going to change the model we used to create the token to the model &lt;em&gt;paraphrase-multilingual-MiniLM-L12-v2&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Therefore, if we modify this line in the Embedding method, we can use multilingual support in data retrieval.&lt;/p&gt;

&lt;p&gt;This is the result of using this model with the search in Spanish:&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%2F766qb9c8imei7uizsjbn.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%2F766qb9c8imei7uizsjbn.png" alt=" " width="375" height="162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, if you create a language-based token, why does it internally "translate" "&lt;em&gt;Feliz&lt;/em&gt;" to "&lt;em&gt;Happy&lt;/em&gt;"? Yes, I have used double quotation marks around "translate" because it is not a context-based translation like in any other online dictionary. It interprets the word by creating a token, which can cause some confusion when similar words are used.&lt;/p&gt;

&lt;p&gt;For example, in Spanish, the word "&lt;em&gt;tiempo&lt;/em&gt;" is the same word for the meaning of the word "&lt;em&gt;time&lt;/em&gt;" or "&lt;em&gt;weather&lt;/em&gt;", so the token for this word could be confusing. Fortunately, the token is not based on a single word, but on a set of words that make up the phrase. It means that the rest of the phrase's context helps us create a token much closer to the final one. Therefore, our vector seems to have translated the text we want to search for.&lt;/p&gt;

&lt;h1&gt;
  
  
  Case Study
&lt;/h1&gt;




&lt;p&gt;&lt;strong&gt;Very important note:&lt;/strong&gt; I encountered some difficulties developing these practical examples. When trying to create a vector from an IRIS production environment, the process would completely freeze.&lt;/p&gt;

&lt;p&gt;The solution to using the &lt;em&gt;St.Vectorsearch.Vector.Embedding&lt;/em&gt; method without any issues was to download the model locally instead of querying the online model for each request. When using the model locally, invoking this class from production did not require an Internet connection to download the model, making the vector creation process much faster.&lt;/p&gt;




&lt;p&gt;I have created a small demo to show how a text, entered from a form, is "tokenized" and searched in the database to find the five closest results.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://localhost:52773/csp/user/feeling.html" rel="noopener noreferrer"&gt;http://localhost:52773/csp/user/feeling.html&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Make sure production is running. To do this, log in to the Management Portal and start production if it was stopped.&lt;/p&gt;




&lt;p&gt;The first time you ask about a feeling, it may take a little while, because it caches the model for future queries.&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%2F3wxh2wb1e2vq683071j5.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%2F3wxh2wb1e2vq683071j5.png" alt=" " width="800" height="789"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;This website invokes an API running on IRIS that converts the text into a vector. Then it invokes the SQL search as I mentioned previously.&lt;/p&gt;

&lt;p&gt;It performs a search for the 5 closest phrases to the calculated vector. Then the website displays those results and indicates which sentiment is repeated more often, showing the arithmetic mean of the most repeated results.&lt;/p&gt;

&lt;h1&gt;
  
  
  Can We Have More Than One Element to Create a Vector?
&lt;/h1&gt;

&lt;p&gt;In the sentiment example, the phrase included information we used to create a search vector. However, what do we need if we have more than one column to perform the search?&lt;/p&gt;

&lt;p&gt;If we want someone to recommend a similar movie to the one we have already seen, the result will depend on many factors. It could be related to the movie release year (because you like older films) or the genre (comedy, drama, science fiction), etc.&lt;/p&gt;

&lt;p&gt;In this case, we will create a vector based on the text that includes the fields we want to index. This way, we will have all the fields we wish to use for our query:&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The order of the data is important because if we are interested in films from the same year and genre, those columns will have the most weight in the index.&lt;/p&gt;




&lt;p&gt;If we are looking for recommendations based on the plot, genre, director, and actors, that would be the order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;“Overview: [Overview]. Genre: [Genre]. Directed by [Director] and starring by [Star1], [Star2], [Star3] and [Star4]. Movie year [Year]. Ranking: [Rating]."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We created the St.Vectorsearch.Movie table with the following fields:&lt;br&gt;
|Field|Type|Description|&lt;br&gt;
|-|-|&lt;br&gt;
|Link|%String|Link to the movie poster|&lt;br&gt;
|Title|%String|Movie title|&lt;br&gt;
|Year|%Integer|Year of the film release|&lt;br&gt;
|Certificate|%String|Age classification (see attached table)|&lt;br&gt;
|Runtime|%Integer|Film duration in minutes|&lt;br&gt;
|Genre|%String|Film genre|&lt;br&gt;
|Rating|%Decimal(3,1)|IMDb rating|&lt;br&gt;
|Overview|%String|Movie description|&lt;br&gt;
|Director|%String|Name of director|&lt;br&gt;
|Star1|%String|Names of actors/actresses|&lt;br&gt;
|Star2|%String|Names of actors/actresses|&lt;br&gt;
|Star3|%String|Names of actors/actresses|&lt;br&gt;
|Star4|%String|Names of actors/actresses|&lt;br&gt;
|Vector|%Vector|Vector value of the film card|&lt;/p&gt;

&lt;p&gt;To understand the age classifications better, I will show you the meaning of those values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Certificate&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Comment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;A is Adults (equivalent to the USA R).&lt;/td&gt;
&lt;td&gt;Rating in the UK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UA&lt;/td&gt;
&lt;td&gt;UA is for ages 12 and up with parental supervision.&lt;/td&gt;
&lt;td&gt;Rating in the UK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U&lt;/td&gt;
&lt;td&gt;U is Universal (for everyone).&lt;/td&gt;
&lt;td&gt;Rating in the UK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PG-13&lt;/td&gt;
&lt;td&gt;PG-13 (Parents Strongly Cautioned): Strong warning for parents. Some materials may be inappropriate for children under the age of 13.&lt;/td&gt;
&lt;td&gt;Current (MPA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;R (Restricted): Those under 17 years of age must be accompanied by a parent or adult guardian because it contains adult material.&lt;/td&gt;
&lt;td&gt;Current (MPA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PG&lt;/td&gt;
&lt;td&gt;PG (Parental Guidance Suggested): Parental guidance is suggested. Some content may not be suitable for young children.&lt;/td&gt;
&lt;td&gt;Current (MPA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;G (General Audiences): For all audiences.&lt;/td&gt;
&lt;td&gt;Current (MPA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PASSED&lt;/td&gt;
&lt;td&gt;The film complies with the strict moral standards of the time to be shown in cinemas.&lt;/td&gt;
&lt;td&gt;They were used between the 1930s and 1960s under the famous Hays Code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TV-14&lt;/td&gt;
&lt;td&gt;TV-14: Parents strongly advised. Contains material that many parents would consider inappropriate for children under the age of 14.&lt;/td&gt;
&lt;td&gt;TV Parental Guidelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Not recommended for children under 16 years old.&lt;/td&gt;
&lt;td&gt;Typical numerical age classification of European or Latin American systems.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TV-MA&lt;/td&gt;
&lt;td&gt;It may be inappropriate for viewers under the age of 17&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;due to graphic violence, explicit sexual activity, or crude language.&lt;/td&gt;
&lt;td&gt;TV Parental Guidelines&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UNRATED&lt;/td&gt;
&lt;td&gt;Not classified.&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GP&lt;/td&gt;
&lt;td&gt;GP: It was a temporary code used in the early 1970s. It was equivalent to what we know today as PG (Parental Guidance Suggestion).&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APPROVED&lt;/td&gt;
&lt;td&gt;The film complies with the strict moral standards of the time in order to be shown in cinemas.&lt;/td&gt;
&lt;td&gt;They were used between the 1930s and 1960s under the famous Hays Code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TV-PG&lt;/td&gt;
&lt;td&gt;Recommended parental guidance.&lt;/td&gt;
&lt;td&gt;TV Parental Guidelines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U/A&lt;/td&gt;
&lt;td&gt;U is Universal (for everyone).&lt;/td&gt;
&lt;td&gt;Rating in the UK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the same way that we initialized the table and loaded the data from the Feeling table, we employ the following commands:&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; To access the terminal of our Docker instance, use the following command:&lt;br&gt;
&lt;code&gt;Docker-compose exec iris iris session iris&lt;/code&gt;&lt;/p&gt;




&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do ##class(St.vectorsearch.Data).InitMovie()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Next, we will load the movie data from the &lt;em&gt;/opt/irisbuild/data/imdb_top_1000.csv&lt;/em&gt; directory, using the PopulateMovie command from the &lt;em&gt;St.vectorsearch.Data&lt;/em&gt; class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USER&amp;gt;do ##class(St.Vectorsearch.Data).PopulateMovie()
Truncating table St_Vectorsearch.Movie
Preparing to load data from file imdb_top_1000.csv
Loading data from file imdb_top_1000.csv
Total records loaded: 999
Calculating vectors for records...
Processing 116 of 999
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; During the demo initialization process in Docker, the test data loading operations are already performed.&lt;/p&gt;




&lt;p&gt;In each movie, we have initialized the vector value using the phrase we have created with the different fields. I encourage everyone to make appropriate changes to the St.Vectorsearch.Data.PopulateMovie class to align with your priorities better.&lt;/p&gt;

&lt;p&gt;As I mentioned in the first part, there is another vector approximation method (by cosine similarity), which allows us to search vectors that form the closest angle (those that are closest in the same direction).&lt;/p&gt;

&lt;p&gt;That is why it is the best system to use if you wish to find data recommendations.&lt;/p&gt;

&lt;p&gt;To use this, we will need the condition &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_vectorcosine" rel="noopener noreferrer"&gt;“VECTOR_COSINE”&lt;/a&gt;, which can tell us the closest angles to the indicated vector.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Year&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;St_Vectorsearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Movie&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; 
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;VECTOR_COSINE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, we are going to look for movies that approximate the angle of the vector associated with the film we have, without, of course, having it recommend itself.&lt;/p&gt;

&lt;p&gt;You can access the demo via the link below:&lt;/p&gt;

&lt;p&gt;&lt;a href="http://localhost:52773/csp/user/movies.html" rel="noopener noreferrer"&gt;http://localhost:52773/csp/user/movies.html&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%2F1o5nj7xujqfigz3hdkql.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%2F1o5nj7xujqfigz3hdkql.png" alt=" " width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;In this case, the recommended films are similar to 1988's "Akira," although I would personally put "Kôkaku Kidôtai," also known as "Ghost in the Shell," first. They are similar because they are both animated films. Mental note: I still need to watch Papurika...&lt;/p&gt;

&lt;p&gt;So, that is all for now. You have everything you need to work with vector indices, including practical examples and an application you can use for your experiments.&lt;/p&gt;

&lt;p&gt;Please leave any suggestions or ideas you have in the comments, including practical ways to implement this knowledge, etc...&lt;/p&gt;

&lt;p&gt;See you at the next “InterSystems for Dummies”!&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>python</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>InterSystems for dummies – IRIS Vector Search (Part I)</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 23 Aug 2026 15:35:13 +0000</pubDate>
      <link>https://dev.to/intersystems/intersystems-for-dummies-iris-vector-search-part-i-2995</link>
      <guid>https://dev.to/intersystems/intersystems-for-dummies-iris-vector-search-part-i-2995</guid>
      <description>&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%2Fnwkbmacfzn2gnwsau9wj.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%2Fnwkbmacfzn2gnwsau9wj.png" alt=" " width="500" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;You may have heard the term "Vector Search" before. Do not worry, though; it is not "Vector Holmes" investigating a crime from 221B Baker Street.&lt;/p&gt;

&lt;p&gt;So, let’s explain, step by step, how to tackle vector data searches.&lt;/p&gt;

&lt;p&gt;:::pagebreak:::&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Vectors?
&lt;/h2&gt;

&lt;p&gt;In mathematics, a vector is an arrow that indicates a quantity with three elements: its value (magnitude), its direction, and its course. It is used to represent things that cannot be described with a single number, such as velocity, force, or displacement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Magnitude&lt;/strong&gt;: It is the length of the arrow, which indicates the numerical value of the magnitude.&lt;br&gt;
&lt;strong&gt;Direction:&lt;/strong&gt; It is the straight line on which the vector acts (it can be horizontal, vertical, or diagonal).&lt;br&gt;
&lt;strong&gt;Course:&lt;/strong&gt; Indicates where the arrow points (it is the endpoint of the direction).&lt;br&gt;
&lt;strong&gt;Point of application:&lt;/strong&gt; It is the starting point of the vector.&lt;/p&gt;

&lt;p&gt;Two vectors are equal when they have the same magnitude (modulus), direction, and course. It means that regardless of their position in space, if their components are identical, they are the same vector.&lt;/p&gt;

&lt;p&gt;Two vectors are opposite when they have the same magnitude and the same direction, but opposite course, meaning they are facing away from each other.&lt;/p&gt;

&lt;p&gt;In a two-dimensional system, a vector is represented by its start (origin) and end (destination) coordinates on the X and Y axes.&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%2Fnjg8da6ev4yzwjagumul.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%2Fnjg8da6ev4yzwjagumul.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;br&gt;
These two vectors are represented as follows:&lt;/p&gt;

&lt;p&gt;{(1,1),(5,3)}&lt;/p&gt;

&lt;p&gt;{(2,4),(7,5)}&lt;/p&gt;

&lt;p&gt;In a three-dimensional space, coordinates include the X, Y, and Z axes.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Do Mathematical Vectors Have to Do with Vector Search?
&lt;/h2&gt;

&lt;p&gt;Vector search is a method of searching known in numerical representations as vectors. It is similar to the vectors we have seen previously, but with the origin point being point 0 in Euclidean space.&lt;/p&gt;

&lt;p&gt;Instead of looking for precise keyword matches, this technique examines similarities between vectors, as previously explained, enabling more accurate, semantically and contextually meaningful results, even if the words in the query are not exactly the same.&lt;/p&gt;

&lt;p&gt;For example, "Iron Maiden," "Van Halen," and "Scorpions" are all the names of heavy metal bands. If we convert these names into tokens to create vectors, their coordinates will be similar but not identical. There will be slight differences in magnitude and direction. Therefore, they will be “orbiting” in the same “band” zone.&lt;/p&gt;

&lt;p&gt;Conversely, "Bob Dylan" or "Genesis" will point in the opposite direction. They are still musical groups, but their styles do not have "the same direction".[&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%2Fayam4fl9u3w9otlvb5x7.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%2Fayam4fl9u3w9otlvb5x7.png" alt=" " width="800" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we perform a traditional search, we must use the exact same word, which is"Music group". It will encompass all groups. However, if we want to look for a specific genre, for instance, "thrash rock", we will not know how to differentiate Metallica from Dire Straits.&lt;/p&gt;

&lt;p&gt;In contrast, if we perform a vector search, it would locate these groups based on their proximity in both genre and musical style, showing that Metallica and Slayer are similar.&lt;/p&gt;
&lt;h2&gt;
  
  
  Are Vectors Really “Arrows”?
&lt;/h2&gt;

&lt;p&gt;In computer science, a vector is a data structure that includes an array of numbers. In our case, these vectors store a digital summary of the dataset to which they have been applied. Consider it a summary or a digital fingerprint.&lt;/p&gt;

&lt;p&gt;Images can also be analyzed for similarities. If you had to develop an application to compare two images, how would you do it? If you simply compared every pixel of one image with every pixel of the other, you would only find images that are identical in resolution, color, encoding, and other aspects.&lt;br&gt;
However, if you could examine the images and generate vector embeddings of the content, you would be able to compare them and identify similarities. In the case of images, a vector embedding explains the content of each image and then allows for comparison.&lt;br&gt;
This constitutes a much more robust method for discovering similarities between images.&lt;/p&gt;

&lt;p&gt;A vector, speaking in computer science terms, would have a representation similar to the one below&lt;/p&gt;

&lt;p&gt;{10,-4,6,34,0,-35,67,203,466,4,356,3,-53,-3,0}&lt;/p&gt;

&lt;p&gt;It will be like indicating the vector's final position only, since the vector's origin is point 0.&lt;/p&gt;
&lt;h2&gt;
  
  
  Vector Search Algorithms
&lt;/h2&gt;

&lt;p&gt;The data, which includes images, texts, audio, etc., is encoded in numerical vectors called "Embeddings". These values have varying dimensions depending on how they are encoded. The more dimensions our vector has, the more specific its definition will be, and the better it can be grouped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vector Similarity Searches:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can use different types of metrics in vector similarity searches, each with its own advantages and disadvantages. The appropriate metric will depend on the data type and the application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Euclidean Distance:&lt;/strong&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;To determine if two vectors are close, we should measure the “Euclidean distance”, which is the straight-line length between two points in Euclidean space. It equals the square root of the sum of the squared differences between the point coordinates:&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%2Fabq94si34v4ezx8k8mwy.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%2Fabq94si34v4ezx8k8mwy.png" alt=" " width="392" height="69"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In an n-dimensional space, it is measured as the difference between each of the dimensions squared.&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%2Fw18j3ykkpekhj3im7xxe.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%2Fw18j3ykkpekhj3im7xxe.png" alt=" " width="718" height="63"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since the value is smaller, we can deduce that the final vector points are closer to each other than the initial ones.&lt;/p&gt;

&lt;p&gt;If we compare it with other vectors in the same space, those with the smallest values will be approximate vectors.&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%2F6ybe3grtc5kntyap56xa.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%2F6ybe3grtc5kntyap56xa.png" alt=" " width="800" height="580"&gt;&lt;/a&gt;&lt;br&gt;
The distance between v1 and v3 is less than the distance between v1 and v2.&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%2F8z7amyyhjnmkyfaiohbd.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%2F8z7amyyhjnmkyfaiohbd.png" alt=" " width="800" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is important to know how to calculate the distance between two points because it will be used later for measuring the magnitude of a vector.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Magnitude of a Vector&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is the distance from the vector base to its endpoint. In our case, all vectors originate at point 0, so the magnitude of a vector would be calculated using the following formula:&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%2F4yxwqr2jnlntwp38ccfy.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%2F4yxwqr2jnlntwp38ccfy.png" alt=" " width="195" height="59"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The data above gives us the following magnitudes of the vectors:&lt;/p&gt;

&lt;p&gt;V1 = 5,83095189&lt;/p&gt;

&lt;p&gt;V2 = 10&lt;/p&gt;

&lt;p&gt;V3 = 5,6568542&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dot Product&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The dot product (also called the scalar product) is an operation between two vectors that results in a real number (a scalar). It is one of the most important tools in mathematics and physics because it tells us how "aligned" two vectors are.&lt;/p&gt;

&lt;p&gt;It is calculated by multiplying the sum of the coordinates in each dimension of the vectors to be compared.&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%2F5c6yjsf6fnmql20tx769.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%2F5c6yjsf6fnmql20tx769.png" alt=" " width="306" height="131"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The information from our example gives us the dot product values as follows:&lt;/p&gt;

&lt;p&gt;Dot &amp;nbsp;product v1v2 = (3 * 6) + (5 * 8) = 58&lt;/p&gt;

&lt;p&gt;Dot product v2v3 = (6 * 4) + (8 * 4) = 56&lt;/p&gt;

&lt;p&gt;Dot product v1v3 = (3 * 4) + (5 * 4) = 32&lt;/p&gt;

&lt;p&gt;The larger the dot product, the greater the similarity between these two vectors.&lt;/p&gt;

&lt;p&gt;We can say that v1 and v2 are very similar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cosine Similarity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This search method allows us to look for vectors not by the distance between their points, but by the angle they form.&lt;/p&gt;

&lt;p&gt;The cosine similarity measures the orientation between two vectors in a multidimensional space, ignoring their magnitudes. It is calculated by dividing the dot product of two vectors (A) and (B) by the product of their magnitudes, resulting in a value between -1 and 1.&lt;/p&gt;

&lt;p&gt;It is calculated using the following formula:&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%2F2twlaw6fsv117o4yliaa.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%2F2twlaw6fsv117o4yliaa.png" alt=" " width="348" height="99"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Following the previous example,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Similarity of the cosine of v1 and v2:
Point product v1 y v2 = (3 * 6) + (5 *8) = 18 + 40 = 58
Magnitude v1 = 5,83095189
Magnitude v2 = 10
Cosine(v1, v2) = 58 / 58,309 = 0,9947

Similarity of the cosine of v2 and v3
Point product v2 y v3 = (6 * 4) + (8 * 4) = 56
Magnitude v2 = 10
Magnitude v3 = 5,6568542
Cosine(v2, v3) = 58 / 58,309 = 0,9899

Similarity of the cosine of v1 y v3
Point product v1 y v3 = (3 * 4) + (5 * 4) = 32
Magnitude v1 = 5,83095189
Magnitude v3 = 5,6568542
Cosine(v1, v3) = 32 / 32,984 = 0,9701
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can confirm that v1 and v2 are more similar than v2 and v3, and v1 and v3.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In the case of multidimensional vectors, we should perform the sum of each of the dimensions.&lt;/p&gt;




&lt;p&gt;This approach returns vectors with similar directions, so it can help us find resemblances to make recommendations (this is how Netflix or HBO works).&lt;/p&gt;

&lt;p&gt;In the example of music groups, we can start by searching for "thrash rock" bands. Then we can suggest other groups not categorized as thrash rock but within the heavy metal genre. That is, we start by listening to Metallica and, through suggestions, move on to Van Halen. Now you understand why Netflix might suggest watching "The Nightmare Before Christmas" after seeing "Hotel Transylvania”.&lt;/p&gt;

&lt;p&gt;As Foghorn Leghorn once said, “It’s math, son!! The numbers don’t lie.”&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%2F35vpwodjh27yhfx6m9g6.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%2F35vpwodjh27yhfx6m9g6.png" alt=" " width="662" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do We Use This Mathematical Knowledge In Vector Search?
&lt;/h2&gt;

&lt;p&gt;First, we need to convert our data (text, images, etc.) into a vector, as we have indicated before.&lt;/p&gt;

&lt;p&gt;However, vectors do not create themselves. That means it is not enough just to give them a random value defined by us.&lt;/p&gt;

&lt;p&gt;LLM models allow us to create the dot product and use cosine similarity to perform searches within &amp;nbsp;InterSystems IRIS. Euclidean search is not implemented, but could be applied with Python libraries.&lt;/p&gt;

&lt;p&gt;Some people have already thought about this because it will allow us to associate correct vectors based on the text context… Yet, I will explain this in the next article.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I am sorry for the math lesson. Yet, I had to explain how it works first to help you better understand the "search magic".&lt;/p&gt;




</description>
      <category>beginners</category>
      <category>python</category>
      <category>tooling</category>
      <category>vectordatabase</category>
    </item>
    <item>
      <title>The Gaia Planetarium: A Full-Stack Embedded Python Project</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 23 Aug 2026 15:26:23 +0000</pubDate>
      <link>https://dev.to/intersystems/the-gaia-planetarium-a-full-stack-embedded-python-project-5708</link>
      <guid>https://dev.to/intersystems/the-gaia-planetarium-a-full-stack-embedded-python-project-5708</guid>
      <description>&lt;p&gt;If you are a regular on the developer community, you may have seen some recent posts about the InterSystems First Employee Programming competition. The challenge is simple — transform some Gaia Epoch Photometry data, calculate the percentage change of flux (between the maximum and minimum values), and output a list of the stars with a percentage change of greater than 100.&lt;/p&gt;
&lt;p&gt;Now while this was a fun challenge, but I felt there was something missing: &lt;em&gt;a lack of scope for creativity&lt;/em&gt;. After all, we are looking at stars! How can something as magical as the night sky be distilled down to a CSV files of IDs and flux values?&lt;/p&gt;
&lt;p&gt;So I decided to do something different with my main entry, and visualise stars the way they should be viewed — in the night sky above us. Let's take a look at the Gaia planetarium:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/8ROMg2bN3Vo"&gt;
  &lt;/iframe&gt;
&lt;br&gt;
&lt;/p&gt;
&lt;h1&gt;Project Design&lt;/h1&gt;
&lt;p&gt;The first question was what information is needed to plot the stars in the nights sky? Now its easy to plot a point on earth - you just need a longitude and latitude. For stars, you also need two locating values, Right Ascension (RA) and Declination (Dec). Of course, it gets more complex if you want to know where they are in location to the earth, but that was solved later.&lt;/p&gt;
&lt;p&gt;These values aren't in the Photometry datasets, instead being found in the Gaia source table, which can helpfully be queried using a Python library called &lt;code&gt;astroquery&lt;/code&gt;. This made the decision to use Python a no-brainer.&lt;/p&gt;
&lt;p&gt;So I decided upon the following stack:&lt;/p&gt;
&lt;p&gt;&lt;br&gt;- &lt;strong&gt;PyProd production&lt;/strong&gt;&lt;br&gt;- Ingests Photometry data files&lt;br&gt;- Calculates output (and adds to csv file)&lt;br&gt;- Queries the Gaia Source table to get the location for each source ID.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;- &lt;strong&gt;Flask Web Application&lt;/strong&gt; (hosted using IRIS WSGI hosting):&lt;br&gt;- Query data from IRIS tables using Embedded Python&lt;br&gt;- Create REST Service to send data to the front-end&lt;/p&gt;
&lt;p&gt;&lt;br&gt;- &lt;strong&gt;Front-end UI with HTML/CSS/JS&lt;/strong&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;br&gt;Now, before going through the implementation in a bit more detail, I will detail one issue I found. The Gaia photometry data is &lt;em&gt;heavy&lt;/em&gt;. Each zipped file is around 15.5MB, which doesn't sound like that much, until you realise there is &amp;gt;3000 of them. What's more, each of these files maps a small portion of the sky in immense detail, whereas I am much more interested in covering the sky with stars that could be visible to the naked-eye.&lt;/p&gt;
&lt;p&gt;I therefore decided to add separate production components which can just query the Gaia source data, ordered by brightness to add the most visible stars to my planetarium. I've still included the photometry dataset and, because it is a production, its easy to throw more files into the watched directory to add them into map. The challenge results are available as an overlay, and may not be visible depending on whether the small portion of the sky that the photometry data maps is overhead.&lt;/p&gt;
&lt;p&gt;I also added star data from a different dataset — Hipparcos, because many of the most visible stars overload the Gaia sensor, so are not available in the Gaia dataset. Including Hipparcos stars was important for viewing constellations to my planetarium. I also added constellation data from Stellarium to visualise the constellations in the sky.&lt;/p&gt;
&lt;h1&gt;IRIS Implementation&lt;/h1&gt;
&lt;h2&gt;PyProd&lt;/h2&gt;
&lt;p&gt;I've &lt;a href="https://community.intersystems.com/post/csvgen-pyprod" rel="noopener nofollow noreferrer"&gt;recently written&lt;/a&gt; about PyProd for another example project, so I am going to skip the technical details in this case. One comment I will make though — it was really nice to be able to build a production in Python because I required a few steps which are super easy to do in Python, and not so easy in ObjectScript. The key example of this was querying the Gaia dataset directly with the &lt;code&gt;astroquery.gaia&lt;/code&gt; library.&lt;/p&gt;
&lt;p&gt;I also used this project as a moment to road test a new PyProd agent skill. This way, as soon as I start using PyProd, my agent can read the skill and see exact patterns of how it should be working with PyProd. A version of this skill is now available in the &lt;a href="https://github.com/intersystems-community/iris-agentic-dev/blob/master/skills/pyprod/SKILL.md" rel="noopener nofollow noreferrer"&gt;iris-agentic-dev skill library&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The Web App: Flask&lt;/h2&gt;
&lt;p&gt;Its not super well known that IRIS can host WSGI applications, lets face it, I didn't even know what a WSGI application was when I first heard this! Web-Server Gateway Interface is a Python standard for running web applications. Some of the most popular Python web application frameworks, including Flask and Django, run on WSGI.&lt;/p&gt;
&lt;p&gt;Flask is a lightweight framework for developing REST APIs. It has pretty simple syntax, where you define a function with a decorator to make it an REST endpoint. Combining this with Embedded Python makes it easy to get data from IRIS. For example, a simplified version of the endpoint which collects stars from the IRIS table is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from flask import Flask, request&lt;br&gt;
import iris # Embedded python IRIS import 

&lt;p&gt;app = Flask(&lt;strong&gt;name&lt;/strong&gt;, static_folder="/home/irisowner/dev/src/skymap/static)&lt;/p&gt;

&lt;h1&gt;
  
  
  Endpoint to access stars from the Database
&lt;/h1&gt;

&lt;p&gt;@app.route("/api/stars", methods=["GET"])&lt;br&gt;
def get_stars() &lt;br&gt;
    # SQL Query &lt;br&gt;
    query = """&lt;br&gt;
        SELECT TOP 1000 SourceId, Ra, DecDeg, PhotGMeanMag &lt;br&gt;
        FROM "Gaia.SourceLocation"&lt;br&gt;
        """&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Execute query 
rows = iris.sql.exec(query)

# Collect results
output = [] 
for row in rows: 
    output.append({
      "source_id": row[0],
        "ra"     : row[1],
        "dec"    : row[2],
        "mag"    : row[3]
    })

# Return results
return output
&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;And we can also use this to activate a PyProd adapterless Business Service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from intersystems_pyprod import director 

&lt;h1&gt;
  
  
  POST endpoint
&lt;/h1&gt;

&lt;p&gt;@app.route("/api/more-stars", method=["POST"])&lt;br&gt;
def add_stars(): &lt;br&gt;
    # Get the info from the post request&lt;br&gt;
    n_stars = int(request.args.get("n_stars"))&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Create Business Service
status, service = director.create_business_service("Gaia.StarCatalogService")

# Activate Business Service
service.process_input(n_stars)
&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;h3&gt;Flask hosting&lt;/h3&gt;
&lt;p&gt;WSGI applications can be hosted through the Management Portal at System -&amp;gt; Security -&amp;gt; Applications:&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%2Fhuf1td6l5m2gwxsnmpmm.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%2Fhuf1td6l5m2gwxsnmpmm.png" alt=" " width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But I wanted to do this programatically through Embedded Python. Using the Security.Applications.Create() function required me passing an IRIS array by reference into the function, which can be achieved using an &lt;code&gt;iris.arrayref(&amp;lt;python dict&amp;gt;)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import iris
# Define settings 
props = iris.arrayref({
            'Type': 2,
            'NameSpace': 'USER',
            'WSGIAppLocation': '/home/irisowner/dev/src', # Path to flask project
            'WSGIAppName': 'skymap.server', # FolderName.FileName (without .py)
            'WSGICallable': 'app',
            'WSGIDebug': 0,
            'WSGIType': 1,
            'AutheEnabled': 64,
            'Enabled': 1,
            'Description': 'Gaia sky map Flask/WSGI application',
            "DispatchClass":"%SYS.Python.WSGI", # Needed for WSGI hosting
            "Path":"/home/irisowner/dev/src", # Path to flask project
            "WSGIDebug": 1, # Refresh when the code changes
            "MatchRoles": ":%All" # Security roles
        })

# Create Web App
sc = iris.Security.Applications.Create('/skymap', props)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An easy point to miss here if you normally create WSGI applications through the management portal: you need to set the DispatchClass to &lt;code&gt;"%SYS.Python.WSGI"&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Front-end&lt;/h2&gt;
&lt;p&gt;Unlike a pure Python framework like Streamlit, Flask include a Python front-end framework, instead relying on building a front-end with HTML/CSS/JavaScript (or some front-end framework).&lt;/p&gt;
&lt;p&gt;I'm going to skip most of the details on this because it was largely a question of me describing clearly what I wanted, and getting some AI generated code as a result.&lt;/p&gt;
&lt;p&gt;It is pure HTML/JS/CSS, which I personally am a big fan of. I know it has limits, but sometimes I think starting with a front-end framework overcomplicates things.&lt;/p&gt;
&lt;p&gt;The Front end is hosted in the Flask app through a static route:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from flask import Flask, jsonify, request, send_from_directory

STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
app = Flask(__name__, static_folder=STATIC_DIR)

@app.get("/")
def index():
        return send_from_directory(STATIC_DIR, "index.html")

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only other thing to mention is that Cross Origin Resource Sharing is also handled from Flask&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@app.after_request
def add_cors(response):
        response.headers["Access-Control-Allow-Origin"] = "*"
        return response
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As I mentioned at the start of this article, this project started out as a competition entry for the Employee Programming Competition. However, it quickly spiralled into something else, once I realised the limitations of only using the Gaia Photometry Data. It has become a Python Full Stack Application, which integrates multiple data sources (Gaia, Hipparcus and files of constellations and star names).&lt;/p&gt;
&lt;p&gt;To be clear though, &lt;strong&gt;it still does what was asked by the competition&lt;/strong&gt;, and it even plots it as a bright yellow/orange/red patch (coloured by magnitude of flux change) of sky which is covered by the Photometry data. So if you've made it this far, please consider voting for my planetarium in the community vote!&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%2Fu3b49tct6b6mt8hp63jj.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%2Fu3b49tct6b6mt8hp63jj.png" alt=" " width="793" height="751"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I hope you've enjoyed reading about this Embedded Python project as much as I've enjoyed making it!&lt;/p&gt;

</description>
      <category>python</category>
      <category>resources</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
