<?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.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>New SMART on FHIR v2 Scopes</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 24 May 2026 15:13:59 +0000</pubDate>
      <link>https://dev.to/intersystems/new-smart-on-fhir-v2-scopes-e3j</link>
      <guid>https://dev.to/intersystems/new-smart-on-fhir-v2-scopes-e3j</guid>
      <description>&lt;p&gt;In v2026.1 we introduced support for a more robust and real-life secure authorization for your FHIR endpoints.&lt;/p&gt;
&lt;p&gt;This is achieved by using &lt;a href="https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=HXFHIRADM_server_auth#HXFHIRADM_server_auth_oauth_scopes" rel="noopener noreferrer"&gt;SMART on FHIR v2 fine-grained scopes&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.amazonaws.com%2Fuploads%2Farticles%2Fjt7urawsgh29q7so1ca0.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.amazonaws.com%2Fuploads%2Farticles%2Fjt7urawsgh29q7so1ca0.png" alt=" " width="800" height="435"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h2&gt;Focus - Not SMART in general, rather, the fine-grained scopes; Hands-on easy sample&lt;/h2&gt;
&lt;span&gt; &lt;/span&gt;&lt;p&gt;I have dived into the topic of SMART on FHIR in the past, for example see &lt;a href="https://community.intersystems.com/post/smart-fhir-app-sample-hands-exerciseworkshop-instructions" rel="noopener noreferrer"&gt;this article&lt;/a&gt; I wrote (with an accompanying &lt;a href="https://openexchange.intersystems.com/package/smart-day-hands-on" rel="noopener noreferrer"&gt;Open Exchange app&lt;/a&gt;, and &lt;a href="https://www.youtube.com/watch?v=OHaZ5qiyQ1c" rel="noopener noreferrer"&gt;related video series&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Also others have discussed this topic, for example &lt;span&gt;&lt;span&gt;@LuisAngel.PérezRamos&lt;/span&gt;&lt;/span&gt; in his &lt;a href="https://community.intersystems.com/post/developing-smart-fhir-applications-auth0-and-intersystems-iris-fhir-server-introduction" rel="noopener noreferrer"&gt;Developing SMART On FHIR Applications with Auth0 and InterSystems IRIS FHIR Server&lt;/a&gt; article series, &lt;a class="mentioned-user" href="https://dev.to/nicole"&gt;@nicole&lt;/a&gt;.Sun&lt;span&gt;&lt;span&gt; in her &lt;/span&gt;&lt;/span&gt;&lt;a href="https://community.intersystems.com/post/smart-fhir-ehr-launch-iris-health" rel="noopener noreferrer"&gt;SMART on FHIR EHR Launch with IRIS for Health&lt;/a&gt; article, and &lt;a class="mentioned-user" href="https://dev.to/kate"&gt;@kate&lt;/a&gt;.Lau&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;in her two-part &lt;a href="https://community.intersystems.com/post/using-postman-testing-oauth20-intersystems-fhir-repository-part1" rel="noopener noreferrer"&gt;Using Postman for testing the OAuth2.0 of the InterSystems FHIR repository&lt;/a&gt;.&lt;/p&gt;In addition this Learning Services video - &lt;a href="https://www.youtube.com/watch?v=wAB-msyXq_8" id="OWAb43c6521-1114-4055-9f43-89a408a783e1" rel="noopener noreferrer"&gt;Configuring OAuth for InterSystems FHIR Server&lt;/a&gt; - explains this nicely, and even demonstrates part of the latest SMART scope-based result filtering that we'll discuss here.But in the above mentioned articles and samples, we either used InterSystems IRIS itself as the OAuth Server, or a 3rd party cloud OAuth Server (like auth0 by Okta), but in this article and sample I want to do a few things differently -&lt;p&gt;1. I want to use a 3rd party OAuth Server, but not one you will need to register (and perhaps pay) for. This will be &lt;a href="https://www.keycloak.org/" rel="noopener noreferrer"&gt;&lt;strong&gt;Keycloak&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;2. I want to take care of all of the setup for you in a &lt;strong&gt;Dockerized sample&lt;/strong&gt; - &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;span&gt;&lt;em&gt;InterSystems IRIS for Health&lt;/em&gt;&lt;/span&gt; up an running with a FHIR Endpoint defined, including an OAuth client defined, and some Resources in the Repository.&lt;/li&gt;
&lt;li&gt;
&lt;span&gt;&lt;em&gt;Keycloak &lt;/em&gt;&lt;/span&gt;up and running with a client corresponding to the IRIS OAuth client.&lt;/li&gt;
&lt;li&gt;A &lt;span&gt;&lt;em&gt;Postman &lt;/em&gt;&lt;/span&gt;Collection to allow for quick testing and demonstration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;3. I want to focus on the relatively newer &lt;strong&gt;fine-grained SMART scopes&lt;/strong&gt;, not the basic ones. This is really the crux of the matter here. The other two above, are enablers for letting us focus just on this item.&lt;/p&gt;
&lt;h2&gt;SMART Scopes - The Granular Fine-grained Version&lt;/h2&gt;
&lt;p&gt;Above I generated (thank you NotebookLM) a nice infographic that summarizes the general syntax and usage of SMART scopes.&lt;/p&gt;
&lt;p&gt;In particular I want to focus on the filter part, the part in the scopes from the question mark (?).&lt;/p&gt;
&lt;p&gt;Here you can use standard FHIR Search syntax, with standard FHIR Search Parameters.&lt;/p&gt;
&lt;p&gt;Let's take this example:&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.amazonaws.com%2Fuploads%2Farticles%2Fq5n17wu4jx19gnypxukg.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.amazonaws.com%2Fuploads%2Farticles%2Fq5n17wu4jx19gnypxukg.png" alt=" " width="799" height="107"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of just allowing access (Read &amp;amp; Search in this case) to all categories of Observations, here we are allowing only access to lab results (category=laboratory).&lt;/p&gt;
&lt;p&gt;So to illustrate, instead of getting access to a set like this:&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.amazonaws.com%2Fuploads%2Farticles%2Fpb9gm49k6et2pd63kmro.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.amazonaws.com%2Fuploads%2Farticles%2Fpb9gm49k6et2pd63kmro.png" alt=" " width="800" height="714"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can limit the access to a set like this:&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.amazonaws.com%2Fuploads%2Farticles%2Fcmg0nc9x3461rasx2teq.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.amazonaws.com%2Fuploads%2Farticles%2Fcmg0nc9x3461rasx2teq.png" alt=" " width="800" height="705"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This allows for an ABAC (Attribute-Based Access Control) approach to access FHIR data (see more about this topic in the &lt;a href="https://build.fhir.org/security.html#binding" rel="noopener noreferrer"&gt;FHIR docs&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Some local national regulations mandate enforcing this kind of access control, including for example using security tags to limit access to data.&lt;/p&gt;
&lt;p&gt;One example is the ONC certification in the US, but other countries have similar demands.&lt;/p&gt;
&lt;p&gt;So supporting this is not only important for securing your data, it is also a hard requirement by local law, in a growing number of places.&lt;/p&gt;
&lt;h2&gt;The Power to Filter (or Not to)&lt;/h2&gt;
&lt;p&gt;You can control whether, if the FHIR request does not adhere exactly to the scopes, to filter out the unauthorized data and return just what is allowed, or to fail the request and return a 403 error HTTP status.&lt;/p&gt;
&lt;p&gt;This setting is in the FHIR endpoint Authorization settings:&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.amazonaws.com%2Fuploads%2Farticles%2Ft058qt6elitfrzr2jluw.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.amazonaws.com%2Fuploads%2Farticles%2Ft058qt6elitfrzr2jluw.png" alt=" " width="799" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note this is relevant not only to fine-grained scopes but also without using the ? filter. For example if you use _include or the $everything operation, this could filter "whole" Resource Types from the Result Set. &lt;/p&gt;
&lt;p&gt;Here's an example to illustrate -&lt;/p&gt;
&lt;h4&gt;Observation Search Example&lt;/h4&gt;
&lt;p&gt;Say we issue a Search for Observations, using Basic Authentication, so no SMART Scope are applied.&lt;/p&gt;
&lt;p&gt;You can see here we are getting back 793 Resources.&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.amazonaws.com%2Fuploads%2Farticles%2F0bt29oujkxpe55h8wqmf.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.amazonaws.com%2Fuploads%2Farticles%2F0bt29oujkxpe55h8wqmf.png" alt=" " width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Looking a little closer we can see for example the first one has a category of vital-signs:&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.amazonaws.com%2Fuploads%2Farticles%2F1eypyi5t0u33kjl0ckwl.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.amazonaws.com%2Fuploads%2Farticles%2F1eypyi5t0u33kjl0ckwl.png" alt=" " width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And in comparison if I use OAuth 2 authentication and have a scope of user/Observation.rs?category=laboratory, we get only 385 (vs. 793 above) Resources:&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.amazonaws.com%2Fuploads%2Farticles%2Fdzelpiswoe3m8426i671.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.amazonaws.com%2Fuploads%2Farticles%2Fdzelpiswoe3m8426i671.png" alt=" " width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; And the first one (instead of vital-signs) is usurpingly of a category of laboratory:&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.amazonaws.com%2Fuploads%2Farticles%2Fw5zcmyrm8cltdtx6lm2y.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.amazonaws.com%2Fuploads%2Farticles%2Fw5zcmyrm8cltdtx6lm2y.png" alt=" " width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A similar comparison can been seen with $everything -&lt;/p&gt;
&lt;h4&gt;$everyting Example&lt;/h4&gt;
&lt;p&gt;With Basic Authentication (no Scopes):&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.amazonaws.com%2Fuploads%2Farticles%2Fmjlpi45fufagp91fb78q.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.amazonaws.com%2Fuploads%2Farticles%2Fmjlpi45fufagp91fb78q.png" alt=" " width="799" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We get of course the Patient Resource itself, but also related Resources (per the example above): Encounter, Practitioner, Organization, Condition, Claim, ExplanationOfBenefit, Observation (of various types), MedicationRequest, Immunization, DiagnosticReport&lt;/p&gt;
&lt;p&gt;With OAuth (and scopes that include only: user/Patient.rs and user/Observation.rs?category=laboratory):&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.amazonaws.com%2Fuploads%2Farticles%2F3ahgk6ynaahkv24bposl.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.amazonaws.com%2Fuploads%2Farticles%2F3ahgk6ynaahkv24bposl.png" alt=" " width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here apart from the Patient itself, we only get Observations (laboratory ones), and no other related Resources.&lt;/p&gt;
&lt;p&gt;So, 171 Resources vs. 35 after the filtering.&lt;/p&gt;
&lt;h2&gt;Some Technical Notes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;As mentioned you need to be at least on v2026.1 to support this.&lt;/li&gt;
&lt;li&gt;Most FHIR Interactions are supported for these kind of Scopes (Create, Read, Update, Delete, Search), some not yet (History, VRead)&lt;/li&gt;
&lt;li&gt;As mentioned in the filter search string you can use standard FHIR Search syntax, but some parameters simply won't make sense in this context (like _include), so some might fail the request and others might simply be ignored, see referenced Docs for details.&lt;/li&gt;
&lt;li&gt;There are some notes re the $everything and $lastn, again see Docs for details.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Debugging&lt;/h2&gt;
&lt;p&gt;While using OAuth in general, and with SMART scopes in particular, not everything will work always as expected at first.&lt;/p&gt;
&lt;p&gt;Good resources to debug your situation will be the FHIR Server Log (aka FSLOG) and the HTTP Request Log (aka ISCLOG), see more details in &lt;a href="https://docs.intersystems.com/irisforhealth20261/csp/docbook/DocBook.UI.Page.cls?KEY=HXFHIRADM_server_debugMaintain#HXFHIRADM_server_debug_log" rel="noopener noreferrer"&gt;the Docs here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To illustrate here's an example -&lt;/p&gt;
&lt;p&gt;Say this time we turned off the filter results settings, and we're trying to Search for all Observation while our scope allows only laboratory.&lt;/p&gt;
&lt;p&gt;We will get a 403 Forbidden HTTP status:&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.amazonaws.com%2Fuploads%2Farticles%2Fpm6ihjxkoaa48t6qxj3l.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.amazonaws.com%2Fuploads%2Farticles%2Fpm6ihjxkoaa48t6qxj3l.png" alt=" " width="799" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if we turned on the FHIR Server Log, we can see something like this:&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.amazonaws.com%2Fuploads%2Farticles%2Fce2g0jfrh9xgajiv5py7.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.amazonaws.com%2Fuploads%2Farticles%2Fce2g0jfrh9xgajiv5py7.png" alt=" " width="799" height="48"&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.amazonaws.com%2Fuploads%2Farticles%2Fd6odvcizcas74buyipml.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.amazonaws.com%2Fuploads%2Farticles%2Fd6odvcizcas74buyipml.png" alt=" " width="794" height="41"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Sample Demo&lt;/h2&gt;
&lt;p&gt;Tackling a topic hands-on always helps and deepens the understanding, so I encourage you to take the related Open Exchange app for a ride. It is a very simple click &amp;amp; go sample, where a docker compose will build and start up everything you need, and includes a sample Postman Collection for you test drive with.&lt;/p&gt;
&lt;p&gt;Here's a recording of the demo from a READY 2026 session:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.intersystems.com/smarter-scopes-in-action-live-demo-of-smart-v2-with-is-fhir-server-intersystems/" rel="noopener noreferrer"&gt;SMARTer Scopes in Action - Live Demo of SMART v2 with InterSystems FHIR Server&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>oauth</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>Continuous integration in IRIS with Git and Jenkins</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 24 May 2026 14:36:04 +0000</pubDate>
      <link>https://dev.to/intersystems/continuous-integration-in-iris-with-git-and-jenkins-a87</link>
      <guid>https://dev.to/intersystems/continuous-integration-in-iris-with-git-and-jenkins-a87</guid>
      <description>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;In healthcare interoperability environments, InterSystems Health Connect typically contains critical components such as productions, business processes, operations, services, utility classes, routines, and other ObjectScript artifacts. Traditionally, many deployments of these components have been done manually, by copying classes, importing XML, or using administrative tools from the management portal.&lt;/p&gt;
&lt;p&gt;While this approach may work in the initial stages, it becomes difficult to maintain as the project grows, when multiple developers are working in parallel, or when repeatable deployments are needed across environments such as development, integration, pre-production, and production.&lt;/p&gt;
&lt;p&gt;A more robust alternative is to integrate Health Connect within a &lt;strong&gt;continuous integration&lt;/strong&gt; flow , using Git as the source code repository and Jenkins as the deployment orchestrator.&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.amazonaws.com%2Fuploads%2Farticles%2Fhl34gn2yqdfrs5k3m0d5.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.amazonaws.com%2Fuploads%2Farticles%2Fhl34gn2yqdfrs5k3m0d5.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The aim of this article is to show a practical approach to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Versioning Health Connect code on GitHub.&lt;/li&gt;
&lt;li&gt;Detect only the files modified since the last deployment.&lt;/li&gt;
&lt;li&gt;Copy those files to a staging folder.&lt;/li&gt;
&lt;li&gt;Load and compile the changes to a Health Connect namespace.&lt;/li&gt;
&lt;li&gt;Run the entire process remotely from Jenkins using SSH.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Architecture&lt;/h2&gt;
&lt;p&gt;For our example, we have configured the following elements:&lt;/p&gt;
&lt;h3&gt;IRIS for Health Instance&lt;/h3&gt;
&lt;p&gt;I have deployed InterSystems IRIS for Health on an AWS machine with RHEL10 with its own Apache Server and enabled connectivity via HTTP and SSH. &lt;/p&gt;
&lt;p&gt;For development, I have configured Visual Studio Code to work on a local instance of IRIS, on which I will make the code changes that I will then upload to GitHub.&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.amazonaws.com%2Fuploads%2Farticles%2F809bozgf0dgwdui9b5z3.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.amazonaws.com%2Fuploads%2Farticles%2F809bozgf0dgwdui9b5z3.png" alt=" " width="800" height="465"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt;GitHub repository&lt;/h3&gt;
&lt;p&gt;We have chosen GitHub as our version control system, taking advantage of the extension available in Visual Studio Code. This will allow us to work with branches if necessary.&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.amazonaws.com%2Fuploads%2Farticles%2Fyz64svj9sc5zv3kneiro.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.amazonaws.com%2Fuploads%2Farticles%2Fyz64svj9sc5zv3kneiro.png" alt=" " width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This element will be key to the CI/CD process since it is where we can obtain the latest code developed for deployment.&lt;/p&gt;
&lt;h3&gt;Jenkins&lt;/h3&gt;
&lt;p&gt;For those of you who don't know Jenkins, it's an open-source automation server widely used for continuous integration processes because it has a multitude of plugins that will make the task easier.&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.amazonaws.com%2Fuploads%2Farticles%2Fql4ffpcym4bfnezvjbgj.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.amazonaws.com%2Fuploads%2Farticles%2Fql4ffpcym4bfnezvjbgj.png" alt=" " width="800" height="215"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Jenkins has a Groovy scripting tool that allows us to implement the necessary steps for the integration process. For this example, we won't get too complicated.&lt;/p&gt;
&lt;h2&gt;Integration procedure&lt;/h2&gt;
&lt;p&gt;For this example, we've assumed we're working on an interoperability project with a DEVELOPMENT instance (deployed on the AWS server) where we want to deploy the changes developers make to their local instances for testing. The steps would be roughly as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The developer implements the functionalities in their local instance.&lt;/li&gt;
&lt;li&gt;The developer uploads changes to the corresponding branch of the GitHub repository.&lt;/li&gt;
&lt;li&gt;The person responsible for the deployment accesses Jenkins and launches a pipeline.&lt;/li&gt;
&lt;li&gt;Jenkins connects via SSH to the DEVELOPMENT server.&lt;/li&gt;
&lt;li&gt;A Linux script is running on the server.&lt;/li&gt;
&lt;li&gt;The script downloads the latest changes from the repository using a git pull.&lt;/li&gt;
&lt;li&gt;This script identifies new or modified files that are copied to a server directory.&lt;/li&gt;
&lt;li&gt;With the files identified, the script invokes a second script in ObjectScript.&lt;/li&gt;
&lt;li&gt;The second script loads and compiles the files into the IRIS for Health instance.&lt;/li&gt;
&lt;li&gt;If the upload was successful, the script restarts production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As you can see, we have chosen a very basic operation, but one that can be quite helpful.&lt;/p&gt;
&lt;p&gt;Let's now take a look at the scripts we will run using Jenkins on our DEVELOPMENT server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash &lt;br&gt;
set -euo pipefail 
&lt;h1&gt;
  
  
  =========================
&lt;/h1&gt;
&lt;h1&gt;
  
  
  Configuration
&lt;/h1&gt;
&lt;h1&gt;
  
  
  =========================
&lt;/h1&gt;

&lt;p&gt;REPO_URL="&lt;a href="https://github.com/intersystems-ib/workshop-cicd-demo" rel="noopener noreferrer"&gt;https://github.com/intersystems-ib/workshop-cicd-demo&lt;/a&gt;" &lt;br&gt;
BRANCH="main" &lt;/p&gt;

&lt;h1&gt;
  
  
  Local clone used to compare commits
&lt;/h1&gt;

&lt;p&gt;CACHE_REPO="/opt/git-cache/project_repo" &lt;/p&gt;

&lt;h1&gt;
  
  
  Folder to copy the files to be uploaded into Health Connect
&lt;/h1&gt;

&lt;p&gt;EXPORT_DIR="/projectGit" &lt;/p&gt;

&lt;h1&gt;
  
  
  File with the latest processed commit
&lt;/h1&gt;

&lt;p&gt;STATE_FILE="${CACHE_REPO}/.last_sync_commit" &lt;/p&gt;

&lt;h1&gt;
  
  
  CLean up EXPORT_DIR before to copy the new updates
&lt;/h1&gt;

&lt;p&gt;CLEAN_EXPORT_DIR="true" &lt;/p&gt;

&lt;h1&gt;
  
  
  =========================
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Validations
&lt;/h1&gt;

&lt;h1&gt;
  
  
  =========================
&lt;/h1&gt;

&lt;p&gt;if ! command -v git &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then &lt;br&gt;
  echo "Error: git is not installed." &lt;br&gt;
  exit 1 &lt;br&gt;
fi &lt;/p&gt;

&lt;p&gt;mkdir -p "${EXPORT_DIR}" &lt;br&gt;
mkdir -p "$(dirname "${CACHE_REPO}")" &lt;/p&gt;

&lt;h1&gt;
  
  
  =========================
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Clone or update cache folder
&lt;/h1&gt;

&lt;h1&gt;
  
  
  =========================
&lt;/h1&gt;

&lt;p&gt;if [ ! -d "${CACHE_REPO}/.git" ]; then &lt;br&gt;
  echo "Cloning repository into cache..." &lt;br&gt;
  git clone --branch "${BRANCH}" "${REPO_URL}" "${CACHE_REPO}" &lt;br&gt;
else &lt;br&gt;
  echo "Updating local cache..." &lt;br&gt;
  git -C "${CACHE_REPO}" fetch origin &lt;br&gt;
  git -C "${CACHE_REPO}" checkout "${BRANCH}" &lt;br&gt;
  git -C "${CACHE_REPO}" reset --hard "origin/${BRANCH}" &lt;br&gt;
fi &lt;/p&gt;

&lt;p&gt;REMOTE_COMMIT="$(git -C "${CACHE_REPO}" rev-parse HEAD)" &lt;/p&gt;

&lt;h1&gt;
  
  
  =========================
&lt;/h1&gt;

&lt;h1&gt;
  
  
  First execution
&lt;/h1&gt;

&lt;h1&gt;
  
  
  =========================
&lt;/h1&gt;

&lt;p&gt;if [ ! -f "${STATE_FILE}" ]; then &lt;br&gt;
  echo "First execution." &lt;br&gt;
  echo "Copying all the contains from branch into ${EXPORT_DIR}..." &lt;/p&gt;

&lt;p&gt;if [ "${CLEAN_EXPORT_DIR}" = "true" ]; then &lt;br&gt;
    find "${EXPORT_DIR}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + &lt;br&gt;
  fi &lt;/p&gt;

&lt;p&gt;rsync -av --delete --exclude ".git" "${CACHE_REPO}/" "${EXPORT_DIR}/" &lt;/p&gt;

&lt;p&gt;echo "${REMOTE_COMMIT}" &amp;gt; "${STATE_FILE}" &lt;br&gt;
  echo "First export finished." &lt;br&gt;
  exit 0 &lt;br&gt;
fi &lt;/p&gt;

&lt;p&gt;LAST_COMMIT="$(cat "${STATE_FILE}")" &lt;/p&gt;

&lt;p&gt;if [ "${LAST_COMMIT}" = "${REMOTE_COMMIT}" ]; then &lt;br&gt;
  echo "No updates." &lt;br&gt;
  exit 0 &lt;br&gt;
fi &lt;/p&gt;

&lt;p&gt;echo "Comparing commits:" &lt;br&gt;
echo "  anterior: ${LAST_COMMIT}" &lt;br&gt;
echo "  actual:   ${REMOTE_COMMIT}" &lt;/p&gt;

&lt;p&gt;if [ "${CLEAN_EXPORT_DIR}" = "true" ]; then &lt;br&gt;
  echo "Cleaning up export folder..." &lt;br&gt;
  find "${EXPORT_DIR}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + &lt;br&gt;
fi &lt;/p&gt;

&lt;h1&gt;
  
  
  =========================
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Export just added or modified files
&lt;/h1&gt;

&lt;h1&gt;
  
  
  =========================
&lt;/h1&gt;

&lt;p&gt;while IFS= read -r -d '' status &amp;amp;&amp;amp; IFS= read -r -d '' path1; do &lt;br&gt;
  case "${status}" in &lt;br&gt;
    M|A) &lt;br&gt;
      echo "Exporting ${status}: ${path1}" &lt;br&gt;
      mkdir -p "${EXPORT_DIR}/$(dirname "${path1}")" &lt;br&gt;
      cp -f "${CACHE_REPO}/${path1}" "${EXPORT_DIR}/${path1}" &lt;br&gt;
      ;; &lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;D) 
  # Ignoring deletes 
  echo "Ignoring deleted: ${path1}" 
  ;; 

R*) 
  IFS= read -r -d '' path2 
  echo "Exporting renamed: ${path1} -&amp;amp;gt; ${path2}" 
  mkdir -p "${EXPORT_DIR}/$(dirname "${path2}")" 
  cp -f "${CACHE_REPO}/${path2}" "${EXPORT_DIR}/${path2}" 
  ;; 

*) 
  echo "Change not automatically managed: ${status} ${path1}" 
  ;; 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;esac &lt;br&gt;
done &amp;lt; &amp;lt;(git -C "${CACHE_REPO}" diff --name-status -z "${LAST_COMMIT}" "${REMOTE_COMMIT}") &lt;/p&gt;

&lt;p&gt;echo "${REMOTE_COMMIT}" &amp;gt; "${STATE_FILE}" &lt;br&gt;
echo "Incremental export concluded in ${EXPORT_DIR}"&lt;br&gt;
echo "Starting file upload and compile in Health Connect" &lt;br&gt;
(echo '_system'; echo 'SYS'; cat iris.script) | iris session IRISHEALTH &lt;br&gt;
echo "Compilation successfully finished" &lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, this script executes the git pull on our GitHub repository, updates the source code in a directory on the DEVELOPMENT server, detects the changes compared to the last downloaded version, extracts them to a second directory ( &lt;strong&gt;/projectGit&lt;/strong&gt; ) and finally invokes the IRIS script.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(echo '_system'; echo 'SYS'; cat iris.script) | iris session IRISHEALTH &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those first two &lt;strong&gt;echo &lt;/strong&gt;commands will allow us to pass the username and password to the terminal session we need to open to run our ObjectScript script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zn "DEMO" &lt;br&gt;
set sc = $SYSTEM.OBJ.LoadDir("/projectGit/src/Demo", "ck", , 1) &lt;br&gt;
if '$SYSTEM.Status.IsOK(sc) do $SYSTEM.Status.DisplayError(sc) quit &lt;br&gt;
set production = "Demo.Order.Production" &lt;br&gt;
set ^Ens.Configuration("csp","LastProduction") = production &lt;br&gt;
do ##class(Ens.Director).SetAutoStart(production) &lt;br&gt;
do ##class(Ens.Director).StartProduction(production) &lt;br&gt;
write !,"Produccion iniciada correctamente: ",production,! &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This script is where we import the classes we've identified as modified or created and compile them. If the compilation is successful, we restart the corresponding production environment of our DEMO namespace so that the changes are implemented.&lt;/p&gt;
&lt;p&gt;Perfect, we have our scripts, our DEVELOPMENT server and our GitHub, let's configure our Jenkins.&lt;/p&gt;
&lt;h2&gt;Configuring Jenkins&lt;/h2&gt;
&lt;p&gt;Before we start creating our pipeline, we must install a plugin that allows us to connect via SSH to our DEVELOPMENT server with our primary username and password.&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.amazonaws.com%2Fuploads%2Farticles%2F9noio8ynmf9s94fskinv.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.amazonaws.com%2Fuploads%2Farticles%2F9noio8ynmf9s94fskinv.png" alt=" " width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the Jenkins configuration, we created an access credential to our DEVELOPMENT server:&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.amazonaws.com%2Fuploads%2Farticles%2Fsffpb4igwj2jeqpfbu59.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.amazonaws.com%2Fuploads%2Farticles%2Fsffpb4igwj2jeqpfbu59.png" alt=" " width="800" height="1090"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And finally we proceed to create the Pipeline.&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.amazonaws.com%2Fuploads%2Farticles%2F0gsrqiumemwhc3toso8x.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.amazonaws.com%2Fuploads%2Farticles%2F0gsrqiumemwhc3toso8x.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Within the pipeline configuration, we define the following script that will allow us to deploy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipeline {&lt;br&gt;
    agent any
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parameters {
    string(name: 'GIT_BRANCH', defaultValue: 'main', description: 'Repository branch')
    string(name: 'REMOTE_HOST', defaultValue: 'ec2-**-**-***-**.**-*****.compute.amazonaws.com', description: 'Remote Host')
    string(name: 'REMOTE_USER', defaultValue: 'ec2-user', description: 'Remote SSH user')
    string(name: 'REMOTE_SCRIPT_NAME', defaultValue: 'shell_script.sh', description: 'Remote script name')
}

environment {
    REPO_URL = 'https://github.com/intersystems-ib/workshop-cicd-demo'
    SSH_CREDENTIALS_ID = 'ssh-healthconnect-remote'
}

stages {
    stage('Checkout') {
        steps {
            git branch: "${params.GIT_BRANCH}", url: "${env.REPO_URL}"
        }
    }

    stage('Validate script') {
        steps {
            sh '''
                set -eu
                test -f shell_script.sh
                chmod +x shell_script.sh
            '''
        }
    }

    stage('Launch remote script') {
        steps {
            sshagent(credentials: ["${env.SSH_CREDENTIALS_ID}"]) {
                sh '''
                    set -eu

                    ssh -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" \
                      "sudo sh '/${REMOTE_SCRIPT_NAME}'" | tee remote_execution.log
                '''
            }
        }
    }
}

post {
    always {
        archiveArtifacts artifacts: 'remote_execution.log', allowEmptyArchive: true
    }
    success {
        echo 'Remote deployment successfully finished.'
    }
    failure {
        echo 'Remote deployment failed. Check remote_execution.log.'
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What does our script do? Very simple, it checks that our GitHub repository exists with its associated branch and then, via SSH, sends the instruction to execute the Linux script that will be in charge of downloading and updating our instance.&lt;/p&gt;
&lt;p&gt;Let's see it in action with a small example.&lt;/p&gt;
&lt;h2&gt;Running the process&lt;/h2&gt;
&lt;p&gt;Our production is running normally and we want to make a change to one of our components so that the default value shown in one of the parameters is different:&lt;/p&gt;
&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2Fdpst17pm4hv4qdf3y3si.png" alt=" " width="799" height="355"&gt;

&lt;p&gt;Now we want our &lt;strong&gt;TenantId&lt;/strong&gt; parameter to have the value ZZZ-999, great, let's correct the code we have in our local instance from Visual Studio Code and upload the change to our GitHub.&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.amazonaws.com%2Fuploads%2Farticles%2F057g8lmzb696c2z3148s.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.amazonaws.com%2Fuploads%2Farticles%2F057g8lmzb696c2z3148s.png" alt=" " width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;With our change now pushed to our repository, we can run the pipeline from our Jenkins instance. Let's see the pipeline's output:&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.amazonaws.com%2Fuploads%2Farticles%2F965l4ikdejfqzv7yfklr.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.amazonaws.com%2Fuploads%2Farticles%2F965l4ikdejfqzv7yfklr.png" alt=" " width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Everything is correct; it has detected our change and executed the script successfully.&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.amazonaws.com%2Fuploads%2Farticles%2F1v5bc0c5uju6rhvavd91.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.amazonaws.com%2Fuploads%2Farticles%2F1v5bc0c5uju6rhvavd91.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's verify that the parameter has changed and production has restarted successfully.&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.amazonaws.com%2Fuploads%2Farticles%2Fzbztxqx2s4rwrczlin84.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.amazonaws.com%2Fuploads%2Farticles%2Fzbztxqx2s4rwrczlin84.png" alt=" " width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There we have our new TenantId! A complete and resounding success!&lt;/p&gt;
&lt;h2&gt;Conclusions and next steps.&lt;/h2&gt;
&lt;p&gt;As you may have noticed, there are no technological limitations from IRIS for participating in a continuous integration process. You simply need the appropriate scripts that best suit your daily operations.&lt;/p&gt;
&lt;p&gt;In this article we have seen a small example of continuous integration with IRIS for Health, but this could be expanded to certain configurations that could be deployed using features such as Configuration Merge.&lt;/p&gt;
&lt;p&gt;Give it a try!&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>automation</category>
      <category>programming</category>
      <category>github</category>
    </item>
    <item>
      <title>Introducing iris-synthetic-data-gen</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 17 May 2026 15:46:56 +0000</pubDate>
      <link>https://dev.to/intersystems/introducing-iris-synthetic-data-gen-2l8j</link>
      <guid>https://dev.to/intersystems/introducing-iris-synthetic-data-gen-2l8j</guid>
      <description>&lt;p&gt;Today I have published a new &lt;a href="https://openexchange.intersystems.com/package/iris-synthetic-data-gen" rel="noopener noreferrer"&gt;Open Exchange package&lt;/a&gt; for generation of Synthetic Data directly into IRIS.&lt;/p&gt;
&lt;p&gt; It can be a frustrating process to find decent datasets when you are looking to make a demo app. Maybe the dataset doesn't matter that much, but you still want it to appear somewhat genuine and with several linked tables that are usable directly within IRIS with the neat implicit joins with &lt;code&gt;-&amp;gt;&lt;/code&gt;. Maybe you just want linked tables that are easily installable with IPM to benchmark queries, this dataset generation would be perfect.&lt;/p&gt;
&lt;p&gt;I have opted to create datasets using Embedded Python, these datasets are configurable by custom config files. The datasets are generated directly with a single IRIS class method, and can be scaled with a multiplier to create however small or large datasets you want without having to measure configs.&lt;/p&gt;
&lt;p&gt;At the moment I have four datasets:&lt;br&gt;- Financial services (e.g. Bank Cards, accounts, transactions )&lt;br&gt;- Retail (Stores, Products, Users, Inventory)&lt;br&gt;- Supply Chain (products, sales orders, inventory movement)&lt;br&gt;- Theme Park management (parks, zones, rides, incidents)&lt;/p&gt;
&lt;p&gt;I am not an expert in any of these domains, so I doubt they are super accurate, and the data generation uses python libraries like &lt;code&gt;faker&lt;/code&gt; and statistical weighted generation with &lt;code&gt;numpy&lt;/code&gt;, so it all feels a bit synthetic.&lt;/p&gt;
&lt;p&gt;I will also be honest that, as a side-of-desk project which I couldn't give a huge amount of time to, this project was only made possible by AI. I used AI extensively for the design of datasets and the generation of the code to create the datasets. I supervised, tested for personal use cases and was very involved with the project design, but the code is all AI generated and I have not carefully reviewed the dataset generation process.&lt;/p&gt;
&lt;p&gt;For me, this project is a great use case for full "vibe coding" i.e. letting the agent handle the entire coding process. That is to say, the consequences of bugs is low as these datasets are not designed for any production use. The code can largely be judged on the results outputted, in the knowledge that the details or edge cases don't matter.&lt;/p&gt;
&lt;p&gt;Its also a good template to make new datasets - the first of the datasets took me a couple of hours of careful planning, discussion with agents, and iterating as to how best to create the dataset and add it to IRIS. Whereas for the last dataset, I could ask the agent "Create a new dataset with retail tables that is configured and generated like the others here", and it did a pretty good job without any real oversight.&lt;/p&gt;
&lt;p&gt;I hope this can be useful for some, and feel free to give feedback, contributions or to use it as a template to make your own synthetic datasets!&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>embeddedpython</category>
      <category>productivity</category>
    </item>
    <item>
      <title>An Introduction to AI Hub, Part 1: Agents in ObjectScript</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 17 May 2026 15:44:11 +0000</pubDate>
      <link>https://dev.to/intersystems/an-introduction-to-ai-hub-part-1-agents-in-objectscript-2p1e</link>
      <guid>https://dev.to/intersystems/an-introduction-to-ai-hub-part-1-agents-in-objectscript-2p1e</guid>
      <description>&lt;p&gt;For those of you that weren't at READY last week, you may have missed the exciting announcement that the Early Access Program for AI Hub is officially open. It was announced during an amazing demo from &lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/benjamin"&gt;@benjamin&lt;/a&gt;.DeBoe&lt;/span&gt; and &lt;span&gt;@Jeffrey.Fried&lt;/span&gt;, I recommend catching up with this demo when the recording is released!  I had the opportunity to play with AI Hub in advance, and thought I might share an introduction with the community.&lt;/p&gt;
&lt;p&gt;Before getting into the details, &lt;a href="https://github.com/intersystems-community/ai-hub-eap/tree/master" rel="noopener nofollow noreferrer"&gt;here is a link for the documentation&lt;/a&gt; and &lt;a href="https://evaluation.intersystems.com/Eval/early-access/AIHub" rel="noopener nofollow noreferrer"&gt;here is a link to the EAP portal to download AI Hub&lt;/a&gt;, its currently available as standalone install kits or container images. &lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Please note, this is a preview and there are likely to be significant changes before the official release, it is not designed for production use, and you may run into some issues - if you do, raise an issue on the Github page!&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2&gt;Agents&lt;/h2&gt;
&lt;p&gt;The most exciting feature, for me at least, has been the new ObjectScript agents SDK. You can now create agents and tools directly in ObjectScript, using an intuitive SDK.&lt;/p&gt;
&lt;p&gt;Creating an Agent is simple you can give it a system prompt with the &lt;code&gt;XData INSTRUCTIONS&lt;/code&gt; component, then just set the provider, model and tools:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Sample.Agent Extends %AI.Agent
{
    /// LLM Model
    Parameter MODEL = "gpt-5-nano";

    /// Toolsets that the agent can use
    Parameter TOOLSETS = "Sample.ToolSet";
    
    /// System Prompt
    XData INSTRUCTIONS [ MimeType = text/markdown ]
    {
    # Sample Assistant

    You are a helpful assistant with access to a set of tools to interact with a database of people.
    }

    Method %OnInit() As %Status
    {
        // Set provider with API key from environment variable
        Set key = $System.Util.GetEnviron("OPENAI_API_KEY")  // or whatever
        Set ..Provider = ##class(%AI.Provider).Create("openai", {"api_key": (key)})
        
        Return $$$OK
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Tools&lt;/h2&gt;
&lt;p&gt;Tools are even easier to create - its as simple as extending &lt;code&gt;%AI.Tools&lt;/code&gt;, after that, all methods, class methods and queries become tools that agents can use. So we can do something like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Sample.Tools Extends %AI.Tool [dependsOn=Sample.Person]
{

/// Tool to add a person to the database
Method AddPerson(name As %String, age As %Integer) As %Status{
   Set person = ##class(Sample.Person).%New()
   Set person.Name = name
   Set person.Age = age
   Set sc =  person.%Save()
   Quit sc
}

/// Tool query database for people younger than a specified age
Query GetPeopleYoungerThan(age As %Integer) As %SQLQuery(ROWSPEC = "Name:%String,Age:%Integer") [ SqlProc ]
{
   SELECT Name, Age From Sample.Person Where Age &amp;lt; :age
}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tools can also be organised into toolsets, these are, as the name suggests, sets of tools that can be used to combine many tools from different classes, filter tools by regex matching, add policies and use MCP servers defined outside of IRIS.&lt;/p&gt;
&lt;p&gt;In the example below we combine the tools we defined above, &lt;code&gt;Sample.Tools&lt;/code&gt;, with a policy which logs tool calls to the terminal (&lt;code&gt;%AI.Policy.ConsoleAudit&lt;/code&gt;) and a custom Python MCP server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Class Sample.ToolSet Extends %AI.ToolSet [DependsOn=Sample.Tools]
{
    XData Definition
    {
        &amp;lt;ToolSet&amp;gt;
            &amp;lt;Description&amp;gt;Sample Toolset&amp;lt;/Description&amp;gt;
            
            &amp;lt;Policies&amp;gt;
           &amp;lt;!--Policy to Log tool calls to Console--&amp;gt;
                &amp;lt;Audit Class="%AI.Policy.ConsoleAudit"/&amp;gt;
            &amp;lt;/Policies&amp;gt;
            
            &amp;lt;!--ObjectScript Tools--&amp;gt;
            &amp;lt;Include Class="Sample.Tools"&amp;gt;&amp;lt;/Include&amp;gt;
            
            &amp;lt;!--Python MCP Server created with FastMCP--&amp;gt;
            &amp;lt;MCP Name="PythonServer"&amp;gt; 
                &amp;lt;Stdio Executable="/usr/irissys/bin/irispython" 
                Args="/home/irisowner/dev/src/Python/multiplication_mcp.py" /&amp;gt;
            &amp;lt;/MCP&amp;gt;
            
        &amp;lt;/ToolSet&amp;gt;
    }    
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Other ObjectScript features&lt;/h2&gt;
&lt;p&gt;There are a load more of cool features to create super powerful agents, including support for agent skills (&lt;code&gt;%AI.Agent.Skill&lt;/code&gt;), delegation of tasks to subagents (&lt;code&gt;%AI.Agent.SubAgent&lt;/code&gt;) and tools for creating knowledge bases with RAG (&lt;code&gt;%AI.RAG&lt;/code&gt;). You can also create custom audit or authentication policies, to either log tool calls or decide whether they should be allowed.&lt;/p&gt;
&lt;p&gt;One very cool feature is that tools and toolsets can be &lt;code&gt;stateful&lt;/code&gt;, meaning they retain the state between tool calls. As such, a tool could be called multiple times, with the actions of the previous tool call being retained. For example, a file could be opened once and the contents 'remembered' the next time the tool is called. To use this, define tools with methods (instead of class methods) and save attributes as properties. There's a nice example of this in the &lt;a href="https://github.com/intersystems-community/ai-hub-eap/blob/master/ObjectScript_SDK_Guide.md#stateful-tools-instance-methods" rel="noopener nofollow noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I've been working with AI hub for well over a month now, and am still overwhelmed by the amount of features, particularly at the advanced end, that I still need to explore.&lt;/p&gt;
&lt;h2&gt;Template&lt;/h2&gt;
&lt;p&gt;If you want to start playing around with AI Hub, I published a &lt;a href="https://openexchange.intersystems.com/package/ai-hub-dev-template" rel="noopener noreferrer"&gt;dev template to the Open Exchange&lt;/a&gt; which includes instructions for downloading and building the AI Hub container, and has a few pre-loaded sample classes (you might recognise them from this article). It even has some agent skills, in case you'd like your AI agent of choice to know what's in the documentation before you do!&lt;/p&gt;
&lt;p&gt;It even creates an MCP server and has instructions on how to connect to it.&lt;/p&gt;
&lt;h2&gt;Next time&lt;/h2&gt;
&lt;p&gt;In my next article, I'll show how you can package your agent tools into an MCP server to connect directly to your data from any MCP client!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gpt3</category>
      <category>documentation</category>
      <category>programming</category>
    </item>
    <item>
      <title>IRIS Dockerization and Embedded Python for Data Science — One-Command Setup for Reproducible ML Workflows</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Thu, 30 Apr 2026 15:41:55 +0000</pubDate>
      <link>https://dev.to/intersystems/iris-dockerization-and-embedded-python-for-data-science-one-command-setup-for-reproducible-ml-am4</link>
      <guid>https://dev.to/intersystems/iris-dockerization-and-embedded-python-for-data-science-one-command-setup-for-reproducible-ml-am4</guid>
      <description>&lt;p&gt;1-command only required for an entire IRIS instance for Data Science projects, and leveraging this to compare query methods' speed (Dynamic SQL, Pandas Query, and Globals).&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.amazonaws.com%2Fuploads%2Farticles%2Fi0hxffduy4g1brhtt646.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.amazonaws.com%2Fuploads%2Farticles%2Fi0hxffduy4g1brhtt646.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before joining InterSystems, I worked in a team of web developers as a data scientist. Most of my day-to-day work involved training and embedding ML models in Python-based backend applications through microservices, mainly built with the Django framework and using Postgres SQL for sourcing the data. During development, testing, and deployment, I realized the importance of repeatability of results, both for the model’s inferences and for the performance inside the application, regardless of the hardware being used to run the code.&lt;/p&gt;

&lt;p&gt;This naturally went hand in hand with adopting good coding practices, such as modularization to reduce code repeatability and boilerplate, making maintenance easier and speeding up development. For this reason, Docker in particular became an essential tool in our workflow, not only for scalability and ease of deployment, but also to reduce human error and ensure that code behaves the same way everywhere, regardless of the underlying machine.&lt;/p&gt;

&lt;p&gt;When I joined InterSystems, I was immediately impressed by the robustness of IRIS as a data platform. Its resilience to human error when following guidelines to create services through productions, the multi-model nature of how information can be stored, and, in particular, the lightning-fast access to data through globals opened my eyes to a different way of thinking about performance and data access patterns, especially when compared to a traditional relational-only mindset.&lt;/p&gt;

&lt;p&gt;I was also lucky to join the company (September 2025) at a time when a rich ecosystem of tools was already in place, significantly flattening the learning curve. The VS Code ObjectScript Extension Pack, Embedded Python, the official IRIS Docker images, and the InterSystems Package Manager (IPM) for easily importing ObjectScript packages (&lt;a href="https://github.com/intersystems/ipm" rel="noopener noreferrer"&gt;https://github.com/intersystems/ipm&lt;/a&gt;) quickly became my everyday toolbelt.&lt;/p&gt;

&lt;p&gt;After about three months, I felt confident enough working with this stack that I started standardizing my own development environment. In this article, I’d like to share how I set up a fully containerized IRIS instance for Data Science projects using Docker—ready to use Embedded Python out of the box, with all required dependencies installed from both Python’s &lt;code&gt;pip&lt;/code&gt; and IPM.&lt;/p&gt;

&lt;p&gt;I’ll also use this setup to share some insights on the incredible speed of using globals to query tables, in a practical scenario where the popular gradient boosting model &lt;strong&gt;LightGBM&lt;/strong&gt; is used to train and make inferences on a mock dataset. This allows us to measure inference speed while comparing the different querying approaches available in IRIS.&lt;/p&gt;

&lt;p&gt;Some important highlights that will be addressed in this article are how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Link custom Python packages during the Docker build process, so they can be imported naturally (e.g. &lt;code&gt;from mypythonpackage import myclassorfunc&lt;/code&gt;) inside any Embedded Python methods living on ObjectScript classes, without repetitive boilerplate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Automatically execute IRIS terminal commands as soon as the container starts, which in this scenario is used to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Import custom ObjectScript packages into IRIS.&lt;/li&gt;
&lt;li&gt;Install IPM and, through it, Shavrov’s &lt;code&gt;csvgenpy&lt;/code&gt; utility
(&lt;a href="https://community.intersystems.com/post/csvgenpy-import-any-csv-intersystems-iris-using-embedded-python" rel="noopener noreferrer"&gt;https://community.intersystems.com/post/csvgenpy-import-any-csv-intersystems-iris-using-embedded-python&lt;/a&gt;),
used to create and populate new tables from a single CSV file.&lt;/li&gt;
&lt;li&gt;Check whether an IRIS table already exists and, if it doesn’t, populate it using &lt;code&gt;csvgenpy&lt;/code&gt; with a CSV file mounted into the container via Docker volumes.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;All of this by only running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker-compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, the repository accompanying this article uses this setup to create a complete IRIS environment with all the tools and data needed to compare different ways of querying the same IRIS table and converting the results into a Pandas DataFrame (NumPy-based), which is typically what gets passed to Python-based machine learning models.&lt;/p&gt;

&lt;p&gt;The comparison includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dynamic SQL queries&lt;/li&gt;
&lt;li&gt;Pandas querying the table directly&lt;/li&gt;
&lt;li&gt;Direct access through globals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each approach, execution time is measured to quantitatively compare the performance of the different querying methods. This analysis shows that direct global access provides the lowest-latency data retrieval for machine learning inference workloads by far.&lt;/p&gt;

&lt;p&gt;At the same time, consistency across querying methods is validated by asserting equality of the resulting Pandas DataFrames, ensuring that identical dataframes (and therefore identical downstream ML predictions) are produced regardless of the query mechanism used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── docker-compose.yml             # Docker orchestration configuration
├── dockerfile                     # Multi-stage build with IRIS + Python
├── iris_autoconf.sh               # Auto-configuration script for IRIS terminal commands
├── requirements.txt               # Python libraries
├── MockPackage/                   # Custom package
│   ├── MockDataManager.cls        # Data management utilities
│   ├── MockModelManager.cls       # ML model training
│   └── MockInference.cls          # Data retrieval and inference benchmarks
├── python_utils/                  # Custom Python packages
│   ├── __init__.py
│   ├── utils.py                   # ML preprocessing &amp;amp; inference
|   └── querymethods.py            # Methods for Querying IRIS tables
└── dur/                           # Volume for durable data on host machine and container
    ├── data/                      # CSV datasets
    └── models/                    # Trained LightGBM models
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Dockerization of IRIS
&lt;/h2&gt;

&lt;p&gt;This section describes the main building blocks used to dockerize a Python-ready IRIS instance. The goal here is not only to run IRIS inside a container, but to do so in a way that makes it immediately usable for Data Science workflows: Embedded Python enabled, Python dependencies installed, ObjectScript packages available through IPM, and data automatically loaded when the container starts.&lt;/p&gt;

&lt;p&gt;The setup relies on three main components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker-compose.yml&lt;/code&gt; to define how the IRIS container is built and run&lt;/li&gt;
&lt;li&gt;a multi-stage &lt;code&gt;Dockerfile&lt;/code&gt; to prepare Embedded Python and dependencies&lt;/li&gt;
&lt;li&gt;an &lt;code&gt;iris_autoconf.sh&lt;/code&gt; script to automate IRIS-side configuration at startup&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  docker-compose.yml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: '3.8'

services:
  iris:
    build: # How is the image built
      context: . # Path to the directory containing the Dockerfile
      dockerfile: Dockerfile # Name of the Dockerfile
    container_name: iris-experimentation # Name of the container
    ports:
      - "1972:1972"    # SuperServer port
      - "52773:52773"  # Management Portal/Web Gateway
    volumes:
      - ./dur/.:/dur:rw # map host directory to container directory with read-write permissions
    restart: always # Always restart the container if it stops (unless explicitly stopped)
    healthcheck:
      test: ["CMD", "iris", "session", "iris", "-U", "%SYS", "##class(SYS.Database).GetMountedSize()"] # Health check command
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    command: --after "/usr/irissys/iris_autoconf.sh" # Run autoconf script after startup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker Compose specifies how the IRIS container is built, which ports are exposed, how storage is handled, and what commands are executed at startup. In particular, I want to highlight the following points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;volumes: ./dur/.:/dur:rw&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates the &lt;code&gt;/dur&lt;/code&gt; directory inside the container and maps it to &lt;code&gt;./dur&lt;/code&gt; (relative to the location of &lt;code&gt;docker-compose.yml&lt;/code&gt;) on the host machine, with both read and write permissions.&lt;/p&gt;

&lt;p&gt;In practice, this means that both the host machine and the container share the same path. This makes it very easy to load files into IRIS and inspect or modify them from the host without any extra copying steps.&lt;/p&gt;

&lt;p&gt;In this project, this is how the &lt;code&gt;/data&lt;/code&gt; and &lt;code&gt;/models&lt;/code&gt; folders are directly made available inside the container under &lt;code&gt;/dur&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;command: --after "/usr/irissys/iris_autoconf.sh"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This command allows the execution of a bash script immediately after the container is up and running. The script contains all the commands needed to open an IRIS terminal session and execute any required IRIS-side configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The commands in this script are executed every time the container starts. This means that if the container goes down for any reason and restarts (for example, due to &lt;code&gt;restart: always&lt;/code&gt;), all the commands in this script will be executed again. If this behavior is not taken into account when writing the script, it can lead to unintended side effects such as reinstalling packages or resetting tables.&lt;/p&gt;

&lt;h3&gt;
  
  
  dockerfile
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Stage 1: Build stage for installing dependencies
FROM python:3.12-slim AS builder

# Set the working directory
WORKDIR /app

# Copy the requirements file into the image
COPY requirements.txt requirements.txt

# Install the Python dependencies into a temporary location
RUN pip install --no-cache-dir --target /install -r requirements.txt

# Stage 2: Final image with InterSystems IRIS and the installed Python libraries
FROM containers.intersystems.com/intersystems/iris-community:latest-em

# Switch to the root user to install necessary system packages
USER root

# Install the correct Python 3.12 development library for Ubuntu Noble
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y libpython3.12-dev wget &amp;amp;&amp;amp; \
    rm -rf /var/lib/apt/lists/*

# Set the environment variables for Embedded Python
ENV PythonRuntimeLibrary=/usr/lib/x86_64-linux-gnu/libpython3.12.so
ENV PythonRuntimeLibraryVersion=3.12

# Update the LD_LIBRARY_PATH
ENV LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}

# Copy the installed Python packages from the builder stage
COPY --from=builder /install /usr/irissys/mgr/python

# Your own Python package
COPY python_utils /usr/irissys/mgr/python/python_utils
ENV PYTHONPATH=/usr/irissys/mgr/python:${PYTHONPATH}


# Copy ObjectScript classes into the image
COPY MockPackage /usr/irissys/mgr/MockPackage
# Copy and set permissions for the autoconf script while still root
COPY iris_autoconf.sh /usr/irissys/iris_autoconf.sh
RUN chmod +x /usr/irissys/iris_autoconf.sh

# Switch back to the default `irisowner` user
USER irisowner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a two-stage Dockerfile.&lt;/p&gt;

&lt;p&gt;The first stage is a lightweight build stage used to install all Python dependencies listed in &lt;code&gt;requirements.txt&lt;/code&gt; into a temporary directory. This keeps the final image clean and avoids installing build tools directly into the IRIS image.&lt;/p&gt;

&lt;p&gt;The second stage is based on the official InterSystems IRIS image. Here, the Python runtime library required for Embedded Python is installed, and IRIS is configured so that Embedded Python can recognize both the runtime library and all installed Python packages, including custom ones.&lt;/p&gt;

&lt;p&gt;It is worth highlighting the following configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embedded Python runtime configuration&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ENV PythonRuntimeLibrary=/usr/lib/x86_64-linux-gnu/libpython3.12.so
  ENV PythonRuntimeLibraryVersion=3.12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These environment variables achieve what would otherwise be configured manually through the Management Portal by navigating to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;System Administration → Configuration → Additional Settings → Advanced Memory&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;and updating the Embedded Python runtime settings. Defining them in the Dockerfile makes the configuration explicit, reproducible, and version-controlled.&lt;/p&gt;

&lt;p&gt;Additionally, the classes inside the package "MockPackage" are copied inside the container through:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COPY MockPackage /usr/irissys/mgr/MockPackage&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;to be later on, automatically imported to IRIS when the the following bash file is executed after the container is up and running.&lt;/p&gt;

&lt;h3&gt;
  
  
  iris_autoconf.sh
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
set -e

iris session IRIS &amp;lt;&amp;lt;'EOF'

/* Install IPM/ZPM client if you still need that first
   (your original snippet did this already) */
s version="latest" s r=##class(%Net.HttpRequest).%New(),r.Server="pm.community.intersystems.com",r.SSLConfiguration="ISC.FeatureTracker.SSL.Config" d r.Get("/packages/zpm/"_version_"/installer"),$system.OBJ.LoadStream(r.HttpResponse.Data,"c")

/* Configure registry */
zpm
repo -r -n registry -url https://pm.community.intersystems.com/ -user "" -pass ""
install csvgenpy
quit

/* Import and Compile the MockPackage */
/* The "ck" flags will Compile and Keep the source */
Do $system.OBJ.Import("/usr/irissys/mgr/MockPackage", "ck")

/* Upload csv data ONCE to Table Automatically using csvgenpy */
SET exists = ##class(%SYSTEM.SQL.Schema).TableExists("MockPackage.NoShowsAppointments")
IF 'exists {   do ##class(shvarov.csvgenpy.csv).Generate("/dur/data/healthcare_noshows_appointments.csv","NoShowsAppointments","MockPackage")   }

halt
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a bash script that is executed inside the container immediately after startup. It opens an IRIS terminal session using &lt;code&gt;iris session IRIS&lt;/code&gt; and runs IRIS-specific commands to perform additional configuration steps automatically.&lt;/p&gt;

&lt;p&gt;These steps include importing custom packages whose classes were copied inside the container's storage, installing IPM (available as &lt;code&gt;zpm&lt;/code&gt; inside the IRIS terminal), installing IPM packages such as &lt;code&gt;csvgenpy&lt;/code&gt;, and using &lt;code&gt;csvgenpy&lt;/code&gt; to load a CSV file mounted into the container at &lt;code&gt;/dur/data/healthcare_noshows_appointments.csv&lt;/code&gt; to create and populate a corresponding table in IRIS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; This script is executed every time the container starts. If this behavior is not considered, it can lead to unintended side effects such as reloading or resetting data. That is why it is important to make the script safe to run multiple times, for example, by checking whether the target table already exists before creating or populating it. This is especially relevant here because the Docker Compose restart policy is set to &lt;code&gt;restart: always&lt;/code&gt;, meaning the container will automatically restart and re-execute these commands whenever it goes down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packages for Benchmarking
&lt;/h2&gt;

&lt;p&gt;This section introduces the ObjectScript packages used to benchmark different data access strategies in IRIS for a Machine Learning inference workload. The focus here is not on model quality, but on measuring and comparing the time it takes to retrieve data from IRIS, convert it into a Pandas DataFrame, and run inference using a trained LightGBM model.&lt;/p&gt;

&lt;p&gt;Each class plays a specific role in this process, from data preparation, to model training, and finally to inference and performance comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  MockDataManager.cls
&lt;/h3&gt;

&lt;p&gt;This class contains methods for taking a given CSV file and duplicating its rows to reach a desired dataset size (&lt;code&gt;AdjustDataSize&lt;/code&gt;), as well as updating a given IRIS table with the specified CSV (&lt;code&gt;UpdateTableFromCSV&lt;/code&gt;). The main purpose of these utilities is to allow testing query and inference time across multiple table sizes in a controlled way.&lt;/p&gt;

&lt;p&gt;Note: Throughout this analysis, we focus exclusively on the &lt;strong&gt;inference time&lt;/strong&gt; of a LightGBM model. We are not concerned with model performance metrics such as F1 score, precision, recall, accuracy, or else at this stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  MockModelManager.cls
&lt;/h3&gt;

&lt;p&gt;In this class, the only relevant method is &lt;code&gt;TrainNoShowsModel&lt;/code&gt;. It leverages the data processing pipeline defined in &lt;code&gt;python_utils.utils&lt;/code&gt; to prepare the raw data, passed in as a Pandas DataFrame, fit a LightGBM model, and persist the trained model to disk.&lt;/p&gt;

&lt;p&gt;The model is saved to a predefined location, which in this setup corresponds to the persistent storage mounted through Docker volumes in &lt;code&gt;docker-compose.yml&lt;/code&gt;. This allows the trained model to be reused across container restarts and inference runs without retraining.&lt;/p&gt;

&lt;h3&gt;
  
  
  MockInference.cls
&lt;/h3&gt;

&lt;p&gt;The core of the performance comparison lives in this class. The process begins by loading the trained LightGBM model weights from the file path specified in the &lt;code&gt;MODELPATH&lt;/code&gt; parameter. While this path is currently hardcoded, it serves as a static reference point shared by all inference tests.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RunInferenceWDynamicSQL&lt;/code&gt; represents the first approach. It relies on an ObjectScript method called &lt;code&gt;DynamicSQL&lt;/code&gt;, which executes a Dynamic SQL statement to filter records by age. The results are packed into a &lt;code&gt;%DynamicArray&lt;/code&gt; of &lt;code&gt;%DynamicObjects&lt;/code&gt;. This method is then called by the &lt;code&gt;dynamic_sql_query&lt;/code&gt; Python function in &lt;code&gt;python_utils/querymethods.py&lt;/code&gt;, where the IRIS objects are converted into a structure that can be easily transformed into a Pandas DataFrame.&lt;/p&gt;

&lt;p&gt;The entire workflow, including execution time measurement via a Python decorator defined in &lt;code&gt;python_utils/utils.py&lt;/code&gt;, is orchestrated inside &lt;code&gt;RunInferenceWDynamicSQL&lt;/code&gt;. The resulting DataFrame is then passed through the inference pipeline to produce predictions and measure end-to-end inference latency.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RunInferenceWIRISSQL&lt;/code&gt; follows a simpler path. It uses the &lt;code&gt;iris_sql_query&lt;/code&gt; method from &lt;code&gt;python_utils/querymethods.py&lt;/code&gt; to execute the SQL query directly from Python. The resulting IRIS SQL iterator is transformed directly into a Pandas DataFrame, after which the same inference and timing logic used in the previous method is applied.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RunInferenceWGLobals&lt;/code&gt; is the most direct approach, as it queries the underlying data structures (globals) backing the table. It uses the &lt;code&gt;iris_global_query&lt;/code&gt; method to fetch data directly from &lt;code&gt;^vCVc.Dvei.1&lt;/code&gt;. This particular global was identified as the &lt;code&gt;DataLocation&lt;/code&gt; in the storage definition of the &lt;code&gt;MockPackage.NoShowsAppointments&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;The global name is a result of the hashed storage automatically generated when the table was built from the CSV file.&lt;/p&gt;

&lt;p&gt;Finally, the integrity of all three approaches is verified using the &lt;code&gt;ConsistencyCheck&lt;/code&gt; method. This utility asserts that the Pandas DataFrames produced by each query strategy are identical, ensuring that data types, values, and numerical precision remain perfectly consistent regardless of the access method used.&lt;/p&gt;

&lt;p&gt;Because this check raises no errors, it confirms that Dynamic SQL, direct SQL access from Python, and high-speed global access are all returning exactly the same dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance comparison
&lt;/h2&gt;

&lt;p&gt;To evaluate performance, we measured query and inference times for increasing table sizes and report in the table below the average time over 10 runs for each configuration. Query time corresponds to retrieving the data from the database, while inference time corresponds to running the LightGBM model on the resulting dataset.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;th&gt;DynamicSQL – Query&lt;/th&gt;
&lt;th&gt;DynamicSQL – Infer&lt;/th&gt;
&lt;th&gt;IRISSQL – Query&lt;/th&gt;
&lt;th&gt;IRISSQL – Infer&lt;/th&gt;
&lt;th&gt;Globals – Query&lt;/th&gt;
&lt;th&gt;Globals – Infer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0.003219271&lt;/td&gt;
&lt;td&gt;0.042354488&lt;/td&gt;
&lt;td&gt;0.001749706&lt;/td&gt;
&lt;td&gt;0.043090796&lt;/td&gt;
&lt;td&gt;0.001184559&lt;/td&gt;
&lt;td&gt;0.043616056&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;0.031865168&lt;/td&gt;
&lt;td&gt;0.052698898&lt;/td&gt;
&lt;td&gt;0.019246697&lt;/td&gt;
&lt;td&gt;0.056159472&lt;/td&gt;
&lt;td&gt;0.005061340&lt;/td&gt;
&lt;td&gt;0.045210719&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;0.237553477&lt;/td&gt;
&lt;td&gt;0.082497978&lt;/td&gt;
&lt;td&gt;0.099582171&lt;/td&gt;
&lt;td&gt;0.068728352&lt;/td&gt;
&lt;td&gt;0.036206818&lt;/td&gt;
&lt;td&gt;0.061128354&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;5.279174852&lt;/td&gt;
&lt;td&gt;0.189197206&lt;/td&gt;
&lt;td&gt;1.122253346&lt;/td&gt;
&lt;td&gt;0.177564192&lt;/td&gt;
&lt;td&gt;0.535172153&lt;/td&gt;
&lt;td&gt;0.175085044&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;td&gt;68.741133046&lt;/td&gt;
&lt;td&gt;0.639807224&lt;/td&gt;
&lt;td&gt;7.015313649&lt;/td&gt;
&lt;td&gt;0.610818386&lt;/td&gt;
&lt;td&gt;2.743980526&lt;/td&gt;
&lt;td&gt;0.587647438&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;196.871173100&lt;/td&gt;
&lt;td&gt;1.145034313&lt;/td&gt;
&lt;td&gt;22.138613220&lt;/td&gt;
&lt;td&gt;1.136569023&lt;/td&gt;
&lt;td&gt;5.987578392&lt;/td&gt;
&lt;td&gt;1.106307745&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2,000,000&lt;/td&gt;
&lt;td&gt;711.319680452&lt;/td&gt;
&lt;td&gt;3.021180152&lt;/td&gt;
&lt;td&gt;60.142974615&lt;/td&gt;
&lt;td&gt;2.879153728&lt;/td&gt;
&lt;td&gt;11.92040014&lt;/td&gt;
&lt;td&gt;2.728573560&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To characterise how query and inference times scale with respect to table size, we fitted a power-law regression of the form:&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.amazonaws.com%2Fuploads%2Farticles%2Fenephm08qhsw3ec4zzkb.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.amazonaws.com%2Fuploads%2Farticles%2Fenephm08qhsw3ec4zzkb.png" alt=" " width="388" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Inference Time
&lt;/h3&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.amazonaws.com%2Fuploads%2Farticles%2Ffcvw6y0dacebvenak2m2.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.amazonaws.com%2Fuploads%2Farticles%2Ffcvw6y0dacebvenak2m2.png" alt=" " width="625" height="320"&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.amazonaws.com%2Fuploads%2Farticles%2Fzu1ioe6qjno3i0950a0z.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.amazonaws.com%2Fuploads%2Farticles%2Fzu1ioe6qjno3i0950a0z.png" alt=" " width="431" height="131"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Inference time is very similar across all three query methods, which is expected, as the resulting input DataFrame was verified to be identical in all cases.&lt;/p&gt;

&lt;p&gt;From the measurements, the model is able to perform inference on approximately 1 million rows in about 1 second, highlighting the high throughput of LightGBM.&lt;/p&gt;

&lt;p&gt;The fitted exponent (k ~ 1.3) indicates slightly superlinear scaling of total inference time with respect to the number of rows. This behaviour is commonly observed in large-scale batch processing and is likely attributable to system-level effects such as cache pressure or memory bandwidth saturation, rather than to the algorithmic complexity of the model itself.&lt;/p&gt;

&lt;p&gt;The scaling factor "a" is on the order of tens of nanoseconds, reflecting the efficiency of the per-row computation. While the superlinear exponent implies that the marginal cost per additional row increases with table size, this effect becomes noticeable only at large scales (millions of rows), as illustrated by the increasing slope in the log–log plot.&lt;/p&gt;

&lt;p&gt;The marginal inference cost can be estimated from the derivative of the fitted model:&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.amazonaws.com%2Fuploads%2Farticles%2F1wnvyih0ql36502xmb8g.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.amazonaws.com%2Fuploads%2Farticles%2F1wnvyih0ql36502xmb8g.png" alt=" " width="138" height="57"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Evaluating this expression shows that the per-row marginal inference time increases from approximately 1.9e-7 seconds at 1000 rows to 1.5 e-6 seconds at 1 million rows, remaining firmly in the microsecond range within the observed data regime.&lt;/p&gt;

&lt;p&gt;Finally, the fitted constant offset (c ~ 0.08) seconds likely represents a fixed inference overhead, such as model invocation and runtime initialisation, and should be interpreted as a constant cost independent of table size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query Time
&lt;/h3&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.amazonaws.com%2Fuploads%2Farticles%2Fqjj1v93hxrjouzb4ij4r.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.amazonaws.com%2Fuploads%2Farticles%2Fqjj1v93hxrjouzb4ij4r.png" alt=" " width="357" height="462"&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.amazonaws.com%2Fuploads%2Farticles%2Fpj4on4da3gfoa6e10tpg.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.amazonaws.com%2Fuploads%2Farticles%2Fpj4on4da3gfoa6e10tpg.png" alt=" " width="447" height="85"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Query time exhibits substantially different scaling behavior across the three access methods. In contrast to inference time, which is largely independent of the query mechanism, query performance is dominated by the data access strategy and its interaction with storage and execution layers.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Globals-based&lt;/strong&gt; approach shows nearly linear scaling (k ~ 1.03), indicating that the cost of retrieving each additional row remains approximately constant across the measured range. This behavior is consistent with sequential access patterns and minimal query-planning overhead, making Globals the most scalable option for large result sets.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;IRISSQL&lt;/strong&gt; approach exhibits moderately superlinear scaling (k ~ 1.48). While still efficient for moderate table sizes, the increasing marginal cost suggests growing overhead from SQL execution, query planning, or intermediate result materialization as the number of rows increases.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;DynamicSQL&lt;/strong&gt; approach displays the most pronounced superlinear scaling (k ~ 1.82), resulting in rapidly increasing query times at larger scales. This behavior explains the steep slope observed in the plot and indicates that DynamicSQL incurs significant additional overhead as result size grows, making it the least scalable method for large batch queries.&lt;/p&gt;

&lt;p&gt;Although the fitted scaling factors "a" are numerically small, they must be interpreted jointly with the exponent "k". In practice, the exponent dominates the asymptotic behavior, which is why DynamicSQL, despite a small "a", becomes significantly slower at large table sizes.&lt;/p&gt;

&lt;p&gt;The fitted constant term "c" represents the fixed query overhead. For IRISSQL, "c" is close to zero, indicating a small startup cost. This overhead is even smaller for the Globals-based approach, where the fitted value is slightly negative, effectively suggesting a zero fixed cost. This behavior is expected, as data retrieval via a global key proceeds directly without additional query planning or execution overhead.&lt;/p&gt;

&lt;p&gt;In contrast, the relatively large constant offset observed for DynamicSQL indicates a substantial fixed overhead, likely associated with query preparation or execution setup. This fixed cost penalizes performance across all table sizes and becomes particularly impactful at both small and large scales.&lt;/p&gt;

&lt;p&gt;Overall, these results highlight that query time, unlike inference time, is highly sensitive to the data access method, with Globals offering near-linear scalability, IRISSQL providing a balanced middle ground, and DynamicSQL exhibiting poor scalability for large result sets.&lt;/p&gt;

&lt;p&gt;Please refer to the following repository for more details:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/JorgeIvanJH/IRIS_dockerization.git" rel="noopener noreferrer"&gt;https://github.com/JorgeIvanJH/IRIS_dockerization.git&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Video demo here:&lt;/p&gt;

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

&lt;p&gt;If you have any questions or notice any mistakes, please don’t hesitate to reach out.&lt;/p&gt;

&lt;p&gt;Thank you!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>sql</category>
      <category>python</category>
      <category>performance</category>
    </item>
    <item>
      <title>Vector Search with Embedded Python in InterSystems IRIS</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Thu, 30 Apr 2026 15:35:55 +0000</pubDate>
      <link>https://dev.to/intersystems/vector-search-with-embedded-python-in-intersystems-iris-h3a</link>
      <guid>https://dev.to/intersystems/vector-search-with-embedded-python-in-intersystems-iris-h3a</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;span&gt;One objective of vectorization is to render unstructured text more machine-usable. Vector embeddings accomplish this by encoding the semantics of text as high-dimensional numeric vectors, which can be employed by advanced search algorithms (normally an approximate nearest neighbor algorithm like Hierarchical Navigable Small World). This not only improves our ability to interact with unstructured text programmatically but makes it searchable by context and by meaning beyond what is captured literally by keyword.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;In this article I will walk through a simple vector search implementation that Kwabena Ayim-Aboagye and I fleshed out using embedded python in InterSystems IRIS for Health. I'll also dive a bit into how to use embedded python and dynamic SQL generally, and how to take advantage of vector search features offered natively through IRIS.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;span&gt; &lt;/span&gt;&lt;h2&gt;Environment Details:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;OS: Windows Server 2025&lt;/li&gt;
&lt;li&gt;InterSystems IRIS for Health 2025.1&lt;/li&gt;
&lt;li&gt;VS Code / InterSystems Server Manager&lt;/li&gt;
&lt;li&gt;Python 3.13.7&lt;/li&gt;
&lt;li&gt;Python Libraries: pandas, ollama, iris*&lt;em&gt;&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Ollama 0.12.3 and model all-minilm&lt;/li&gt;
&lt;li&gt;Dynamic SQL&lt;/li&gt;
&lt;li&gt;Sample database of unstructured text (classic poems)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Process:&lt;/h2&gt;
&lt;h3&gt;      0. &lt;strong&gt;Setup the environment; complete installs&lt;/strong&gt;
&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;h3&gt;&lt;strong&gt;Define an auxiliary table&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;The embeddings table &lt;code&gt;User.SamplePoetryVectors&lt;/code&gt; has a foreign key on &lt;code&gt;User.SamplePoetry&lt;/code&gt; as well as an &lt;code&gt;EMBEDDING&lt;/code&gt; property of type &lt;code&gt;%Library.Vector&lt;/code&gt;. Ollama &lt;code&gt;all-minilm&lt;/code&gt; generates embeddings of 384 dimensions, so we imposed a length constraint accordingly.&lt;ul&gt;
&lt;li&gt;&lt;img src="/sites/default/files/inline/images/table_dfns_0.png" alt=""&gt;&lt;/li&gt;
&lt;li&gt;*Note that because the goal is to ultimately take advantage of &lt;a href="https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic.cls?LIBRARY=%25SYS&amp;amp;CLASSNAME=%25SQL.Index.HNSW" rel="noopener noreferrer"&gt;IRIS' native HNSWIndex&lt;/a&gt; and &lt;a href="https://docs.intersystems.com/iris20253/csp/docbook/Doc.View.cls?KEY=RSQL_vectorcosine" rel="noopener noreferrer"&gt;IRIS' native vector search methods&lt;/a&gt;, &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQL_vecsearch#GSQL_vecsearch_index_hnsw" rel="noopener noreferrer"&gt;we must have a column of type %Library.Vector (or %Library.Embedding) of fixed length that is of type decimal or double&lt;/a&gt; upon which to index.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h3&gt;
&lt;strong&gt;Define a &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;RegisteredObject&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt; class&lt;/strong&gt; for our vectorization methods, which will be written in embedded python. First let's focus on a &lt;code&gt;VectorizeTable()&lt;/code&gt; method, which will contain a driver function (of the same name) and a few supporting process functions all written in Python.&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The driver function walks through the process as follows:&lt;ol&gt;
&lt;li&gt;Load from IRIS into a Pandas Dataframe (via supporting function &lt;code&gt;load_table()&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Generate an embedding column (via supporting class method &lt;code&gt;GetEmbeddingString&lt;/code&gt;, which will later be used to generate embeddings for queries as well)&lt;ul&gt;&lt;li&gt;Convert the embedding column to a string that's compatible with IRIS vector type&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Write the dataframe into the auxiliary able&lt;/li&gt;
&lt;li&gt;Create an HNSW index on the auxiliary table&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;VectorizeTable()&lt;/code&gt; class method then simply calls the driver function:&lt;ul&gt;&lt;li&gt;&lt;img src="/sites/default/files/inline/images/vectorizetable.png" alt=""&gt;&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Let's examine it step-by-step:&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;h4&gt;&lt;strong&gt;Load the table from IRIS into a Pandas Dataframe&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;def&lt;/span&gt; &lt;span class="mention"&gt;load_table&lt;/span&gt;&lt;span class="mention"&gt;(sample_size=&lt;/span&gt;&lt;span class="mention"&gt;''&lt;/span&gt;) -&amp;gt; pd.DataFrame:&lt;br&gt;
    sql = &lt;span class="mention"&gt;f"SELECT * FROM SQLUser.SamplePoetry&lt;/span&gt;&lt;span class="mention"&gt;{&lt;/span&gt;&lt;span class="mention"&gt;f' LIMIT &lt;/span&gt;&lt;span class="mention"&gt;{sample_size}&lt;/span&gt;' &lt;span class="mention"&gt;if&lt;/span&gt; sample_size != &lt;span class="mention"&gt;'*'&lt;/span&gt; &lt;span class="mention"&gt;else&lt;/span&gt; &lt;span class="mention"&gt;''&lt;/span&gt;}"&lt;br&gt;
    result_set = iris.sql.exec(sql)&lt;br&gt;
    df = result_set.dataframe()
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;span class="mention"&amp;gt;# Entries without text will not be vectorized nor searchable&amp;lt;/span&amp;gt;
&amp;lt;span class="mention"&amp;gt;for&amp;lt;/span&amp;gt; index, row &amp;lt;span class="mention"&amp;gt;in&amp;lt;/span&amp;gt; df.iterrows():
    &amp;lt;span class="mention"&amp;gt;if&amp;lt;/span&amp;gt; row[&amp;lt;span class="mention"&amp;gt;'poem'&amp;lt;/span&amp;gt;] == &amp;lt;span class="mention"&amp;gt;' '&amp;lt;/span&amp;gt; &amp;lt;span class="mention"&amp;gt;or&amp;lt;/span&amp;gt; row[&amp;lt;span class="mention"&amp;gt;'poem'&amp;lt;/span&amp;gt;] &amp;lt;span class="mention"&amp;gt;is&amp;lt;/span&amp;gt; &amp;lt;span class="mention"&amp;gt;None&amp;lt;/span&amp;gt;:
        df = df.drop(index)

&amp;lt;span class="mention"&amp;gt;return&amp;lt;/span&amp;gt; df&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li data-list-item-id="eefcc1d39f96038e122e461e7526ba90b"&amp;gt;This function leverages the &amp;lt;code&amp;gt;dataframe()&amp;lt;/code&amp;gt; method of &amp;lt;a href="https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic.cls?LIBRARY=%25SYS&amp;amp;amp;CLASSNAME=%25SYS.Python.SQLResultSet#METHOD_dataframe" target="_blank"&amp;gt;the embedded python SQLResultSet objects&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li data-list-item-id="ea4ede0aa2a09dabf2902a06278aa79df"&amp;gt;&amp;lt;code&amp;gt;load_table()&amp;lt;/code&amp;gt; accepts an optional &amp;lt;code&amp;gt;sample_size&amp;lt;/code&amp;gt; argument for testing purposes. There's also a filter for entries without unstructured text. Though our sample database is curated and complete, some use cases may seek to vectorize datasets for which one cannot assume each row will have data for all columns (for example survey responses with skipped questions). As opposed to implementing a "null" or empty vector, we chose to exclude such rows from vector search by removing them at this step in the process.&amp;lt;/li&amp;gt;&amp;lt;li data-list-item-id="e821e2611fe70c080e7a022d8f3c21f1b"&amp;gt;*Note that &amp;lt;code&amp;gt;iris&amp;lt;/code&amp;gt; is the &amp;lt;a href="https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GEPYTHON_reference" target="_blank"&amp;gt;InterSystems IRIS Python Module&amp;lt;/a&amp;gt;. It functions as an API to access IRIS classes, methods, and to interact with the database, etc.&amp;lt;/li&amp;gt;&amp;lt;li data-list-item-id="ea69a4d07e45b85e56b943c780424979e"&amp;gt;*Note that &amp;lt;code&amp;gt;SQLUser&amp;lt;/code&amp;gt; is the &amp;lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQL_tables#GSQL_tables_schemadefault" target="_blank"&amp;gt;system-wide default schema&amp;lt;/a&amp;gt; which &amp;lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GOBJ_defpersobj#GOBJ_defpersobj_sqlproj_pkg" target="_blank"&amp;gt;corresponds to the default package&amp;lt;/a&amp;gt;&amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt;.&amp;lt;/li&amp;gt;&amp;lt;/ul&amp;gt;&amp;lt;/li&amp;gt;&amp;lt;li class="ck-list-marker-bold" data-list-item-id="e9ca354b9ae137911dc78afd93da5b132"&amp;gt;&amp;lt;h4&amp;gt;&amp;lt;strong&amp;gt;Generate an embedding column (support method)&amp;lt;/strong&amp;gt;&amp;lt;/h4&amp;gt;&amp;lt;ul&amp;gt;&amp;lt;li data-list-item-id="ea097b9c25eb08247c89cab9fcfac9e6d"&amp;gt;&amp;lt;pre class="codeblock-container" idlang="0" lang="ObjectScript" tabsize="4"&amp;gt;&amp;lt;code class="language-plaintext language-cls hljs cos"&amp;gt;&amp;lt;span class="mention"&amp;gt;ClassMethod&amp;lt;/span&amp;gt; GetEmbeddingString(aurg &amp;lt;span class="mention"&amp;gt;As&amp;lt;/span&amp;gt; &amp;lt;span class="mention"&amp;gt;%String&amp;lt;/span&amp;gt;) &amp;lt;span class="mention"&amp;gt;As&amp;lt;/span&amp;gt; &amp;lt;span class="mention"&amp;gt;%String&amp;lt;/span&amp;gt; [ Language = python ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;{&lt;br&gt;
  import iris&lt;br&gt;
  import ollama&lt;/p&gt;

&lt;p&gt;response = ollama.embed(model='all-minilm',input=[ aurg ])&lt;br&gt;
  embedding_str = str(response.embeddings[&lt;span&gt;0&lt;/span&gt;])&lt;/p&gt;

&lt;p&gt;&lt;span&gt;return&lt;/span&gt; embedding_str&lt;br&gt;
}&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;We installed Ollama on our VM, loaded the &lt;code&gt;all-minilm&lt;/code&gt; embedding model, and generated embeddings using Ollama’s Python library. This allowed us to run the model locally and generate embeddings without an API key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GetEmbeddingString&lt;/code&gt; returns the embedding as a string because &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_tovector#RSQL_tovector_args_data" rel="noopener noreferrer"&gt;&lt;code&gt;TO_VECTOR&lt;/code&gt;&lt;/a&gt; by default expects the &lt;code&gt;data&lt;/code&gt; argument to be a string, more on that to follow.&lt;/li&gt;
&lt;li&gt;*Note that Embedded Python provides syntax for calling other ObjectScript methods defined within the current class (similar to &lt;code&gt;self&lt;/code&gt; in Python). The earlier example uses &lt;code&gt;iris.cls(&lt;strong&gt;name&lt;/strong&gt;)&lt;/code&gt; syntax to get a reference to the current ObjectScript class and invoke &lt;code&gt;GetEmbeddingString&lt;/code&gt; (ObjectScript method) from &lt;code&gt;VectorizeTable&lt;/code&gt; (Embedded Python method inside ObjectScript method).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h4&gt;&lt;strong&gt;Write the embeddings from the dataframe into the table in IRIS&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;# Write dataframe into new table&lt;/span&gt;&lt;br&gt;
print(&lt;span class="mention"&gt;"Loading data into table..."&lt;/span&gt;)&lt;br&gt;
&lt;span class="mention"&gt;for&lt;/span&gt; index, row &lt;span class="mention"&gt;in&lt;/span&gt; df.iterrows():&lt;br&gt;
    sql = iris.sql.prepare(&lt;span class="mention"&gt;"INSERT INTO SQLUser.SamplePoetryVectors (ID, EMBEDDING) VALUES (?, TO_VECTOR(?, decimal))"&lt;/span&gt;)&lt;br&gt;
    rs = sql.execute(row[&lt;span class="mention"&gt;'id'&lt;/span&gt;], row[&lt;span class="mention"&gt;'embedding'&lt;/span&gt;])

&lt;p&gt;print(&lt;span&gt;"Data loaded into table."&lt;/span&gt;)&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;Here, we use Dynamic SQL to populate &lt;code&gt;SamplePoetryVectors&lt;/code&gt; row-by-row. Because earlier we declared the &lt;code&gt;EMBEDDING&lt;/code&gt; property to be of type &lt;code&gt;%Library.Vector&lt;/code&gt; we must use &lt;a href="http://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_tovector#RSQL_tovector_args_data" rel="noopener noreferrer"&gt;&lt;code&gt;TO_VECTOR&lt;/code&gt;&lt;/a&gt; to convert the embeddings to IRIS' native &lt;a href="https://docs.intersystems.com/iris20253/csp/documatic/%25CSP.Documatic.cls?LIBRARY=%25SYS&amp;amp;PRIVATE=1&amp;amp;CLASSNAME=%25Library.Vector" rel="noopener noreferrer"&gt;&lt;code&gt;VECTOR&lt;/code&gt;&lt;/a&gt; datatype upon insertion. We ensured compatibility with &lt;code&gt;TO_VECTOR&lt;/code&gt; by converting the embeddings to strings earlier.&lt;ul&gt;&lt;li&gt;The &lt;code&gt;iris&lt;/code&gt; python module again allows us to take advantage of Dynamic SQL from within our Embedded Python function.&lt;/li&gt;&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h4&gt;&lt;strong&gt;Create a HNSW Index&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;# Create Index&lt;/span&gt;&lt;br&gt;
iris.sql.exec(&lt;span class="mention"&gt;"CREATE INDEX HNSWIndex ON TABLE SQLUser.SamplePoetryVectors (EMBEDDING) AS HNSW(Distance='Cosine')"&lt;/span&gt;)&lt;br&gt;
print(&lt;span class="mention"&gt;"Index created."&lt;/span&gt;)&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;IRIS will natively implement a &lt;a href="https://arxiv.org/abs/1603.09320" rel="noopener noreferrer"&gt;HNSW graph&lt;/a&gt; for use in vector search methods when an &lt;a href="https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic.cls?LIBRARY=%25SYS&amp;amp;CLASSNAME=%25SQL.Index.HNSW" rel="noopener noreferrer"&gt;HNSW index&lt;/a&gt; is created on a compatible column. The vector search methods available through IRIS are &lt;code&gt;VECTOR_DOT_PRODUCT&lt;/code&gt; and &lt;code&gt;VECTOR_COSINE&lt;/code&gt;. Once the index is created, IRIS will automatically use it to optimize the corresponding vector search method when called in subsequent queries. The parameter defaults for an HNSW index are &lt;code&gt;Distance = Cosine&lt;/code&gt;, &lt;code&gt;M = 16&lt;/code&gt;, and &lt;code&gt;efConstruction = 200&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Note that &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_vectorcosine#RSQL_vectorcosine_desc" rel="noopener noreferrer"&gt;&lt;code&gt;VECTOR_COSINE&lt;/code&gt;&lt;/a&gt; implicitly normalizes its input vectors, so we did not need to perform normalization before inserting them into the table in order for our vector search queries to be scored correctly!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h3&gt;
&lt;strong&gt;Implement a &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;VectorSearch()&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt; class method&lt;/strong&gt;
&lt;/h3&gt;
&lt;ul&gt;&lt;li&gt;&lt;h4&gt; &lt;img src="/sites/default/files/inline/images/vectorsearch_0.png" alt=""&gt; &lt;span&gt; &lt;/span&gt;
&lt;/h4&gt;&lt;/li&gt;&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;h4&gt;Generate an embedding for the query string&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;# Generate embedding of search parameter&lt;/span&gt;&lt;br&gt;
search_vector = iris.cls(&lt;strong&gt;name&lt;/strong&gt;).GetEmbeddingString(aurg)&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;Reusing the class method &lt;code&gt;GetEmbeddingString&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h4&gt;Prepare and execute a query that utilizes &lt;code&gt;VECTOR_COSINE&lt;/code&gt;
&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;# Prepare and execute SQL statement&lt;/span&gt;&lt;br&gt;
stmt = iris.sql.prepare(&lt;br&gt;
        """SELECT top 5 p.poem, p.title, p.author &lt;br&gt;
        FROM SQLUser.SamplePoetry AS p &lt;br&gt;
        JOIN SQLUser.SamplePoetryVectors AS v &lt;br&gt;
        ON p.ID = v.ID &lt;br&gt;
        ORDER BY VECTOR_COSINE(v.embedding, TO_VECTOR(?)) DESC"""&lt;br&gt;
)&lt;br&gt;
results = stmt.execute(search_vector)&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;We use a &lt;code&gt;JOIN&lt;/code&gt; here to combine the poetry text with its corresponding vector embedding so we can rank results by semantic similarity.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;h4&gt;Output the results&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;pre&gt;&lt;code&gt;results_df = pd.DataFrame(results)

&lt;p&gt;pd.set_option(&lt;span&gt;'display.max_colwidth'&lt;/span&gt;, &lt;span&gt;25&lt;/span&gt;)&lt;br&gt;
results_df.rename(columns={&lt;span&gt;0&lt;/span&gt;: &lt;span&gt;'Poem'&lt;/span&gt;, &lt;span&gt;1&lt;/span&gt;: &lt;span&gt;'Title'&lt;/span&gt;, &lt;span&gt;2&lt;/span&gt;: &lt;span&gt;'Author'&lt;/span&gt;}, inplace=&lt;span&gt;True&lt;/span&gt;)&lt;/p&gt;


&lt;p&gt;print(results_df)&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;

&lt;li&gt;Utilizes formatting options from pandas to tweak how it appears in the IRIS Terminal:&lt;ul&gt;&lt;li&gt;

&lt;img src="/sites/default/files/inline/images/terminal_example.png" alt=""&gt; &lt;span&gt; &lt;/span&gt;
&lt;/li&gt;&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ol&gt;

&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>vectordatabase</category>
      <category>database</category>
      <category>tutorial</category>
      <category>ux</category>
    </item>
    <item>
      <title>KMS . Introduction to its use in IRIS and an example of setup on AWS EC2 system</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 26 Apr 2026 16:19:04 +0000</pubDate>
      <link>https://dev.to/intersystems/kms-introduction-to-its-use-in-iris-and-an-example-of-setup-on-aws-ec2-system-425e</link>
      <guid>https://dev.to/intersystems/kms-introduction-to-its-use-in-iris-and-an-example-of-setup-on-aws-ec2-system-425e</guid>
      <description>&lt;p&gt;IRIS can use a KMS (Key Managment Service) as of release 2023.3.  Intersystems documentation is a good resource on KMS implementation but does not go into details of the KMS set up on the system, nor provide an easily followable example of how one might set this up for basic testing.&lt;/p&gt;

&lt;p&gt;The purpose of this article is to supplement the docs with a brief explanation of KMS, an example of its use in IRIS, and notes for setup of a testing system on AWS EC2 RedHat Linux system using the AWS KMS.  It is assumed in this document that the reader/implementor already has access/knowledge to set up an AWS EC2 Linux system running IRIS (2023.3 or later), and that they have proper authority to access the AWS KMS and AWS IAM (for creating roles and polices), or that they will be able to get this access either on their own or via their organizations Security contact in charge of their AWS access.&lt;br&gt;&lt;/p&gt;

&lt;p&gt;What is KMS and what does it do for IRIS?:&lt;/p&gt;

&lt;p&gt;KMS means Key Management Service.   Briefly, it provides an external secure method of encrypting and decrypting IRIS encryption keys through a trusted service, the KMS.&lt;/p&gt;

&lt;p&gt;In prior implementation, when using unattended startup, IRIS would never store unencrypted encryption keys; IRIS would encrypt a key with an encrypted copy of the key encryption key in that key itself.  It would then store a user ID and password in IRIS to unencrypt the encrypted key encryption key.  This leaves an unencrypted copy of the user ID and password stored in an IRIS database, which leaves extra burden on IRIS managers of securing that.  &lt;span&gt;&lt;span&gt;The key encryption key is encrypted/decrypted by a symmetric key that is based on a key admin’s password using PBKDF2 (Password-Based Key Derivation Function 2). So the key that encrypts the key encryption key is never stored anywhere – it’s derived on the fly when a key admin supplies their password. Since there can be multiple admins for keys in a given key file we store in the key file one encrypted copy of the key encryption key (per admin) and then a single encrypted copy of each database/data element encryption key (encrypted with the key encryption key).&lt;/span&gt;&lt;/span&gt;&lt;br&gt; &lt;/p&gt;

&lt;p&gt;With KMS we do not store the id and password in IRIS.  When we create the encryption key with KMS we get an encrypted encryption key, and the KMS keeps the key encryption key for us. We reach out to the kms server with the encrypted encryption key.  the kms server decrypts the encryption key.  The decrypted key is sent back to us and stored in memory.  The communications are secured using TLS.&lt;/p&gt;

&lt;p&gt;We don't ever have access to the raw key encryption key.  We use it as a service via kms.  The key encryption key stays on the kms server.  This helps with key management and key security.&lt;/p&gt;

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

&lt;p&gt;Current implementation (as of 1/22/2024) of KMS is Cloud Vendor Specific&lt;/p&gt;

&lt;p&gt;In AWS you must specify creation of a symmetric key. &lt;/p&gt;

&lt;p&gt;In Azure you must specify creation of an RSA key&lt;/p&gt;

&lt;p&gt;Future implementation my include google KMS.&lt;/p&gt;

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

&lt;p&gt;---&lt;/p&gt;

&lt;p&gt;Example of workflow setting up new encryption key in IRIS using KMS:&lt;/p&gt;

&lt;p&gt;The following assumes you have set up an IRIS system to access an AWS KMS server and your instance has been authorized to access the keys there and you have set up a key for use.  (See Setup Notes following this example for an example of setting up KMS on AWS to connect with an AWS EC2 RedHat Linux instance.)&lt;/p&gt;

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

&lt;p&gt;1.%SYS&amp;gt;D ^EncryptionKey&lt;/p&gt;

&lt;p&gt;2.Create New Key&lt;/p&gt;

&lt;p&gt;3.Name the key&lt;/p&gt;

&lt;p&gt;4.Use KMS: yes&lt;/p&gt;

&lt;p&gt;      Here you specify properties of the key.  Choose backup if you want a regular encryption key made to backup this KMS key.  This is the only place you can do this.  Treat this backup as you would a normal Encryption key. &lt;/p&gt;

&lt;p&gt;5. Select AWS for the kms server&lt;/p&gt;

&lt;p&gt;6. Get the key ID and the region from your AWS Key Managed Service console&lt;/p&gt;

&lt;p&gt;7. Env Key ; you should not need to specify anything here if your system is set up correctly (per this article). See AWS docs for further details if necessary for your needs.  Leave blank for the purpose of simplifying this for testing example.&lt;/p&gt;

&lt;p&gt;8. You should receive a message like:&lt;/p&gt;

&lt;p&gt;Encryption key file created: iriskmstest1&lt;br&gt;Encryption key created via KMS: 87A85627-9F8C-11EE-8839-0608ECAD1BAF&lt;/p&gt;

&lt;p&gt;This key is NOT activated.&lt;/p&gt;

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

&lt;p&gt;Key Activation and use are then usual encryption key setup steps.&lt;/p&gt;

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

&lt;p&gt;If there are issues with the activation at startup it will error and go into interactive mode&lt;/p&gt;

&lt;p&gt;For interactive startup if you pass in a kms key it will not prompt for username or password&lt;/p&gt;

&lt;p&gt;If you put in the backup key (generated in step 14 above) then it will ask for the username and password you created at key creation time (just like normal key)&lt;/p&gt;

&lt;p&gt;If there are issues you will see errors in your startup, or logged in messages.log if silent startup.&lt;/p&gt;

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

&lt;p&gt;In general, your IRIS system does not need to be on AWS or other cloud system, it accesses the KMS for the key over TLS.&lt;/p&gt;

&lt;p&gt;IRIS uses credentials of current user when accessing the KMS server, so you need to make sure that user has access to KMS&lt;/p&gt;

&lt;p&gt;the AWS key policy defines who can use the key on AWS.  See following setup notes for an example.&lt;/p&gt;

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

&lt;p&gt;----&lt;/p&gt;

&lt;p&gt;Setup Notes: Getting an AWS EC2 Linux system running IRIS to work with an AWS KMS:&lt;/p&gt;

&lt;p&gt;(The following assumes you already have an AWS EC2 RedHat Linux system running an IRIS version that supports KMS)&lt;/p&gt;

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

&lt;p&gt;To set up the AWS EC2 system to use the AWS KMS server:&lt;/p&gt;

&lt;p&gt;Follow Setup instructions in following link to install the AWS CLI on your EC2 system:&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;  Install or update the latest version of the AWS CLI - AWS Command Line Interface (amazon.com)&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;There are instructions for different OS types.  For the purpose of this instruction set I used an AWS RedHat Linux system.  It was fairly strait forward to follow that doc to install the AWS CLI on the system.&lt;/p&gt;

&lt;p&gt;I also had to use 'sudo yum install unzip' to install unzip on the system in order to follow the instructions which had me use unzip on the AWS client download zip file.&lt;/p&gt;

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

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

&lt;p&gt;Here are the steps to create a key that could be used by an IRIS instance for encryption key encryption:&lt;/p&gt;

&lt;p&gt;1. In AWS Mgmnt Console go to Key Management Service.&lt;/p&gt;

&lt;p&gt;2. Click on Customer Managed Keys&lt;/p&gt;

&lt;p&gt;3. Click on Create Key&lt;/p&gt;

&lt;p&gt;5. Accept the Defaults&lt;/p&gt;

&lt;p&gt;6. Enter an Alias; this is the name for the key&lt;/p&gt;

&lt;p&gt;7.Key Admin Options: default policy&lt;/p&gt;

&lt;p&gt;8. Click Finish&lt;/p&gt;

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

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

&lt;p&gt;The IRIS instance will also need to be authorization to use the KMS key. This is done either by running the instance as a user who has authenticated to AWS and is authorized to use the key, specifying a credentials file with the AWS_SHARED_CREDENTIALS_FILE environment variable or by assigning to the EC2 itself an IAM role that either has a policy attached to it that allows key usage or that has an explicit allowance specified in the key policy itself.&lt;/p&gt;

&lt;p&gt;For the purpose of this instruction set we are following the 3rd as ISC Development has suggested this would be the most commonly used by customers in AWS.  In the following we will create an IAM role that can be assigned to the EC2 instance itself. The role can have a policy attached to it that gives it very targeted privileges to access a given key in the KMS (or even just allow specific operations with the key).  We are only exploring the most simple process to give us something to use for testing...&lt;/p&gt;

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

&lt;p&gt;Here are the steps for Authorizing an Instance of IRIS on an AWS EC2 system to use the key on the KMS server:&lt;/p&gt;

&lt;p&gt;1.In AWS Managment Console go to Key Management Service&lt;/p&gt;

&lt;p&gt;2. Under "Customer managed keys" click on the Key ID of the key you want to use.&lt;/p&gt;

&lt;p&gt;3. In the "General configuration" section click the "Copy" icon next to the ARN to copy the ARN to the clipboard. Paste this value somewhere to use later in the policy configuration.&lt;/p&gt;

&lt;p&gt;4. In AWS Mgmnt Console go to IAM.&lt;br&gt;5. Under "Access Management"&amp;gt;"Policies" click "Create policy".&lt;br&gt;6. Under "Select a service" choose KMS from the drop-down list. Click "Next".&lt;br&gt;7. Under "Actions allowed" click on the "Write" access level expander. Check the "Decrypt" and "Encrypt" checkboxes.&lt;br&gt;8. Under "Resources" click on the "Add ARNs" link.&lt;br&gt;9. Paste the entire ARN from Step 3 above into the "Resource ARN" text field. Click "Add ARNs". Click "Next".&lt;br&gt;10. Under "Policy details" provide a policy name and, if desired, a policy description. Click "Create policy".&lt;/p&gt;

&lt;p&gt;11. In IAM under "Access Management"&amp;gt;"Roles" click "Create role".&lt;br&gt;12. Under "Trusted entity type" click "AWS service". Under "Use case" select EC2 from the drop-down list. Click "Next".&lt;br&gt;13. Under "Permissions policies" start typing the policy name from Step 10 until it appears in the list. Click the checkbox next to it. Click "Next".&lt;br&gt;14. Under "Role details" provide a role name. Click "Create role".&lt;/p&gt;

&lt;p&gt;15. In AWS Mgmnt Console go to EC2. Navigate to "Instances"&amp;gt;"Instances".&lt;br&gt;16. If EC2 instance already exists:&lt;br&gt;    a. Click checkbox next to instance name.&lt;br&gt;    b. Click "Actions"&amp;gt;"Security"&amp;gt;"Modify IAM role".&lt;br&gt;    c. Choose the role from Step 15 from the drop-down list.&lt;br&gt;    d. Click "Update IAM role".&lt;br&gt;16. If launching new EC2 instance:&lt;br&gt;    a. Click "Launch instances".&lt;br&gt;    b. Under "Advanced details" choose role from Step 15 in "IAM instance profile" drop-down list.&lt;/p&gt;

&lt;p&gt;17.You can now use the kms key in ^EncryptionKey&lt;/p&gt;

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

&lt;p&gt;Notes:&lt;br&gt; After creating policy/role you might need to refresh the Mgmt Console for these new resources to show up.&lt;/p&gt;

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

&lt;p&gt;---&lt;/p&gt;

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

&lt;p&gt;Supplemental:&lt;/p&gt;

&lt;p&gt;Classes methods of interest:&lt;/p&gt;

&lt;p&gt;%SYSTEM.Encryption.KMSCreatEncryptionKey()&lt;/p&gt;

&lt;p&gt;%SYSTEM.Encryption.ActivateEncryptionKey() ;just supply the kms key, no need for username or password&lt;/p&gt;

&lt;p&gt;do ReadFile^EncryptionKey(&amp;lt;key&amp;gt;,.data) zw data ;it will be obvious if the key is kms type from the data returned.&lt;/p&gt;

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

&lt;p&gt;Doc link:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.intersystems.com/irisforhealth20233/csp/docbook/DocBook.UI.Page.cls?KEY=ROARS_encrypt_mgmt#ROARS_encrypt_KMS" rel="noopener noreferrer"&gt;Key Management Tasks | InterSystems IRIS for Health 2023.3&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>aws</category>
      <category>encryption</category>
      <category>beginners</category>
    </item>
    <item>
      <title>IRIS SIEM System Integration with Crowdstrike Logscale</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 26 Apr 2026 16:17:27 +0000</pubDate>
      <link>https://dev.to/intersystems/iris-siem-system-integration-with-crowdstrike-logscale-5406</link>
      <guid>https://dev.to/intersystems/iris-siem-system-integration-with-crowdstrike-logscale-5406</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.amazonaws.com%2Fuploads%2Farticles%2Fg68k8f6ffrgaoekhdq3l.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.amazonaws.com%2Fuploads%2Farticles%2Fg68k8f6ffrgaoekhdq3l.png" alt=" " width="800" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IRIS makes &lt;a href="https://www.irs.gov/privacy-disclosure/security-information-and-event-management-siem-systems" rel="noopener noreferrer"&gt;SIEM&lt;/a&gt; systems integration simple with Structured Logging and Pipes!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Adding a SIEM integration to InterSystems IRIS for "Audit Database Events" was dead simple with the &lt;a href="https://cloud.community.humio.com/" rel="noopener noreferrer"&gt;Community Edition of CrowdStrike's Falcon LogScale&lt;/a&gt;, and here's how I got it done.  &lt;br&gt;&lt;br&gt;&lt;strong&gt;CrowdStrike Community LogScale Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloud.community.humio.com/" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt; was ridiculously straight forward and I had the account approved in a couple of days with the following disclaimer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Falcon LogScale Community is a free service providing you with up to 16 GB/day of data ingest, up to 5 users, and 7 day data retention, if you exceed the limitations, you’ll be asked to upgrade to a paid offering. You can use Falcon LogScale under the limitations as long as you want, provided, that we can modify or terminate the Community program at any time without notice or liability of any kind.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pretty generous and a good fit for this implementation, with the caveat all good things can come to an end I guess, cut your self an ingestion token in the UI and save it to your favorite hiding place for secrets.&lt;br&gt;&lt;br&gt;&lt;strong&gt;Python Interceptor - irislogd2crwd.py&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wont go over this amazing piece of software engineering in detail, but it is as simple as a python implementation that accepts STDIN, breaks up what it sees into events, and ships them off to the SIEM platform to be ingested.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;#!/usr/bin/env python&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; json
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; time
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; os
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; sys
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; requests
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; socket
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; datetime &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; datetime
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; humiolib.HumioClient &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; HumioIngestClient


input_list = sys.stdin.read().splitlines() &lt;span class="hljs-comment"&gt;# From ^LOGDMN Pipe!&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;for&lt;/span&gt; irisevent &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; input_list:
    &lt;span class="hljs-comment"&gt;# Required for CRWD Data Source&lt;/span&gt;
    today = datetime.now()
    fqdn = socket.getfqdn()

    payload = [
        {
            &lt;span class="hljs-string"&gt;"tags"&lt;/span&gt;: {
                &lt;span class="hljs-string"&gt;"host"&lt;/span&gt;: fqdn,
                &lt;span class="hljs-string"&gt;"source"&lt;/span&gt;: &lt;span class="hljs-string"&gt;"irislogd"&lt;/span&gt;
            },
                &lt;span class="hljs-string"&gt;"events"&lt;/span&gt;: [
                {
                    &lt;span class="hljs-string"&gt;"timestamp"&lt;/span&gt;: today.isoformat(sep=&lt;span class="hljs-string"&gt;'T'&lt;/span&gt;,timespec=&lt;span class="hljs-string"&gt;'auto'&lt;/span&gt;) + &lt;span class="hljs-string"&gt;"Z"&lt;/span&gt;,
                    &lt;span class="hljs-string"&gt;"attributes"&lt;/span&gt;: {&lt;span class="hljs-string"&gt;"irislogd"&lt;/span&gt;:json.loads(irisevent)} 
                }
            ]
        }
    ]

    client = HumioIngestClient(
        base_url= &lt;span class="hljs-string"&gt;"https://cloud.community.humio.com"&lt;/span&gt;,
        ingest_token= os.environ[&lt;span class="hljs-string"&gt;"CRWD_LOGSCALE_APIKEY"&lt;/span&gt;]
    )
    ingest_response = client.ingest_json_data(payload)

    
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;You will want to &lt;strong&gt;chmod +x&lt;/strong&gt; this script and put it where &lt;strong&gt;irisowner&lt;/strong&gt; can enjoy it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;strong&gt;InterSystems IRIS Structured Logging Setup&lt;/strong&gt;&lt;br&gt;&lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=ALOG" rel="noopener noreferrer"&gt;Structured Logging in IRIS&lt;/a&gt; is documented to the 9's, so this will be a Cliff Note to the end state of configuring ^LOGDMN.  The thing that caught my attention in the docs is probably the most unclear part of the implementation, but the most powerful and fun for sure.&lt;br&gt;&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2Frqogkjvf367fcrclgkb7.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.amazonaws.com%2Fuploads%2Farticles%2Frqogkjvf367fcrclgkb7.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After:&lt;br&gt;&lt;strong&gt;ENABLING&lt;/strong&gt; the Log Daemon, &lt;strong&gt;CONFIGURING&lt;/strong&gt; the Log Daemon and &lt;strong&gt;STARTING&lt;/strong&gt; Logging your configuration should look like this:&lt;br&gt; &lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="hljs-built_in"&gt;%SYS&lt;/span&gt;&amp;gt;&lt;span class="hljs-keyword"&gt;Do&lt;/span&gt; &lt;span class="hljs-symbol"&gt;^LOGDMN&lt;/span&gt;
&lt;span class="hljs-number"&gt;1&lt;/span&gt;) Enable logging
&lt;span class="hljs-number"&gt;2&lt;/span&gt;) Disable logging
&lt;span class="hljs-number"&gt;3&lt;/span&gt;) Display configuration
&lt;span class="hljs-number"&gt;4&lt;/span&gt;) Edit configuration
&lt;span class="hljs-number"&gt;5&lt;/span&gt;) &lt;span class="hljs-keyword"&gt;Set&lt;/span&gt; default configuration
&lt;span class="hljs-number"&gt;6&lt;/span&gt;) Display logging status
&lt;span class="hljs-number"&gt;7&lt;/span&gt;) Start logging
&lt;span class="hljs-number"&gt;8&lt;/span&gt;) Stop logging
&lt;span class="hljs-number"&gt;9&lt;/span&gt;) Restart logging

LOGDMN option? &lt;span class="hljs-number"&gt;3&lt;/span&gt;
LOGDMN configuration

Minimum level: -&lt;span class="hljs-number"&gt;1&lt;/span&gt; (DEBUG)
 Pipe command: /tmp/irislogd2crwd.py
       Format: JSON
     Interval: &lt;span class="hljs-number"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;pre&gt;/tmp/irislogd2crwd.py  # Location of our chmod +x Python Interceptor
JSON                   # Important&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now that we are logging somewhere else, lets just pump up the verbosity in the Audit Log and enable all the events since somebody else is paying for it.&lt;br&gt;&lt;br&gt;Stealing from &lt;a class="mentioned-user" href="https://dev.to/sylvain"&gt;@sylvain&lt;/a&gt;.Guilbaud&lt;span&gt; 's &lt;a href="https://community.intersystems.com/post/how-activate-all-audit-system-events" rel="noopener noreferrer"&gt;post&lt;/a&gt;:&lt;/span&gt;&lt;br&gt;&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2Fh0ii8d2x1fi65u4v2ahc.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.amazonaws.com%2Fuploads%2Farticles%2Fh0ii8d2x1fi65u4v2ahc.png" alt=" " width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;strong&gt;CrowdStrike LogScale Event Processing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It wont take long to get the hang of, but the Search Console is the beginning of all good things with setting up customized observability based on your events.  The search pane with filter criteria displays in the left corner, the available attributes on the left sidebar and the matching events in the results pane in the main view.&lt;br&gt;&lt;br&gt;LogScale uses The LogScale &lt;em&gt;Query Language&lt;/em&gt; (&lt;a href="https://library.humio.com/data-analysis/syntax.html" rel="noopener noreferrer"&gt;LQL&lt;/a&gt;) &lt;span&gt; to back the widgets, alerts and actions.&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.amazonaws.com%2Fuploads%2Farticles%2Fr8vz7xje2v52zmgh7wyz.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.amazonaws.com%2Fuploads%2Farticles%2Fr8vz7xje2v52zmgh7wyz.png" alt=" " width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I suck at visualizations, so I am sure you could do better than below with a box of crayons, but here is my 4 widgets of glory to put a clown suit on the SIEM events for this post:&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.amazonaws.com%2Fuploads%2Farticles%2Fw9fh286x7cg58rut35nn.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.amazonaws.com%2Fuploads%2Farticles%2Fw9fh286x7cg58rut35nn.png" alt=" " width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we look under the hood for the "Event Types" widget, the following LQL is only needed behind a time series graph lql:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;timechart(irislogd.event)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;br&gt;So we did the thing!&lt;br&gt;&lt;br&gt;&lt;strong&gt;We've integrated IRIS with the Enterprise SIEM implementation&lt;/strong&gt; and the Security Team is "😀 "  &lt;br&gt;&lt;br&gt;The bonus here are the things that are also accomplished with the exact same development pattern as above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notifications&lt;/li&gt;
&lt;li&gt;Actions&lt;/li&gt;
&lt;li&gt;Scheduled Searches&lt;/li&gt;
&lt;li&gt;Scheduled Daily Reports&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>monitoring</category>
      <category>beginners</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>From FHIR Events to Explainable Agentic AI: Building a Clinical Follow‑Up Demo with InterSystems IRIS for Health</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Wed, 22 Apr 2026 16:50:36 +0000</pubDate>
      <link>https://dev.to/intersystems/from-fhir-events-to-explainable-agentic-ai-building-a-clinical-follow-up-demo-with-intersystems-325n</link>
      <guid>https://dev.to/intersystems/from-fhir-events-to-explainable-agentic-ai-building-a-clinical-follow-up-demo-with-intersystems-325n</guid>
      <description>&lt;p&gt;&lt;strong&gt;10:47 AM&lt;/strong&gt; — Jose Garcia's creatinine test results arrive at the hospital FHIR server.&lt;br&gt;
&lt;strong&gt;2.1 mg/dL&lt;/strong&gt; — a 35% increase from last month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens next?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Most systems:&lt;/strong&gt; ❌ The result sits in a queue until a clinician reviews it manually — hours or days later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This system:&lt;/strong&gt; 👍 An AI agent evaluates the trend, consults clinical guidelines, and generates evidence-based recommendations — &lt;strong&gt;in seconds, automatically&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;No chatbot. No manual prompts. No black-box reasoning.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is &lt;strong&gt;event-driven clinical decision support&lt;/strong&gt; with full explainability:&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.amazonaws.com%2Fuploads%2Farticles%2Ftlnxmq8i5cwmr06vj6mu.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.amazonaws.com%2Fuploads%2Farticles%2Ftlnxmq8i5cwmr06vj6mu.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Triggered automatically&lt;/strong&gt; by FHIR events&lt;br&gt;
✅ &lt;strong&gt;Multi-agent reasoning&lt;/strong&gt; (context, guidelines, recommendations)&lt;br&gt;
✅ &lt;strong&gt;Complete audit trail&lt;/strong&gt; in SQL (every decision, every evidence source)&lt;br&gt;
✅ &lt;strong&gt;FHIR-native outputs&lt;/strong&gt; (DiagnosticReport published to server)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built with:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;InterSystems IRIS for Health&lt;/strong&gt; — Orchestration, FHIR, persistence, vector search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CrewAI&lt;/strong&gt; — Multi-agent framework for structured reasoning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You'll learn:&lt;/strong&gt; 🖋️ How to &lt;strong&gt;orchestrate agentic AI workflows&lt;/strong&gt; within production-grade interoperability systems — and why &lt;strong&gt;explainability&lt;/strong&gt; matters more than accuracy alone.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/43Vl7cU_uNY"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  🎬 What This Demo Produces
&lt;/h2&gt;

&lt;p&gt;When Jose's abnormal creatinine observation arrives, the system automatically generates:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;INPUT:&lt;/strong&gt; FHIR Observation (creatinine 2.1 mg/dL, status: HIGH)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OUTPUT:&lt;/strong&gt; FHIR DiagnosticReport containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Risk Level:&lt;/strong&gt; Medium-High (confidence: 85%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendations:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;⚠️ Repeat creatinine in 7–14 days&lt;/li&gt;
&lt;li&gt;💊 Review nephrotoxic medications (currently on Ibuprofen)&lt;/li&gt;
&lt;li&gt;📊 Monitor renal function closely&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Evidence Used:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Patient context: CKD Stage 3 + progressive creatinine rise (&amp;gt;30%)&lt;/li&gt;
&lt;li&gt;Clinical guidelines: KDIGO section on AKI management in CKD&lt;/li&gt;
&lt;li&gt;Lab trend analysis: 1.6 → 1.9 → 2.1 mg/dL over 3 months&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AUDIT TRAIL:&lt;/strong&gt; Every decision, recommendation, and evidence citation persisted in SQL tables for compliance and review.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 What Problem Does This Solve?
&lt;/h2&gt;

&lt;p&gt;Most AI demos in healthcare focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chat interfaces for asking questions&lt;/li&gt;
&lt;li&gt;Unstructured text outputs&lt;/li&gt;
&lt;li&gt;Opaque reasoning ("trust the AI")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In real clinical environments, what matters is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reacting to &lt;strong&gt;clinical events&lt;/strong&gt; automatically&lt;/li&gt;
&lt;li&gt;Understanding complete &lt;strong&gt;patient context&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Providing &lt;strong&gt;explainable recommendations&lt;/strong&gt; with evidence&lt;/li&gt;
&lt;li&gt;Persisting decisions for &lt;strong&gt;audit and compliance&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This demo answers a simple but realistic question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What happens when a new abnormal lab result arrives — and how can we automate the initial clinical assessment while maintaining transparency?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧪 Demo Scenario: CKD + Rising Creatinine
&lt;/h2&gt;

&lt;p&gt;The demo is based on a common healthcare use case:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patient: Jose Garcia (MRN-1000001)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Conditions:&lt;/strong&gt; Chronic Kidney Disease (CKD Stage 3), Hypertension&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medications:&lt;/strong&gt; Ibuprofen (NSAID), Lisinopril&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lab history:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;3 months ago: 1.6 mg/dL&lt;/li&gt;
&lt;li&gt;1 month ago: 1.9 mg/dL&lt;/li&gt;
&lt;li&gt;Today: &lt;strong&gt;2.1 mg/dL&lt;/strong&gt; ← triggers workflow&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;&amp;gt;30% progressive increase&lt;/strong&gt; requires clinical follow-up.&lt;/p&gt;

&lt;p&gt;Instead of waiting for manual review, the system automatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detects the event&lt;/strong&gt; (FHIR Observation POST)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieves patient context&lt;/strong&gt; (conditions, medications, lab history)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consults clinical guidelines&lt;/strong&gt; via RAG (vector search)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performs agentic reasoning&lt;/strong&gt; across three specialized agents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Produces explainable recommendations&lt;/strong&gt; with evidence citations&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  ⏱️ From Event to Evidence: The Complete Journey
&lt;/h2&gt;

&lt;p&gt;Follow a single lab result through the system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FHIR Observation posted to IRIS server&lt;/li&gt;
&lt;li&gt;Interoperability Production triggered&lt;/li&gt;
&lt;li&gt;Context Agent queries patient history from FHIR&lt;/li&gt;
&lt;li&gt;Guidelines Agent searches vector database (clinical documents)&lt;/li&gt;
&lt;li&gt;Reasoning Agent synthesizes 3 recommendations&lt;/li&gt;
&lt;li&gt;Results persisted to SQL (&lt;code&gt;Cases&lt;/code&gt;, &lt;code&gt;CaseRecommendations&lt;/code&gt;, &lt;code&gt;CaseEvidences&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;FHIR DiagnosticReport published to server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete&lt;/strong&gt; — Full audit trail available for review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From event to actionable recommendations.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Architecture Overview
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key Principle
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;InterSystems IRIS for Health is the orchestrator and system of record.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The AI agents are external capabilities that are governed, triggered, and integrated by the IRIS platform. IRIS owns the data, the workflow, and the audit trail — the agents provide specialized reasoning.&lt;/p&gt;

&lt;h3&gt;
  
  
  High-Level Flow
&lt;/h3&gt;

&lt;p&gt;Key steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FHIR Observation&lt;/strong&gt; → POSTed to IRIS FHIR server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interaction Strategy&lt;/strong&gt; → Detects clinical event&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interoperability Production&lt;/strong&gt; → Orchestrates workflow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Operation&lt;/strong&gt; → Calls Agentic AI REST service (FastAPI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents Execute&lt;/strong&gt; → Context retrieval, guideline search, reasoning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Results Return&lt;/strong&gt; → Structured JSON back to IRIS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistence&lt;/strong&gt; → SQL tables store cases, recommendations, evidence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publishing&lt;/strong&gt; → FHIR DiagnosticReport created and stored&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Visual Components
&lt;/h3&gt;

&lt;p&gt;The demo includes a &lt;strong&gt;Gradio web UI&lt;/strong&gt; for interactive demonstration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Post lab values and trigger the workflow&lt;/li&gt;
&lt;li&gt;Watch real-time agent progress&lt;/li&gt;
&lt;li&gt;View recommendations and evidence citations&lt;/li&gt;
&lt;li&gt;Query SQL audit tables&lt;/li&gt;
&lt;li&gt;Access IRIS Production message viewer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the complete flow visible and understandable.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤖 Why CrewAI? Understanding Multi-Agent Architecture
&lt;/h2&gt;

&lt;p&gt;CrewAI is a &lt;strong&gt;multi-agent orchestration framework&lt;/strong&gt; that enables specialized AI agents to collaborate on complex tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In this demo, three agents work sequentially:&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Context Agent
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Gather patient clinical history from FHIR server&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch patient demographics and conditions&lt;/li&gt;
&lt;li&gt;Retrieve historical lab results (creatinine trends)&lt;/li&gt;
&lt;li&gt;Collect active medications&lt;/li&gt;
&lt;li&gt;Identify risk factors (NSAID use + CKD)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt; Structured patient context for reasoning&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Guidelines Agent
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Search clinical knowledge base using RAG (Retrieval-Augmented Generation)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query IRIS vector database with semantic search&lt;/li&gt;
&lt;li&gt;Find relevant guideline sections (clinical protocols, etc.)&lt;/li&gt;
&lt;li&gt;Retrieve evidence chunks with similarity scores&lt;/li&gt;
&lt;li&gt;Provide citations for recommendations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt; Evidence-based clinical guidance&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Reasoning Agent
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Role:&lt;/strong&gt; Synthesize recommendations from context + guidelines&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Analyze lab trends (&amp;gt;30% increase = significant)&lt;/li&gt;
&lt;li&gt;Identify risk factors (CKD + NSAID + progressive rise)&lt;/li&gt;
&lt;li&gt;Apply clinical decision rules&lt;/li&gt;
&lt;li&gt;Generate structured recommendations with confidence levels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt; Risk assessment + actionable follow-up plan&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Multi-Agent Instead of Single LLM Call?
&lt;/h3&gt;

&lt;p&gt;Agentic workflows provide:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Better structured reasoning&lt;/strong&gt; — Each agent has a focused responsibility&lt;br&gt;
✅ &lt;strong&gt;Tool use&lt;/strong&gt; — Agents can query FHIR, search vector databases, analyze trends&lt;br&gt;
✅ &lt;strong&gt;Explainable decision chains&lt;/strong&gt; — Each step is traceable&lt;br&gt;
✅ &lt;strong&gt;Separation of concerns&lt;/strong&gt; — Context ≠ Guidelines ≠ Reasoning&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; IRIS orchestrates the agents — CrewAI is used as a library, not the platform. IRIS owns persistence, orchestration, FHIR integration, and audit trails.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔄 Interoperability Production
&lt;/h2&gt;

&lt;p&gt;The workflow is managed by three IRIS components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Business Service&lt;/strong&gt; (&lt;code&gt;FHIRObservationIn&lt;/code&gt;)&lt;br&gt;
Triggered automatically when FHIR Observation is POSTed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Business Process&lt;/strong&gt; (&lt;code&gt;FollowUpAI&lt;/code&gt;)&lt;br&gt;
Orchestrates three-step workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call agent service&lt;/li&gt;
&lt;li&gt;Persist results to SQL&lt;/li&gt;
&lt;li&gt;Publish DiagnosticReport&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Business Operations&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ClinicalAgenticOperation&lt;/code&gt; → REST call to FastAPI/CrewAI&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ClinicalAiPersistence&lt;/code&gt; → SQL table writes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ClinicalReportPublisher&lt;/code&gt; → FHIR DiagnosticReport POST&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🔍 Explainability: Proving the AI's Reasoning
&lt;/h2&gt;

&lt;p&gt;One of the most critical aspects of clinical AI is &lt;strong&gt;proving why a recommendation was made&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;IRIS persists everything in a &lt;strong&gt;minimal, queryable SQL model&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Cases&lt;/code&gt;&lt;/strong&gt; — What happened (patient, observation, risk level, confidence)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CaseRecommendations&lt;/code&gt;&lt;/strong&gt; — What to do (action type, description, timeframe)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CaseEvidences&lt;/code&gt;&lt;/strong&gt; — Why (guideline citations, similarity scores, text excerpts)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Example Queries
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"What cases were evaluated today?"&lt;/strong&gt;&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;CaseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;PatientRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;RiskLevel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Confidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ReasoningSummary&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;clinicalai_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cases&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&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;CreatedAt&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CaseId: CSE-20260108-001
PatientRef: Patient/1 (Jose Garcia)
RiskLevel: medium-high
Confidence: high
ReasoningSummary: The patient with stage 3 chronic kidney disease and hypertension demonstrates a sustained and progressive increase in serum creatinine over 90 days...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;"Why did the agent recommend nephrotoxic medication review?"&lt;/strong&gt;&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GuidelineId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Similarity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Excerpt&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;clinicalai_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CaseEvidences&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CaseId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'b344f121-db68-4cd6-8877-1855c3d547ff'&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Similarity&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GuidelineId: ckd_creatinine_guideline_demo
Similarity: 0.66
Excerpt: "Recommended actions include repeat serum creatinine testing within 7–14 days, review of current medications for nephrotoxicity, assessment of contributing factors, and close monitoring of renal function."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Every recommendation has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The clinical context used&lt;/li&gt;
&lt;li&gt;The guidelines consulted&lt;/li&gt;
&lt;li&gt;The similarity scores showing relevance&lt;/li&gt;
&lt;li&gt;The reasoning chain from data to decision&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can answer "Why did the AI recommend this?" with SQL queries and evidence citations.&lt;/p&gt;




&lt;h2&gt;
  
  
  🩺 Publishing Results as FHIR DiagnosticReport
&lt;/h2&gt;

&lt;p&gt;The final step closes the loop: AI outputs become part of the &lt;strong&gt;clinical record&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The system publishes a &lt;strong&gt;FHIR DiagnosticReport&lt;/strong&gt; containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subject:&lt;/strong&gt; Patient reference (Jose Garcia)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; Link to triggering Observation (creatinine 2.1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conclusion:&lt;/strong&gt; Risk level + reasoning summary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PresentedForm:&lt;/strong&gt; Human-readable recommendations (Base64-encoded)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensions:&lt;/strong&gt; Case ID, confidence score, model metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the AI output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interoperable&lt;/strong&gt; — Standard FHIR resource&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumable&lt;/strong&gt; — Accessible via FHIR API by EHRs, portals, apps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditable&lt;/strong&gt; — Part of the permanent clinical record&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queryable&lt;/strong&gt; — &lt;code&gt;GET /DiagnosticReport?result=Observation/14&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The DiagnosticReport is not a separate "AI system output" — it's a &lt;strong&gt;first-class clinical document&lt;/strong&gt; that follows the same standards as lab reports and radiology findings.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Try It Yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Quick Start (15 minutes):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clone the repository&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git clone https://github.com/intersystems-ib/iris-health-fhir-agentic-demo
   &lt;span class="nb"&gt;cd &lt;/span&gt;iris-health-fhir-agentic-demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start IRIS container&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Load sample patient data&lt;/strong&gt; (Jose Garcia with CKD history)&lt;br&gt;
Follow the &lt;a href="https://github.com/intersystems-ib/iris-health-fhir-agentic-demo#5-%F0%9F%91%A4-load-sample-fhir-data" rel="noopener noreferrer"&gt;README setup instructions&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run the Gradio UI&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   python run_ui.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open browser to &lt;code&gt;http://localhost:7860&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;POST an abnormal lab value&lt;/strong&gt; and watch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time agent progress&lt;/li&gt;
&lt;li&gt;Evidence retrieval from vector database&lt;/li&gt;
&lt;li&gt;Recommendations generated with confidence scores&lt;/li&gt;
&lt;li&gt;SQL audit trail queries&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Query the results&lt;/strong&gt; using IRIS SQL Explorer or Management Portal&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;💬 Questions or feedback?&lt;/strong&gt; Reply to this post — I'd love to hear about your use cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 What You've Learned
&lt;/h2&gt;

&lt;p&gt;If you've followed along, you now understand how to:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Trigger AI workflows from FHIR events&lt;/strong&gt; — No manual initiation required&lt;br&gt;
✅ &lt;strong&gt;Orchestrate multi-agent systems with CrewAI&lt;/strong&gt; — Context, Guidelines, Reasoning agents&lt;br&gt;
✅ &lt;strong&gt;Build explainable AI with SQL audit trails&lt;/strong&gt; — Every decision is traceable&lt;br&gt;
✅ &lt;strong&gt;Publish AI outputs as FHIR resources&lt;/strong&gt; — Interoperable clinical documents&lt;br&gt;
✅ &lt;strong&gt;Integrate agentic AI with IRIS Interoperability&lt;/strong&gt; — Production-grade orchestration&lt;/p&gt;




&lt;h2&gt;
  
  
  🔮 Beyond Lab Results: What Else Can You Automate?
&lt;/h2&gt;

&lt;p&gt;This pattern applies to many clinical scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medication reconciliation alerts&lt;/strong&gt; — Detect drug-drug interactions or contraindications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Care gap identification&lt;/strong&gt; — Missing screenings based on age, conditions, guidelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk stratification triggers&lt;/strong&gt; — Identify high-risk patients for intervention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clinical trial matching&lt;/strong&gt; — Find eligible patients based on inclusion criteria&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture is the same: &lt;strong&gt;event → context → evidence → reasoning → action&lt;/strong&gt;.&lt;/p&gt;




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

&lt;p&gt;This demo shows how &lt;strong&gt;Agentic AI&lt;/strong&gt; can be safely and effectively integrated into &lt;strong&gt;real clinical workflows&lt;/strong&gt; using &lt;strong&gt;InterSystems IRIS for Health&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By combining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event-driven interoperability&lt;/strong&gt; — React to clinical events automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agentic reasoning&lt;/strong&gt; — Multi-agent collaboration with tool use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL persistence&lt;/strong&gt; — Full audit trails for compliance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FHIR-native outputs&lt;/strong&gt; — Standard clinical documents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We move from AI experiments to &lt;strong&gt;platform-grade clinical AI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;⭐ &lt;strong&gt;Star the repo:&lt;/strong&gt; &lt;a href="https://github.com/intersystems-ib/iris-health-fhir-agentic-demo" rel="noopener noreferrer"&gt;https://github.com/intersystems-ib/iris-health-fhir-agentic-demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🧪 &lt;strong&gt;Try the demo&lt;/strong&gt; with your own clinical guidelines&lt;/p&gt;

&lt;p&gt;💬 &lt;strong&gt;Share your use case&lt;/strong&gt; — What clinical event would you automate first?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>vectordatabase</category>
      <category>sql</category>
    </item>
    <item>
      <title>Dify Now Supports IRIS as a Vector Store — Setup Guide</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 21 Apr 2026 14:18:22 +0000</pubDate>
      <link>https://dev.to/intersystems/dify-now-supports-iris-as-a-vector-store-setup-guide-396l</link>
      <guid>https://dev.to/intersystems/dify-now-supports-iris-as-a-vector-store-setup-guide-396l</guid>
      <description>&lt;h2&gt;Why This Integration Matters&lt;/h2&gt;
&lt;p&gt;InterSystems continues to push AI capabilities forward natively in IRIS — vector search, MCP support, and Agentic AI capabilities. That roadmap is important, and there is no intention of stepping back from it.&lt;/p&gt;
&lt;p&gt;But the AI landscape is also evolving in a way that makes ecosystem integration increasingly essential. Tools like &lt;strong&gt;Dify&lt;/strong&gt; — an open-source, production-grade LLM orchestration platform — have become a serious part of enterprise AI stacks. In Japan in particular, Dify adoption is no longer just for startups or hobbyists; it has reached large enterprises, with employees using it as the backbone of internal AI workflows. Meeting developers and teams where they already are is as valuable as building new capabilities in isolation.&lt;/p&gt;
&lt;p&gt;That's the motivation behind this integration: IRIS handles what it does best — reliable, queryable, SQL-accessible data with built-in processing logic — while Dify handles LLM orchestration, RAG pipelines, and agentic workflows. Together, they form a stack where IRIS users don't have to choose between their data infrastructure and the AI tools gaining momentum around them.&lt;/p&gt;
&lt;p&gt;This integration was contributed to Dify as an OSS pull request and merged in Dify v1.11.2 (&lt;a href="https://github.com/langgenius/dify/pull/29480" rel="noopener noreferrer"&gt;#29480&lt;/a&gt;). Several follow-up fixes have been merged since — covered below. This article walks through the setup.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First, I'd like to thank &lt;span&gt;&lt;strong&gt;&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/megumi"&gt;@megumi&lt;/a&gt;.Kakechi&lt;/span&gt;&lt;/strong&gt;&lt;/span&gt; for encouraging me to post this on the English Developer Community — this would have remained a Japanese-only article without that push. I'd also like to extend my gratitude to &lt;span&gt;&lt;strong&gt;&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/tomohiro"&gt;@tomohiro&lt;/a&gt;.Iwamoto&lt;/span&gt;&lt;/strong&gt;&lt;/span&gt; and &lt;span&gt;&lt;strong&gt;&lt;span&gt;@Mihoko.Iijima&lt;/span&gt;&lt;/strong&gt;&lt;/span&gt;, who took the time to test this integration hands-on and provided invaluable feedback that helped shape the fixes included in later releases.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;OSS Background:&lt;/strong&gt; If you're curious about the contribution journey itself, I wrote about it on Zenn: &lt;a href="https://zenn.dev/tomookuyama/articles/214abc2cb73212" rel="noopener noreferrer"&gt;「このDB、もっと知られてもいいのでは？」からOSSコントリビュートに至った話&lt;/a&gt; (Japanese)&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Docker / Docker Compose&lt;/td&gt;
&lt;td&gt;Any recent version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git&lt;/td&gt;
&lt;td&gt;For cloning the Dify repository&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;Setup&lt;/h2&gt;
&lt;h3&gt;1. Clone the Dify repository&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; git clone &lt;a href="https://github.com/langgenius/dify.git" rel="noopener noreferrer"&gt;https://github.com/langgenius/dify.git&lt;/a&gt;&lt;br&gt;
&amp;gt; cd dify/docker&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Prepare the environment file&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cp .env.example .env&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Enable IRIS as the vector store&lt;/h3&gt;
&lt;p&gt;Open &lt;code&gt;.env&lt;/code&gt; and change one line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Before (default)&lt;br&gt;
VECTOR_STORE=weaviate
&lt;h1&gt;
  
  
  After
&lt;/h1&gt;

&lt;p&gt;VECTOR_STORE=iris&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the minimum. IRIS connection defaults are pre-configured for local use. To connect to an existing IRIS instance or customize the setup, the relevant parameters are:&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Default&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;&lt;code&gt;IRIS_HOST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;iris&lt;/td&gt;
&lt;td&gt;Container service name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IRIS_SUPER_SERVER_PORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1972&lt;/td&gt;
&lt;td&gt;SuperServer port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IRIS_WEB_SERVER_PORT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;52773&lt;/td&gt;
&lt;td&gt;Management Portal port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IRIS_USER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;_SYSTEM&lt;/td&gt;
&lt;td&gt;Login username&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IRIS_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dify@1234&lt;/td&gt;
&lt;td&gt;Set automatically on first launch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IRIS_DATABASE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;USER&lt;/td&gt;
&lt;td&gt;Target namespace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IRIS_SCHEMA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dify&lt;/td&gt;
&lt;td&gt;SQL schema for Dify tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IRIS_TEXT_INDEX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Enable full-text index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IRIS_TEXT_INDEX_LANGUAGE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;en&lt;/td&gt;
&lt;td&gt;Language for text indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IRIS_MIN_CONNECTION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Connection pool minimum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IRIS_MAX_CONNECTION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Connection pool maximum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IRIS_TIMEZONE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UTC&lt;/td&gt;
&lt;td&gt;Timezone setting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;4. Start the containers&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. Confirm all containers are running&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; docker compose ps&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look for the &lt;code&gt;iris&lt;/code&gt; container with a &lt;code&gt;STATUS&lt;/code&gt; of &lt;code&gt;Up&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; docker % docker compose ps --format "table {{.Name}}\t{{.Service}}\t{{.Status}}"&lt;br&gt;
NAME                     SERVICE         STATUS&lt;br&gt;
docker-api-1             api             Up&lt;br&gt;
docker-db_postgres-1     db_postgres     Up (healthy)&lt;br&gt;
docker-nginx-1           nginx           Up&lt;br&gt;
docker-plugin_daemon-1   plugin_daemon   Up&lt;br&gt;
docker-redis-1           redis           Up (healthy)&lt;br&gt;
docker-sandbox-1         sandbox         Up (healthy)&lt;br&gt;
docker-ssrf_proxy-1      ssrf_proxy      Up&lt;br&gt;
docker-web-1             web             Up&lt;br&gt;
docker-worker-1          worker          Up&lt;br&gt;
docker-worker_beat-1     worker_beat     Up&lt;br&gt;
iris                     iris            Up   &amp;lt;-- this one&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Access Dify&lt;/h2&gt;
&lt;p&gt;Navigate to &lt;code&gt;&lt;a href="http://localhost/" rel="noopener noreferrer"&gt;http://localhost/&lt;/a&gt;&lt;/code&gt; in your browser. On first launch you'll be prompted to create an admin account.&lt;/p&gt;

&lt;h2&gt;Verify IRIS is Storing Your Vectors&lt;/h2&gt;
&lt;h3&gt;Step 1 — Create a Knowledge Base&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Log in to Dify&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Knowledge&lt;/strong&gt; → &lt;strong&gt;Create Knowledge&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Upload a text file or PDF&lt;/li&gt;
&lt;li&gt;In Step 2, under &lt;strong&gt;Index Method&lt;/strong&gt;, select &lt;strong&gt;"High Quality"&lt;/strong&gt; (recommended)&lt;/li&gt;
&lt;/ol&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.amazonaws.com%2Fuploads%2Farticles%2Fhlf4cj9ao0q2hbp1woqy.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.amazonaws.com%2Fuploads%2Farticles%2Fhlf4cj9ao0q2hbp1woqy.png" alt=" " width="800" height="912"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If no Embedding Model has been configured yet, the dropdown will show "No model found." Click &lt;strong&gt;"Model Provider Settings"&lt;/strong&gt; at the bottom of the dropdown to proceed.&lt;/p&gt;
&lt;h3&gt;Step 2 — Set Up a Model Provider (OpenAI)&lt;/h3&gt;
&lt;p&gt;The Model Provider screen lists available providers. Find &lt;strong&gt;OpenAI&lt;/strong&gt; and click &lt;strong&gt;Install&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.amazonaws.com%2Fuploads%2Farticles%2F6u6ls59sz97m7erboyj4.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.amazonaws.com%2Fuploads%2Farticles%2F6u6ls59sz97m7erboyj4.png" alt=" " width="800" height="650"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note on cost:&lt;/strong&gt; OpenAI requires an API key separate from a ChatGPT Plus subscription — you'll need to add credits to your OpenAI API account. Embedding costs are extremely low, however; a few dollars will go a long way. If you'd prefer a free alternative, local models via &lt;strong&gt;LM Studio&lt;/strong&gt; or &lt;strong&gt;Ollama&lt;/strong&gt; (OpenAI-API-compatible) are also supported.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;After installation, OpenAI appears under &lt;strong&gt;"To be configured"&lt;/strong&gt;. Click &lt;strong&gt;Setup&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.amazonaws.com%2Fuploads%2Farticles%2Fqi4hw0rq81fljs3po793.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.amazonaws.com%2Fuploads%2Farticles%2Fqi4hw0rq81fljs3po793.png" alt=" " width="800" height="563"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;API Key Authorization Configuration&lt;/strong&gt; dialog opens. If you don't have an API key yet, click &lt;strong&gt;"Get your API Key from OpenAI"&lt;/strong&gt; to open the OpenAI API keys page directly.&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.amazonaws.com%2Fuploads%2Farticles%2F740f24thl60ami9kergd.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.amazonaws.com%2Fuploads%2Farticles%2F740f24thl60ami9kergd.png" alt=" " width="800" height="659"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enter a name (e.g. &lt;code&gt;dify&lt;/code&gt;), paste your API key, then click &lt;strong&gt;Save&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.amazonaws.com%2Fuploads%2Farticles%2F99qze6cofi5yvgog22wl.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.amazonaws.com%2Fuploads%2Farticles%2F99qze6cofi5yvgog22wl.png" alt=" " width="800" height="659"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt; &lt;/h3&gt;
&lt;h3&gt;Step 3 — Select the Embedding Model&lt;/h3&gt;
&lt;p&gt;Return to the Knowledge creation screen. The &lt;strong&gt;Embedding Model&lt;/strong&gt; dropdown now lists available OpenAI models. Select &lt;code&gt;text-embedding-3-small&lt;/code&gt; — it offers an excellent balance of cost and retrieval quality for most use cases.&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.amazonaws.com%2Fuploads%2Farticles%2Fby6madpme774ncrznrz4.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.amazonaws.com%2Fuploads%2Farticles%2Fby6madpme774ncrznrz4.png" alt=" " width="800" height="881"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Save &amp;amp; Process&lt;/strong&gt;. When a green checkmark appears next to your document, embedding is complete — your document chunks are now stored as vectors in IRIS.&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.amazonaws.com%2Fuploads%2Farticles%2F3ak4jg5quwnjhwpd6ijl.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.amazonaws.com%2Fuploads%2Farticles%2F3ak4jg5quwnjhwpd6ijl.png" alt=" " width="800" height="607"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt;Step 4 — Inspect the Data via Management Portal&lt;/h3&gt;
&lt;p&gt;This is where IRIS users have a distinct advantage. Open the Management Portal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;a href="http://localhost:52773/csp/sys/UtilHome.csp?$NAMESPACE=USER" rel="noopener noreferrer"&gt;http://localhost:52773/csp/sys/UtilHome.csp?$NAMESPACE=USER&lt;/a&gt;&lt;/code&gt;&lt;/pre&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;Value&lt;/th&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_SYSTEM&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Dify@1234&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Navigate to &lt;strong&gt;System Explorer → SQL&lt;/strong&gt;, set the schema filter to &lt;code&gt;dify&lt;/code&gt;, and the tables Dify created will appear. Open one, and you'll see your document chunks — including the raw vector embeddings — stored exactly as you'd expect from any IRIS table.&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.amazonaws.com%2Fuploads%2Farticles%2Fmlxzcyc32rbrubyfzc9x.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.amazonaws.com%2Fuploads%2Farticles%2Fmlxzcyc32rbrubyfzc9x.png" alt=" " width="800" height="389"&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.amazonaws.com%2Fuploads%2Farticles%2F2qd6oy4lyhaoz3qqbhj6.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.amazonaws.com%2Fuploads%2Farticles%2F2qd6oy4lyhaoz3qqbhj6.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;The IRIS Advantage:&lt;/strong&gt; With most vector stores, embedding data is opaque — accessible only through a proprietary API. With IRIS, you have full SQL access. Query vector data directly, join it against operational data already in your namespace, inspect what's been indexed, or build custom retrieval logic on top of it. That's a meaningful capability for teams already invested in the IRIS ecosystem.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;What's Been Fixed Since the Initial Release&lt;/h2&gt;
&lt;p&gt;The initial integration shipped in v1.11.2 with some rough edges. All known issues have since been resolved and merged into the official Dify codebase.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;br&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;br&gt;
&lt;th&gt;PR&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;br&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;br&gt;
&lt;tbody&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;&lt;a href="https://github.com/langgenius/dify/pull/29480" rel="noopener noreferrer"&gt;#29480&lt;/a&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;v1.11.2&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Initial IRIS vector store support&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;&lt;a href="https://github.com/langgenius/dify/pull/31309" rel="noopener noreferrer"&gt;#31309&lt;/a&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;post-v1.11.2&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Fix full-text search and hybrid search for IRIS backend&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;&lt;a href="https://github.com/langgenius/dify/pull/31899" rel="noopener noreferrer"&gt;#31899&lt;/a&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;v1.12.1&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Fix IRIS data persistence across container recreation using Durable %SYS&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;&lt;a href="https://github.com/langgenius/dify/pull/31901" rel="noopener noreferrer"&gt;#31901&lt;/a&gt;&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;v1.12.1&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Further improvements to Durable %SYS data persistence&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/tbody&gt;
&lt;br&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note on Durable %SYS:&lt;/strong&gt; Without the fixes in &lt;a href="https://github.com/langgenius/dify/pull/31899" rel="noopener noreferrer"&gt;#31899&lt;/a&gt; and &lt;a href="https://github.com/langgenius/dify/pull/31901" rel="noopener noreferrer"&gt;#31901&lt;/a&gt;, IRIS data could be lost on container recreation — a common issue with the Community Edition Docker image. IRIS developers will recognize Durable %SYS immediately; these fixes ensure the Docker setup respects it correctly.&lt;/p&gt;&lt;/blockquote&gt;

&lt;h2&gt;Troubleshooting&lt;/h2&gt;
&lt;h3&gt;IRIS container fails to start on Windows&lt;/h3&gt;
&lt;p&gt;On Windows, the IRIS container may fail to start due to volume directory permission issues. Run the following from the &lt;code&gt;docker&lt;/code&gt; directory &lt;em&gt;before&lt;/em&gt; starting the containers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod -R 777 ./volumes/iris&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is a known limitation on Windows host environments. A fix to eliminate this step is planned for a future release.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3&gt;IRIS container fails to start (high core count)&lt;/h3&gt;
&lt;p&gt;On machines with high core counts, IRIS Community Edition's 20-core limit may be triggered. Add the following to the &lt;code&gt;iris&lt;/code&gt; service in &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:&lt;br&gt;&lt;br&gt;
  iris:&lt;br&gt;&lt;br&gt;
    cpuset: "0-19"&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Summary&lt;/h2&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;br&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;br&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;br&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;br&gt;
&lt;/tr&gt;&lt;/thead&gt;
&lt;br&gt;
&lt;tbody&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Enable IRIS in Dify&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Set &lt;code&gt;VECTOR_STORE=iris&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;&lt;br&gt;
&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Verify stored vectors&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Management Portal → SQL → schema: &lt;code&gt;dify&lt;/code&gt;&lt;br&gt;
&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Data persistence&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Handled by Durable %SYS (fixed in v1.12.1)&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;tr&gt;
&lt;br&gt;
&lt;td&gt;Community Edition&lt;/td&gt;
&lt;br&gt;
&lt;td&gt;Free · 10 GB data · Up to 20 cores&lt;/td&gt;
&lt;br&gt;
&lt;/tr&gt;
&lt;br&gt;
&lt;/tbody&gt;
&lt;br&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;A follow-up post will walk through building a full RAG chatbot on this stack. Questions or issues — drop them in the comments below.&lt;/p&gt;

&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;

&lt;li&gt;&lt;a href="https://docs.dify.ai" rel="noopener noreferrer"&gt;Dify Documentation&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/langgenius/dify" rel="noopener noreferrer"&gt;Dify GitHub&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://hub.docker.com/r/intersystemsdc/iris-community" rel="noopener noreferrer"&gt;IRIS Community Edition — Docker Hub&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://community.intersystems.com" rel="noopener noreferrer"&gt;InterSystems Developer Community&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>ai</category>
      <category>chatgpt</category>
      <category>python</category>
    </item>
    <item>
      <title>Step-by-Step Guide: Setting Up RAG for Gen AI Agents Using IRIS Vector DB in Python</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 29 Mar 2026 12:14:04 +0000</pubDate>
      <link>https://dev.to/intersystems/step-by-step-guide-setting-up-rag-for-gen-ai-agents-using-iris-vector-db-in-python-3b00</link>
      <guid>https://dev.to/intersystems/step-by-step-guide-setting-up-rag-for-gen-ai-agents-using-iris-vector-db-in-python-3b00</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;strong&gt;How to set up RAG for OpenAI agents using IRIS Vector DB in Python&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;In this article, I’ll walk you through an example of using InterSystems IRIS Vector DB to store embeddings and integrate them with an OpenAI agent.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;To demonstrate this, we’ll create an OpenAI agent with knowledge of InterSystems technology. We’ll achieve this by storing embeddings of some InterSystems documentation in IRIS and then using IRIS vector search to retrieve relevant content—enabling a Retrieval-Augmented Generation (RAG) workflow.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Note: &lt;/span&gt;&lt;span&gt;Section 1 details how process text into embeddings. If you are only interested in IRIS vector search you can skip ahead to Section 2.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&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;Section 1: Embedding Data&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Your embeddings are only as good as your data! To get the best results, you should prepare your data carefully. This may include:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;Cleaning the text (removing special characters or excess whitespace)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;Chunking the data into smaller pieces&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;Other preprocessing techniques&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;For this example, the documentation is stored in simple text files that require minimal cleaning. However, we will divide the text into chunks to enable more efficient and accurate RAG.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&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;Step 1: Chunking Text Files&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Chunking text into manageable pieces benefits RAG systems in two ways:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li value="1"&gt;&lt;span&gt;&lt;span&gt;More accurate retrieval – embeddings represent smaller, more specific sections of text.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;More efficient retrieval – less text per query reduces cost and improves performance.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;For this example, we’ll store the chunked text in Parquet files before uploading to IRIS (though you can use any approach, including direct upload).&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&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;Chunking Function&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;We’ll use RecursiveCharacterTextSplitter from langchain_text_splitters to split text strategically based on paragraph, sentence, and word boundaries.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;Chunk size: 300 tokens (larger chunks provide more context but increase retrieval cost)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;Chunk overlap: 50 tokens (helps maintain context across chunks)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;from&lt;/span&gt; langchain_text_splitters &lt;span class="mention"&gt;import&lt;/span&gt; RecursiveCharacterTextSplitter

&lt;p&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;chunk_text_by_tokens&lt;/span&gt;&lt;span&gt;(text: str, chunk_size: int, chunk_overlap: int)&lt;/span&gt; -&amp;gt; list[str]:&lt;br&gt;
    """&lt;br&gt;
    Chunk text prioritizing paragraph and sentence boundaries using&lt;br&gt;
    RecursiveCharacterTextSplitter. Returns a list of chunk strings.&lt;br&gt;
    """&lt;br&gt;
    splitter = RecursiveCharacterTextSplitter(&lt;br&gt;
        &lt;span&gt;# Prioritize larger semantic units first, then fall back to smaller ones&lt;/span&gt;&lt;br&gt;
        separators=[&lt;span&gt;"\n\n"&lt;/span&gt;, &lt;span&gt;"\n"&lt;/span&gt;, &lt;span&gt;". "&lt;/span&gt;, &lt;span&gt;" "&lt;/span&gt;, &lt;span&gt;""&lt;/span&gt;],&lt;br&gt;
        chunk_size=chunk_size,&lt;br&gt;
        chunk_overlap=chunk_overlap,&lt;br&gt;
        length_function=len,&lt;br&gt;
        is_separator_regex=&lt;span&gt;False&lt;/span&gt;,&lt;br&gt;
    )&lt;br&gt;
    &lt;span&gt;return&lt;/span&gt; splitter.split_text(text)&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Next, we’ll use the chunking function to process one text file at a time and apply a tiktoken encoder to calculate token counts and generate metadata. This metadata will be useful later when creating embeddings and storing them in IRIS.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;from&lt;/span&gt; pathlib &lt;span class="mention"&gt;import&lt;/span&gt; Path&lt;br&gt;
&lt;span class="mention"&gt;import&lt;/span&gt; tiktoken

&lt;p&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;chunk_file&lt;/span&gt;&lt;span&gt;(path: Path, chunk_size: int, chunk_overlap: int, encoding_name: str = &lt;/span&gt;&lt;span&gt;"cl100k_base"&lt;/span&gt;) -&amp;gt; list[dict]:&lt;br&gt;
    """&lt;br&gt;
    Read a file, split its contents into token-aware chunks, and return metadata for each chunk.&lt;br&gt;
    Returns a list of dicts with keys:&lt;br&gt;
    - filename&lt;br&gt;
    - relative_path&lt;br&gt;
    - absolute_path&lt;br&gt;
    - chunk_index&lt;br&gt;
    - chunk_text&lt;br&gt;
    - token_count&lt;br&gt;
    - modified_time&lt;br&gt;
    - size_bytes&lt;br&gt;
    """&lt;br&gt;
    p = Path(path)&lt;br&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;not&lt;/span&gt; p.exists() &lt;span&gt;or&lt;/span&gt; &lt;span&gt;not&lt;/span&gt; p.is_file():&lt;br&gt;
        &lt;span&gt;raise&lt;/span&gt; FileNotFoundError(&lt;span&gt;f"File not found: &lt;/span&gt;&lt;span&gt;{path}&lt;/span&gt;")&lt;br&gt;
    &lt;span&gt;try&lt;/span&gt;:&lt;br&gt;
        text = p.read_text(encoding=&lt;span&gt;"utf-8"&lt;/span&gt;, errors=&lt;span&gt;"replace"&lt;/span&gt;)&lt;br&gt;
    &lt;span&gt;except&lt;/span&gt; Exception &lt;span&gt;as&lt;/span&gt; e:&lt;br&gt;
        &lt;span&gt;raise&lt;/span&gt; RuntimeError(&lt;span&gt;f"Failed to read file &lt;/span&gt;&lt;span&gt;{p}&lt;/span&gt;: &lt;span&gt;{e}&lt;/span&gt;")&lt;br&gt;
    &lt;span&gt;# Prepare tokenizer for accurate token counts&lt;/span&gt;&lt;br&gt;
    &lt;span&gt;try&lt;/span&gt;:&lt;br&gt;
        encoding = tiktoken.get_encoding(encoding_name)&lt;br&gt;
    &lt;span&gt;except&lt;/span&gt; Exception &lt;span&gt;as&lt;/span&gt; e:&lt;br&gt;
        &lt;span&gt;raise&lt;/span&gt; ValueError(&lt;span&gt;f"Invalid encoding name '&lt;/span&gt;&lt;span&gt;{encoding_name}&lt;/span&gt;': &lt;span&gt;{e}&lt;/span&gt;")&lt;br&gt;
    &lt;span&gt;# Create chunks using provided chunker&lt;/span&gt;&lt;br&gt;
    chunks = chunk_text_by_tokens(text, chunk_size, chunk_overlap)&lt;br&gt;
    &lt;span&gt;# File metadata&lt;/span&gt;&lt;br&gt;
    stat = p.stat()&lt;br&gt;
    &lt;span&gt;from&lt;/span&gt; datetime &lt;span&gt;import&lt;/span&gt; datetime, timezone&lt;br&gt;
    modified_time = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()&lt;br&gt;
    absolute_path = str(p.resolve())&lt;br&gt;
    &lt;span&gt;try&lt;/span&gt;:&lt;br&gt;
        relative_path = str(p.resolve().relative_to(Path.cwd()))&lt;br&gt;
    &lt;span&gt;except&lt;/span&gt; Exception:&lt;br&gt;
        relative_path = p.name&lt;br&gt;
    &lt;span&gt;# Build rows&lt;/span&gt;&lt;br&gt;
    rows: list[dict] = []&lt;br&gt;
    &lt;span&gt;for&lt;/span&gt; idx, chunk &lt;span&gt;in&lt;/span&gt; enumerate(chunks):&lt;br&gt;
        token_count = len(encoding.encode(chunk))&lt;br&gt;
        rows.append({&lt;br&gt;
            &lt;span&gt;"filename"&lt;/span&gt;: p.name,&lt;br&gt;
            &lt;span&gt;"relative_path"&lt;/span&gt;: relative_path,&lt;br&gt;
            &lt;span&gt;"absolute_path"&lt;/span&gt;: absolute_path,&lt;br&gt;
            &lt;span&gt;"chunk_index"&lt;/span&gt;: idx,&lt;br&gt;
            &lt;span&gt;"chunk_text"&lt;/span&gt;: chunk,&lt;br&gt;
            &lt;span&gt;"token_count"&lt;/span&gt;: token_count,&lt;br&gt;
            &lt;span&gt;"modified_time"&lt;/span&gt;: modified_time,&lt;br&gt;
            &lt;span&gt;"size_bytes"&lt;/span&gt;: stat.st_size,&lt;br&gt;
        })&lt;br&gt;
    &lt;span&gt;return&lt;/span&gt; rows&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;strong&gt;Step 2: Creating embeddings&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;You can generate embeddings using cloud providers (e.g., OpenAI) or local models via Ollama (e.g., nomic-embed-text). In this example, we’ll use OpenAI’s text-embedding-3-small model to embed each chunk and save the results back to Parquet for later ingestion into IRIS Vector DB.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;from&lt;/span&gt; openai &lt;span class="mention"&gt;import&lt;/span&gt; OpenAI&lt;br&gt;
&lt;span class="mention"&gt;import&lt;/span&gt; pandas &lt;span class="mention"&gt;as&lt;/span&gt; pd

&lt;p&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;embed_and_save_parquet&lt;/span&gt;&lt;span&gt;(input_parquet_path: str, output_parquet_path: str)&lt;/span&gt;:&lt;br&gt;
    """&lt;br&gt;
    Loads a Parquet file, creates embeddings for the 'chunk_text' column using &lt;br&gt;
    OpenAI's small embedding model, and saves the result to a new Parquet file.&lt;br&gt;
    Args:&lt;br&gt;
        input_parquet_path (str): Path to the input Parquet file containing 'chunk_text'.&lt;br&gt;
        output_parquet_path (str): Path to save the new Parquet file with embeddings.&lt;br&gt;
        openai_api_key (str): Your OpenAI API key.&lt;br&gt;
    """&lt;br&gt;
    key = os.getenv(&lt;span&gt;"OPENAI_API_KEY"&lt;/span&gt;)&lt;br&gt;
    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;not&lt;/span&gt; key:&lt;br&gt;
        print(&lt;span&gt;"ERROR: OPENAI_API_KEY environment variable is not set."&lt;/span&gt;, file=sys.stderr)&lt;br&gt;
        sys.exit(&lt;span&gt;1&lt;/span&gt;)&lt;br&gt;
    &lt;span&gt;try&lt;/span&gt;:&lt;br&gt;
        &lt;span&gt;# Load the Parquet file&lt;/span&gt;&lt;br&gt;
        df = pd.read_parquet(input_parquet_path)&lt;br&gt;
        &lt;span&gt;# Initialize OpenAI client&lt;/span&gt;&lt;br&gt;
        client = OpenAI(api_key=key)&lt;br&gt;
        &lt;span&gt;# Generate embeddings for each chunk_text&lt;/span&gt;&lt;br&gt;
        embeddings = []&lt;br&gt;
        &lt;span&gt;for&lt;/span&gt; text &lt;span&gt;in&lt;/span&gt; df[&lt;span&gt;'chunk_text'&lt;/span&gt;]:&lt;br&gt;
            response = client.embeddings.create(&lt;br&gt;
                input=text,&lt;br&gt;
                model=&lt;span&gt;"text-embedding-3-small"&lt;/span&gt;  &lt;span&gt;# Using the small embedding model&lt;/span&gt;&lt;br&gt;
            )&lt;br&gt;
            embeddings.append(response.data[&lt;span&gt;0&lt;/span&gt;].embedding)&lt;br&gt;
        &lt;span&gt;# Add embeddings to the DataFrame&lt;/span&gt;&lt;br&gt;
        df[&lt;span&gt;'embedding'&lt;/span&gt;] = embeddings&lt;br&gt;
        &lt;span&gt;# Save the new DataFrame to a Parquet file&lt;/span&gt;&lt;br&gt;
        df.to_parquet(output_parquet_path, index=&lt;span&gt;False&lt;/span&gt;)&lt;br&gt;
        print(&lt;span&gt;f"Embeddings generated and saved to &lt;/span&gt;&lt;span&gt;{output_parquet_path}&lt;/span&gt;")&lt;br&gt;
    &lt;span&gt;except&lt;/span&gt; FileNotFoundError:&lt;br&gt;
        print(&lt;span&gt;f"Error: Input file not found at &lt;/span&gt;&lt;span&gt;{input_parquet_path}&lt;/span&gt;")&lt;br&gt;
    &lt;span&gt;except&lt;/span&gt; KeyError:&lt;br&gt;
        print(&lt;span&gt;"Error: 'chunk_text' column not found in the input Parquet file."&lt;/span&gt;)&lt;br&gt;
    &lt;span&gt;except&lt;/span&gt; Exception &lt;span&gt;as&lt;/span&gt; e:&lt;br&gt;
        print(&lt;span&gt;f"An unexpected error occurred: &lt;/span&gt;&lt;span&gt;{e}&lt;/span&gt;")&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;strong&gt;Step 3: Put the data processing together&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Now it’s time to run the pipeline. In this example, we’ll load and chunk the Business Service documentation, generate embeddings, and write the results to Parquet for IRIS ingestion.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CHUNK_SIZE_TOKENS = &lt;span class="mention"&gt;300&lt;/span&gt;&lt;br&gt;
    CHUNK_OVERLAP_TOKENS = &lt;span class="mention"&gt;50&lt;/span&gt;&lt;br&gt;
    ENCODING_NAME=&lt;span class="mention"&gt;"cl100k_base"&lt;/span&gt;&lt;br&gt;
    current_file_path = Path(&lt;strong&gt;file&lt;/strong&gt;).resolve()
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load_documentation_to_parquet(input_dir=current_file_path.parent / &amp;lt;span class="mention"&amp;gt;"Documentation"&amp;lt;/span&amp;gt; / &amp;lt;span class="mention"&amp;gt;"BusinessService"&amp;lt;/span&amp;gt;, 
                              output_file=current_file_path.parent / &amp;lt;span class="mention"&amp;gt;"BusinessService.parquet"&amp;lt;/span&amp;gt;, 
                              chunk_size=CHUNK_SIZE_TOKENS, 
                              chunk_overlap=CHUNK_OVERLAP_TOKENS, 
                              encoding_name=ENCODING_NAME)
embed_and_save_parquet(input_parquet_path=current_file_path.parent / &amp;lt;span class="mention"&amp;gt;"BusinessService.parquet"&amp;lt;/span&amp;gt;, 
                        output_parquet_path=current_file_path.parent / &amp;lt;span class="mention"&amp;gt;"BusinessService_embedded.parquet"&amp;lt;/span&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;A row in our final business service parquet file will look something like this:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&lt;span class="mention"&gt;"filename"&lt;/span&gt;:&lt;span class="mention"&gt;"FileInboundAdapters.txt"&lt;/span&gt;,&lt;span class="mention"&gt;"relative_path"&lt;/span&gt;:&lt;span class="mention"&gt;"Documentation\BusinessService\Adapters\FileInboundAdapters.txt"&lt;/span&gt;,&lt;span class="mention"&gt;"absolute_path"&lt;/span&gt;:&lt;span class="mention"&gt;"C:\Users\…\Documentation\BusinessService\Adapters\FileInboundAdapters.txt"&lt;/span&gt;,&lt;span class="mention"&gt;"chunk_index"&lt;/span&gt;:&lt;span class="mention"&gt;0&lt;/span&gt;,&lt;span class="mention"&gt;"chunk_text"&lt;/span&gt;:&lt;span class="mention"&gt;"Settings for the File Inbound Adapter\nProvides reference information for settings of the file inbound adapter, EnsLib.File.InboundAdapterOpens in a new tab. You can configure these settings after you have added a business service that uses this adapter to your production.\nSummary"&lt;/span&gt;,&lt;span class="mention"&gt;"token_count"&lt;/span&gt;:&lt;span class="mention"&gt;52&lt;/span&gt;,&lt;span class="mention"&gt;"modified_time"&lt;/span&gt;:&lt;span class="mention"&gt;"2025-11-25T18:34:16.120336+00:00"&lt;/span&gt;,&lt;span class="mention"&gt;"size_bytes"&lt;/span&gt;:&lt;span class="mention"&gt;13316&lt;/span&gt;,&lt;span class="mention"&gt;"embedding"&lt;/span&gt;:[&lt;span class="mention"&gt;-0.02851865254342556&lt;/span&gt;,&lt;span class="mention"&gt;0.01860344596207142&lt;/span&gt;,…,&lt;span class="mention"&gt;0.0135544464207155&lt;/span&gt;]}&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;strong&gt;Section 2: Using IRIS Vector Search&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&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;Step 4: Upload Your Embeddings to IRIS&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Choose the IRIS namespace and table name you’ll use to store embeddings. (The script below will create the table if it doesn’t already exist.) Then use the InterSystems IRIS Python DB-API driver to insert the chunks and their embeddings.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;The function below reads a Parquet file containing chunk text and embeddings, normalizes the embedding column to a JSON-serializable list of floats, connects to IRIS, creates the destination table if it doesn’t exist (with a VECTOR(FLOAT, 1536) column, where 1536 is the number of dimensions in the embedding), and then inserts each row using TO_VECTOR(?) in a parameterized SQL statement. It commits the transaction on success, logs progress, and cleans up the connection, rolling back on database errors.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;import&lt;/span&gt; iris  &lt;span class="mention"&gt;# The InterSystems IRIS Python DB-API driver &lt;/span&gt;&lt;br&gt;
&lt;span class="mention"&gt;import&lt;/span&gt; pandas &lt;span class="mention"&gt;as&lt;/span&gt; pd&lt;br&gt;
&lt;span class="mention"&gt;import&lt;/span&gt; numpy &lt;span class="mention"&gt;as&lt;/span&gt; np&lt;br&gt;
&lt;span class="mention"&gt;import&lt;/span&gt; json&lt;br&gt;
&lt;span class="mention"&gt;from&lt;/span&gt; pathlib &lt;span class="mention"&gt;import&lt;/span&gt; Path

&lt;p&gt;&lt;span&gt;# --- Configuration ---&lt;/span&gt;&lt;br&gt;
PARQUET_FILE_PATH = &lt;span&gt;"your_embeddings.parquet"&lt;/span&gt;&lt;br&gt;
IRIS_HOST = &lt;span&gt;"localhost"&lt;/span&gt;&lt;br&gt;
IRIS_PORT = &lt;span&gt;8881&lt;/span&gt;&lt;br&gt;
IRIS_NAMESPACE = &lt;span&gt;"VECTOR"&lt;/span&gt;&lt;br&gt;
IRIS_USERNAME = &lt;span&gt;"superuser"&lt;/span&gt;&lt;br&gt;
IRIS_PASSWORD = &lt;span&gt;"sys"&lt;/span&gt;&lt;br&gt;
TABLE_NAME = &lt;span&gt;"AIDemo.Embeddings"&lt;/span&gt; &lt;span&gt;# Must match the table created in IRIS&lt;/span&gt;&lt;br&gt;
EMBEDDING_DIMENSIONS = &lt;span&gt;1536&lt;/span&gt; &lt;span&gt;# Must match the dimensions for the embeddings you used&lt;/span&gt;&lt;br&gt;
&lt;span&gt;def&lt;/span&gt; &lt;span&gt;upload_embeddings_to_iris&lt;/span&gt;&lt;span&gt;(parquet_path: str)&lt;/span&gt;:&lt;br&gt;
    """&lt;br&gt;
    Reads a Parquet file with 'chunk_text' and 'embedding' columns &lt;br&gt;
    and uploads them to an InterSystems IRIS vector database table.&lt;br&gt;
    """&lt;br&gt;
    &lt;span&gt;# 1. Load data from the Parquet file using pandas&lt;/span&gt;&lt;br&gt;
    &lt;span&gt;try&lt;/span&gt;:&lt;br&gt;
        df = pd.read_parquet(parquet_path)&lt;br&gt;
        &lt;span&gt;if&lt;/span&gt; &lt;span&gt;'chunk_text'&lt;/span&gt; &lt;span&gt;not&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; df.columns &lt;span&gt;or&lt;/span&gt; &lt;span&gt;'embedding'&lt;/span&gt; &lt;span&gt;not&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; df.columns:&lt;br&gt;
            print(&lt;span&gt;"Error: Parquet file must contain 'chunk_text' and 'embedding' columns."&lt;/span&gt;)&lt;br&gt;
            &lt;span&gt;return&lt;/span&gt;&lt;br&gt;
    &lt;span&gt;except&lt;/span&gt; FileNotFoundError:&lt;br&gt;
        print(&lt;span&gt;f"Error: The file at &lt;/span&gt;&lt;span&gt;{parquet_path}&lt;/span&gt; was not found.")&lt;br&gt;
        &lt;span&gt;return&lt;/span&gt;&lt;br&gt;
    &lt;span&gt;# Ensure embeddings are in a format compatible with TO_VECTOR function (list of floats)&lt;/span&gt;&lt;br&gt;
    &lt;span&gt;# Parquet often saves numpy arrays as lists&lt;/span&gt;&lt;br&gt;
    &lt;span&gt;if&lt;/span&gt; isinstance(df[&lt;span&gt;'embedding'&lt;/span&gt;].iloc[&lt;span&gt;0&lt;/span&gt;], np.ndarray):&lt;br&gt;
        df[&lt;span&gt;'embedding'&lt;/span&gt;] = df[&lt;span&gt;'embedding'&lt;/span&gt;].apply(&lt;span&gt;lambda&lt;/span&gt; x: x.tolist())&lt;br&gt;
    print(&lt;span&gt;f"Loaded &lt;/span&gt;&lt;span&gt;{len(df)}&lt;/span&gt; records from &lt;span&gt;{parquet_path}&lt;/span&gt;.")&lt;br&gt;
    &lt;span&gt;# 2. Establish connection to InterSystems IRIS&lt;/span&gt;&lt;br&gt;
    connection = &lt;span&gt;None&lt;/span&gt;&lt;br&gt;
    &lt;span&gt;try&lt;/span&gt;:&lt;br&gt;
        conn_string = &lt;span&gt;f"&lt;/span&gt;&lt;span&gt;{IRIS_HOST}&lt;/span&gt;:&lt;span&gt;{IRIS_PORT}&lt;/span&gt;/&lt;span&gt;{IRIS_NAMESPACE}&lt;/span&gt;"&lt;br&gt;
        connection = iris.connect(conn_string, IRIS_USERNAME, IRIS_PASSWORD)&lt;br&gt;
        cursor = connection.cursor()&lt;br&gt;
        print(&lt;span&gt;"Successfully connected to InterSystems IRIS."&lt;/span&gt;)&lt;br&gt;
        &lt;span&gt;# Create embedding table if it doesn't exist&lt;/span&gt;&lt;br&gt;
        cursor.execute(f"""&lt;br&gt;
            CREATE TABLE IF NOT EXISTS  &lt;span&gt;{TABLE_NAME}&lt;/span&gt; (&lt;br&gt;
            ID INTEGER IDENTITY PRIMARY KEY,&lt;br&gt;
            chunk_text VARCHAR(2500), embedding VECTOR(FLOAT, &lt;span&gt;{EMBEDDING_DIMENSIONS}&lt;/span&gt;)&lt;br&gt;
            )"""&lt;br&gt;
        )&lt;br&gt;
        &lt;span&gt;# 3. Prepare the SQL INSERT statement&lt;/span&gt;&lt;br&gt;
        &lt;span&gt;# InterSystems IRIS uses the TO_VECTOR function for inserting vector data via SQL&lt;/span&gt;&lt;br&gt;
        insert_sql = f"""&lt;br&gt;
        INSERT INTO &lt;span&gt;{TABLE_NAME}&lt;/span&gt; (chunk_text, embedding) &lt;br&gt;
        VALUES (?, TO_VECTOR(?))&lt;br&gt;
        """&lt;br&gt;
        &lt;span&gt;# 4. Iterate and insert data&lt;/span&gt;&lt;br&gt;
        count = &lt;span&gt;0&lt;/span&gt;&lt;br&gt;
        &lt;span&gt;for&lt;/span&gt; index, row &lt;span&gt;in&lt;/span&gt; df.iterrows():&lt;br&gt;
            text = row[&lt;span&gt;'chunk_text'&lt;/span&gt;]&lt;br&gt;
            &lt;span&gt;# Convert the list of floats to a JSON string, which is required by TO_VECTOR when using DB-API&lt;/span&gt;&lt;br&gt;
            vector_json_str = json.dumps(row[&lt;span&gt;'embedding'&lt;/span&gt;]) &lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        cursor.execute(insert_sql, (text, vector_json_str))
        count += &amp;lt;span class="mention"&amp;gt;1&amp;lt;/span&amp;gt;
        &amp;lt;span class="mention"&amp;gt;if&amp;lt;/span&amp;gt; count % &amp;lt;span class="mention"&amp;gt;100&amp;lt;/span&amp;gt; == &amp;lt;span class="mention"&amp;gt;0&amp;lt;/span&amp;gt;:
            print(&amp;lt;span class="mention"&amp;gt;f"Inserted &amp;lt;/span&amp;gt;&amp;lt;span class="mention"&amp;gt;{count}&amp;lt;/span&amp;gt; rows...")

    &amp;lt;span class="mention"&amp;gt;# Commit the transaction&amp;lt;/span&amp;gt;
    connection.commit()
    print(&amp;lt;span class="mention"&amp;gt;f"Data upload complete. Total rows inserted: &amp;lt;/span&amp;gt;&amp;lt;span class="mention"&amp;gt;{count}&amp;lt;/span&amp;gt;.")
&amp;lt;span class="mention"&amp;gt;except&amp;lt;/span&amp;gt; iris.DBAPIError &amp;lt;span class="mention"&amp;gt;as&amp;lt;/span&amp;gt; e:
    print(&amp;lt;span class="mention"&amp;gt;f"A database error occurred: &amp;lt;/span&amp;gt;&amp;lt;span class="mention"&amp;gt;{e}&amp;lt;/span&amp;gt;")
    &amp;lt;span class="mention"&amp;gt;if&amp;lt;/span&amp;gt; connection:
        connection.rollback()
&amp;lt;span class="mention"&amp;gt;except&amp;lt;/span&amp;gt; Exception &amp;lt;span class="mention"&amp;gt;as&amp;lt;/span&amp;gt; e:
    print(&amp;lt;span class="mention"&amp;gt;f"An unexpected error occurred: &amp;lt;/span&amp;gt;&amp;lt;span class="mention"&amp;gt;{e}&amp;lt;/span&amp;gt;")
&amp;lt;span class="mention"&amp;gt;finally&amp;lt;/span&amp;gt;:
    &amp;lt;span class="mention"&amp;gt;if&amp;lt;/span&amp;gt; connection:
        connection.close()
        print(&amp;lt;span class="mention"&amp;gt;"Database connection closed."&amp;lt;/span&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Example usage:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;current_file_path = Path(&lt;strong&gt;file&lt;/strong&gt;).resolve()&lt;br&gt;
    upload_embeddings_to_iris(current_file_path.parent / &lt;span class="mention"&gt;"BusinessService_embedded.parquet"&lt;/span&gt;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;strong&gt;Step 5: Create your embedding search functionality&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Next, we’ll create a search function that embeds the user’s query, runs a vector similarity search in IRIS via the Python DB&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;‑&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;API, and returns the top&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;‑&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;k matching chunks from our embeddings table.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;The example function below reads a Parquet file containing text chunks and their corresponding embeddings, then uploads this data into the InterSystems IRIS vector storage table. It first validates the Parquet file and normalizes the embedding format into a JSON array string compatible with IRIS’s TO_VECTOR function. After establishing a connection to IRIS, the function creates the target table if it does not exist, prepares a parameterized SQL INSERT statement, and iterates through each row to insert the chunk text and embedding. Finally, it commits the transaction, logs progress, and ensures proper error handling and cleanup of the database connection.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;import&lt;/span&gt; iris&lt;br&gt;
&lt;span class="mention"&gt;from&lt;/span&gt; typing &lt;span class="mention"&gt;import&lt;/span&gt; List&lt;br&gt;
&lt;span class="mention"&gt;import&lt;/span&gt; os&lt;br&gt;
&lt;span class="mention"&gt;from&lt;/span&gt; openai &lt;span class="mention"&gt;import&lt;/span&gt; OpenAI

&lt;p&gt;&lt;span&gt;# --- Configuration ---&lt;/span&gt;&lt;br&gt;
PARQUET_FILE_PATH = &lt;span&gt;"your_embeddings.parquet"&lt;/span&gt;&lt;br&gt;
IRIS_HOST = &lt;span&gt;"localhost"&lt;/span&gt;&lt;br&gt;
IRIS_PORT = &lt;span&gt;8881&lt;/span&gt;&lt;br&gt;
IRIS_NAMESPACE = &lt;span&gt;"VECTOR"&lt;/span&gt;&lt;br&gt;
IRIS_USERNAME = &lt;span&gt;"superuser"&lt;/span&gt;&lt;br&gt;
IRIS_PASSWORD = &lt;span&gt;"sys"&lt;/span&gt;&lt;br&gt;
TABLE_NAME = &lt;span&gt;"AIDemo.Embeddings"&lt;/span&gt; &lt;span&gt;# Must match the table created in IRIS&lt;/span&gt;&lt;br&gt;
EMBEDDING_DIMENSIONS = &lt;span&gt;1536&lt;/span&gt;&lt;br&gt;
MODEL = &lt;span&gt;"text-embedding-3-small"&lt;/span&gt;&lt;br&gt;
&lt;span&gt;def&lt;/span&gt; &lt;span&gt;get_embedding&lt;/span&gt;&lt;span&gt;(text: str, model: str, client)&lt;/span&gt; -&amp;gt; List[float]:&lt;br&gt;
    &lt;span&gt;# Normalize newlines and coerce to str&lt;/span&gt;&lt;br&gt;
    payload = [(&lt;span&gt;""&lt;/span&gt; &lt;span&gt;if&lt;/span&gt; text &lt;span&gt;is&lt;/span&gt; &lt;span&gt;None&lt;/span&gt; &lt;span&gt;else&lt;/span&gt; str(text)).replace(&lt;span&gt;"\n"&lt;/span&gt;, &lt;span&gt;" "&lt;/span&gt;) &lt;span&gt;for&lt;/span&gt; _ &lt;span&gt;in&lt;/span&gt; range(&lt;span&gt;1&lt;/span&gt;)]&lt;br&gt;
    resp = client.embeddings.create(model=model, input=payload, encoding_format=&lt;span&gt;"float"&lt;/span&gt;)&lt;br&gt;
    &lt;span&gt;return&lt;/span&gt; resp.data[&lt;span&gt;0&lt;/span&gt;].embedding&lt;br&gt;
&lt;span&gt;def&lt;/span&gt; &lt;span&gt;search_embeddings&lt;/span&gt;&lt;span&gt;(search: str, top_k: int)&lt;/span&gt;:&lt;br&gt;
    print(&lt;span&gt;"-------RAG--------"&lt;/span&gt;)&lt;br&gt;
    print(&lt;span&gt;f"Searching IRIS vector store for: "&lt;/span&gt;, search)&lt;br&gt;
    key = os.getenv(&lt;span&gt;"OPENAI_API_KEY"&lt;/span&gt;)&lt;br&gt;
    client = OpenAI(api_key=key)&lt;br&gt;
 &lt;span&gt;# 2. Establish connection to InterSystems IRIS&lt;/span&gt;&lt;br&gt;
    connection = &lt;span&gt;None&lt;/span&gt;&lt;br&gt;
    &lt;span&gt;try&lt;/span&gt;:&lt;br&gt;
        conn_string = &lt;span&gt;f"&lt;/span&gt;&lt;span&gt;{IRIS_HOST}&lt;/span&gt;:&lt;span&gt;{IRIS_PORT}&lt;/span&gt;/&lt;span&gt;{IRIS_NAMESPACE}&lt;/span&gt;"&lt;br&gt;
        connection = iris.connect(conn_string, IRIS_USERNAME, IRIS_PASSWORD)&lt;br&gt;
        cursor = connection.cursor()&lt;br&gt;
        print(&lt;span&gt;"Successfully connected to InterSystems IRIS."&lt;/span&gt;)&lt;br&gt;
        &lt;span&gt;# Embed query for searching&lt;/span&gt;&lt;br&gt;
        &lt;span&gt;#emb_raw = str(test_embedding) # FOR TESTING&lt;/span&gt;&lt;br&gt;
        emb_raw = get_embedding(search, model=MODEL, client=client)&lt;br&gt;
        emb_raw = str(emb_raw)&lt;br&gt;
        &lt;span&gt;#print("EMB_RAW:", emb_raw)&lt;/span&gt;&lt;br&gt;
        emb_values = []&lt;br&gt;
        &lt;span&gt;for&lt;/span&gt; x &lt;span&gt;in&lt;/span&gt; emb_raw.replace(&lt;span&gt;'['&lt;/span&gt;, &lt;span&gt;''&lt;/span&gt;).replace(&lt;span&gt;']'&lt;/span&gt;, &lt;span&gt;''&lt;/span&gt;).split(&lt;span&gt;','&lt;/span&gt;):&lt;br&gt;
            &lt;span&gt;try&lt;/span&gt;:&lt;br&gt;
                emb_values.append(str(float(x.strip())))&lt;br&gt;
            &lt;span&gt;except&lt;/span&gt; ValueError:&lt;br&gt;
                &lt;span&gt;continue&lt;/span&gt;&lt;br&gt;
        emb_str = &lt;span&gt;", "&lt;/span&gt;.join(emb_values)&lt;br&gt;
        &lt;span&gt;# Prepare the SQL SELECT statement&lt;/span&gt;&lt;br&gt;
        search_sql = f"""&lt;br&gt;
        SELECT TOP &lt;span&gt;{top_k}&lt;/span&gt; ID, chunk_text FROM &lt;span&gt;{TABLE_NAME}&lt;/span&gt;&lt;br&gt;
        ORDER BY VECTOR_DOT_PRODUCT((embedding), TO_VECTOR(('&lt;span&gt;{emb_str}&lt;/span&gt;'), FLOAT)) DESC&lt;br&gt;
        """&lt;br&gt;
        cursor.execute(search_sql)&lt;br&gt;
        results = []&lt;br&gt;
        row = cursor.fetchone()&lt;br&gt;
        &lt;span&gt;while&lt;/span&gt; row &lt;span&gt;is&lt;/span&gt; &lt;span&gt;not&lt;/span&gt; &lt;span&gt;None&lt;/span&gt;:&lt;br&gt;
            results.append(row[:])&lt;br&gt;
            row = cursor.fetchone()&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;span class="mention"&amp;gt;except&amp;lt;/span&amp;gt; iris.DBAPIError &amp;lt;span class="mention"&amp;gt;as&amp;lt;/span&amp;gt; e:
    print(&amp;lt;span class="mention"&amp;gt;f"A database error occurred: &amp;lt;/span&amp;gt;&amp;lt;span class="mention"&amp;gt;{e}&amp;lt;/span&amp;gt;")
    &amp;lt;span class="mention"&amp;gt;if&amp;lt;/span&amp;gt; connection:
        connection.rollback()
&amp;lt;span class="mention"&amp;gt;except&amp;lt;/span&amp;gt; Exception &amp;lt;span class="mention"&amp;gt;as&amp;lt;/span&amp;gt; e:
    print(&amp;lt;span class="mention"&amp;gt;f"An unexpected error occurred: &amp;lt;/span&amp;gt;&amp;lt;span class="mention"&amp;gt;{e}&amp;lt;/span&amp;gt;")
&amp;lt;span class="mention"&amp;gt;finally&amp;lt;/span&amp;gt;:
    &amp;lt;span class="mention"&amp;gt;if&amp;lt;/span&amp;gt; connection:
        connection.close()
        print(&amp;lt;span class="mention"&amp;gt;"Database connection closed."&amp;lt;/span&amp;gt;)
    print(&amp;lt;span class="mention"&amp;gt;"------------RAG Finished-------------"&amp;lt;/span&amp;gt;)
    &amp;lt;span class="mention"&amp;gt;return&amp;lt;/span&amp;gt; results
&lt;/code&gt;&lt;/pre&gt;


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;strong&gt;Step 6: Add RAG context to your agent&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Now that you’ve:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li&gt;&lt;span&gt;&lt;span&gt;Chunked and embedded your documentation,&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;

&lt;li&gt;&lt;span&gt;&lt;span&gt;Uploaded embeddings to IRIS and created a vector index,&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;

&lt;li&gt;&lt;span&gt;&lt;span&gt;Built a search function for IRIS vector queries,&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;

&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;it’s time to put it all together into an interactive Retrieval-Augmented Generation (RAG) chat using the OpenAI Responses API. For this example we will give the agent access to the search function directly (for more fine-grained control of the agent), but this can also be done using a library like langchain as well.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;First, you will need to create your instructions for the agent, making sure give it access to the search function:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;import&lt;/span&gt; os&lt;br&gt;&lt;br&gt;
&lt;span class="mention"&gt;# ---------------------------- Configuration ----------------------------&lt;/span&gt;&lt;br&gt;&lt;br&gt;
MODEL = os.getenv(&lt;span class="mention"&gt;"OPENAI_RESPONSES_MODEL"&lt;/span&gt;, &lt;span class="mention"&gt;"gpt-5-nano"&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
SYSTEM_INSTRUCTIONS = (&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;"You are a helpful assistant that answers questions about InterSystems "&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;"business services and related integration capabilities. You have access "&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;"to a vector database of documentation chunks about business services. "&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;"\n\n"&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;"Use the &lt;code&gt;search_business_docs&lt;/code&gt; tool whenever the user asks about specific "&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;"settings, configuration options, or how to perform tasks with business "&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;"services. Ground your answers in the retrieved context, quoting or "&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;"summarizing relevant chunks. If nothing relevant is found, say so "&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;"clearly and answer from your general knowledge with a disclaimer."&lt;/span&gt;&lt;br&gt;&lt;br&gt;
)


&lt;p&gt;&lt;span&gt;# ---------------------------- Tool Definition ----------------------------&lt;/span&gt;&lt;br&gt;&lt;br&gt;
TOOLS = [&lt;br&gt;&lt;br&gt;
    {&lt;br&gt;&lt;br&gt;
        &lt;span&gt;"type"&lt;/span&gt;: &lt;span&gt;"function"&lt;/span&gt;,&lt;br&gt;&lt;br&gt;
        &lt;span&gt;"name"&lt;/span&gt;: &lt;span&gt;"search_business_docs"&lt;/span&gt;,&lt;br&gt;&lt;br&gt;
        &lt;span&gt;"description"&lt;/span&gt;: (&lt;br&gt;&lt;br&gt;
            &lt;span&gt;"Searches a vector database of documentation chunks related to "&lt;/span&gt;&lt;br&gt;&lt;br&gt;
            &lt;span&gt;"business services and returns the most relevant snippets."&lt;/span&gt;&lt;br&gt;&lt;br&gt;
        ),&lt;br&gt;&lt;br&gt;
        &lt;span&gt;"parameters"&lt;/span&gt;: {&lt;br&gt;&lt;br&gt;
            &lt;span&gt;"type"&lt;/span&gt;: &lt;span&gt;"object"&lt;/span&gt;,&lt;br&gt;&lt;br&gt;
            &lt;span&gt;"properties"&lt;/span&gt;: {&lt;br&gt;&lt;br&gt;
                &lt;span&gt;"query"&lt;/span&gt;: {&lt;br&gt;&lt;br&gt;
                    &lt;span&gt;"type"&lt;/span&gt;: &lt;span&gt;"string"&lt;/span&gt;,&lt;br&gt;&lt;br&gt;
                    &lt;span&gt;"description"&lt;/span&gt;: (&lt;br&gt;&lt;br&gt;
                        &lt;span&gt;"Natural language search query describing what you want "&lt;/span&gt;&lt;br&gt;&lt;br&gt;
                        &lt;span&gt;"to know about business services."&lt;/span&gt;&lt;br&gt;&lt;br&gt;
                    ),&lt;br&gt;&lt;br&gt;
                },&lt;br&gt;&lt;br&gt;
                &lt;span&gt;"top_k"&lt;/span&gt;: {&lt;br&gt;&lt;br&gt;
                    &lt;span&gt;"type"&lt;/span&gt;: &lt;span&gt;"integer"&lt;/span&gt;,&lt;br&gt;&lt;br&gt;
                    &lt;span&gt;"description"&lt;/span&gt;: (&lt;br&gt;&lt;br&gt;
                        &lt;span&gt;"Maximum number of results to retrieve from the vector DB."&lt;/span&gt;&lt;br&gt;&lt;br&gt;
                    ),&lt;br&gt;&lt;br&gt;
                    &lt;span&gt;"minimum"&lt;/span&gt;: &lt;span&gt;1&lt;/span&gt;,&lt;br&gt;&lt;br&gt;
                    &lt;span&gt;"maximum"&lt;/span&gt;: &lt;span&gt;10&lt;/span&gt;,&lt;br&gt;&lt;br&gt;
                },&lt;br&gt;&lt;br&gt;
            },&lt;br&gt;&lt;br&gt;
            &lt;span&gt;"required"&lt;/span&gt;: [&lt;span&gt;"query"&lt;/span&gt;, &lt;span&gt;"top_k"&lt;/span&gt;],&lt;br&gt;&lt;br&gt;
            &lt;span&gt;"additionalProperties"&lt;/span&gt;: &lt;span&gt;False&lt;/span&gt;,&lt;br&gt;&lt;br&gt;
        },&lt;br&gt;&lt;br&gt;
        &lt;span&gt;"strict"&lt;/span&gt;: &lt;span&gt;True&lt;/span&gt;,&lt;br&gt;&lt;br&gt;
    }&lt;br&gt;&lt;br&gt;
]&lt;br&gt;&lt;br&gt;
&lt;/p&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Now we need a small “router” method to let the model actually use our RAG tool.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;call_rag_tool(name, args) receives a function call emitted by the OpenAI Responses API and routes it to our local implementation (the search_business_docs tool that wraps Search.search_embeddings). It takes the model’s query and top_k, runs the IRIS vector search, and returns a JSON&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;‑&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;encoded payload of the top matches (IDs and text snippets). This stringified JSON is important because the Responses API expects tool outputs as strings; by formatting the results predictably, we make it easy for the model to ground its final answer in the retrieved documentation. If an unknown tool name is requested, the function returns an error payload so the model can handle it gracefully.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;def&lt;/span&gt; &lt;span class="mention"&gt;call_rag_tool&lt;/span&gt;&lt;span class="mention"&gt;(name: str, args: Dict[str, Any])&lt;/span&gt; -&amp;gt; str:&lt;br&gt;&lt;br&gt;
    """Route function calls from the model to our local Python implementations.&lt;br&gt;&lt;br&gt;
    Currently only supports the &lt;code&gt;search_business_docs&lt;/code&gt; tool, which wraps&lt;br&gt;&lt;br&gt;
    &lt;code&gt;Search.search_embeddings&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
    The return value must be a string. We will JSON-encode a small structure&lt;br&gt;&lt;br&gt;
    so the model can consume the results reliably.&lt;br&gt;&lt;br&gt;
    """&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;if&lt;/span&gt; name == &lt;span class="mention"&gt;"search_business_docs"&lt;/span&gt;:&lt;br&gt;&lt;br&gt;
        query = args.get(&lt;span class="mention"&gt;"query"&lt;/span&gt;, &lt;span class="mention"&gt;""&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
        top_k = args.get(&lt;span class="mention"&gt;"top_k"&lt;/span&gt;, &lt;span class="mention"&gt;""&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
        results = search_embeddings(query, top_k)&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;# Expecting each row to be something like (ID, chunk_text)&lt;/span&gt;&lt;br&gt;&lt;br&gt;
        formatted: List[Dict[str, Any]] = []&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;for&lt;/span&gt; row &lt;span class="mention"&gt;in&lt;/span&gt; results:&lt;br&gt;&lt;br&gt;
            &lt;span class="mention"&gt;if&lt;/span&gt; &lt;span class="mention"&gt;not&lt;/span&gt; row:&lt;br&gt;&lt;br&gt;
                &lt;span class="mention"&gt;continue&lt;/span&gt;&lt;br&gt;&lt;br&gt;
            &lt;span class="mention"&gt;# Be defensive in case row length/structure changes&lt;/span&gt;&lt;br&gt;&lt;br&gt;
            doc_id = row[&lt;span class="mention"&gt;0&lt;/span&gt;] &lt;span class="mention"&gt;if&lt;/span&gt; len(row) &amp;gt; &lt;span class="mention"&gt;0&lt;/span&gt; &lt;span class="mention"&gt;else&lt;/span&gt; &lt;span class="mention"&gt;None&lt;/span&gt;&lt;br&gt;&lt;br&gt;
            text = row[&lt;span class="mention"&gt;1&lt;/span&gt;] &lt;span class="mention"&gt;if&lt;/span&gt; len(row) &amp;gt; &lt;span class="mention"&gt;1&lt;/span&gt; &lt;span class="mention"&gt;else&lt;/span&gt; &lt;span class="mention"&gt;None&lt;/span&gt;&lt;br&gt;&lt;br&gt;
            formatted.append({&lt;span class="mention"&gt;"id"&lt;/span&gt;: doc_id, &lt;span class="mention"&gt;"text"&lt;/span&gt;: text})&lt;br&gt;&lt;br&gt;
        payload = {&lt;span class="mention"&gt;"query"&lt;/span&gt;: query, &lt;span class="mention"&gt;"results"&lt;/span&gt;: formatted}&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;return&lt;/span&gt; json.dumps(payload, ensure_ascii=&lt;span class="mention"&gt;False&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;# Unknown tool; return an error-style payload&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;return&lt;/span&gt; json.dumps({&lt;span class="mention"&gt;"error"&lt;/span&gt;: &lt;span class="mention"&gt;f"Unknown tool name: &lt;/span&gt;&lt;span class="mention"&gt;{name}&lt;/span&gt;"})&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;Now that we have our RAG tool, we can start work on the chat loop logic. First, we need a helper to reliably pull the model’s final answer and any tool outputs from the OpenAI Responses API. extract_answer_and_sources(response) walks the response.output items containing the models outputs and concatenates them into a single answer string. It also collects the function_call_output payloads (the JSON we returned from our RAG tool), parses them, and exposes them as tool_context for transparency and debugging. The function parses the model output into a compact structure: {"answer": ..., "tool_context": [...]}.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;def&lt;/span&gt; &lt;span class="mention"&gt;extract_answer_and_sources&lt;/span&gt;&lt;span class="mention"&gt;(response: Any)&lt;/span&gt; -&amp;gt; Dict[str, Any]:&lt;br&gt;&lt;br&gt;
    """Extract a structured answer and optional sources from a Responses API object.&lt;br&gt;&lt;br&gt;
    We don't enforce a global JSON response schema here. Instead, we:&lt;br&gt;&lt;br&gt;
    - Prefer the SDK's &lt;code&gt;output_text&lt;/code&gt; convenience when present&lt;br&gt;&lt;br&gt;
    - Fall back to concatenating any &lt;code&gt;output_text&lt;/code&gt; content parts&lt;br&gt;&lt;br&gt;
    - Also surface any tool-call-output payloads we got back this turn as&lt;br&gt;&lt;br&gt;
      &lt;code&gt;tool_context&lt;/code&gt; for debugging/inspection.&lt;br&gt;&lt;br&gt;
    """&lt;br&gt;&lt;br&gt;
    answer_text = &lt;span class="mention"&gt;""&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;# Preferred: SDK convenience&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;if&lt;/span&gt; hasattr(response, &lt;span class="mention"&gt;"output_text"&lt;/span&gt;) &lt;span class="mention"&gt;and&lt;/span&gt; response.output_text:&lt;br&gt;&lt;br&gt;
        answer_text = response.output_text&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;else&lt;/span&gt;:&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;# Fallback: walk output items&lt;/span&gt;&lt;br&gt;&lt;br&gt;
        parts: List[str] = []&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;for&lt;/span&gt; item &lt;span class="mention"&gt;in&lt;/span&gt; getattr(response, &lt;span class="mention"&gt;"output"&lt;/span&gt;, []) &lt;span class="mention"&gt;or&lt;/span&gt; []:&lt;br&gt;&lt;br&gt;
            &lt;span class="mention"&gt;if&lt;/span&gt; getattr(item, &lt;span class="mention"&gt;"type"&lt;/span&gt;, &lt;span class="mention"&gt;None&lt;/span&gt;) == &lt;span class="mention"&gt;"message"&lt;/span&gt;:&lt;br&gt;&lt;br&gt;
                &lt;span class="mention"&gt;for&lt;/span&gt; c &lt;span class="mention"&gt;in&lt;/span&gt; getattr(item, &lt;span class="mention"&gt;"content"&lt;/span&gt;, []) &lt;span class="mention"&gt;or&lt;/span&gt; []:&lt;br&gt;&lt;br&gt;
                    &lt;span class="mention"&gt;if&lt;/span&gt; getattr(c, &lt;span class="mention"&gt;"type"&lt;/span&gt;, &lt;span class="mention"&gt;None&lt;/span&gt;) == &lt;span class="mention"&gt;"output_text"&lt;/span&gt;:&lt;br&gt;&lt;br&gt;
                        parts.append(getattr(c, &lt;span class="mention"&gt;"text"&lt;/span&gt;, &lt;span class="mention"&gt;""&lt;/span&gt;))&lt;br&gt;&lt;br&gt;
        answer_text = &lt;span class="mention"&gt;""&lt;/span&gt;.join(parts)&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;# Collect any function_call_output items for visibility&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    tool_context: List[Dict[str, Any]] = []&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;for&lt;/span&gt; item &lt;span class="mention"&gt;in&lt;/span&gt; getattr(response, &lt;span class="mention"&gt;"output"&lt;/span&gt;, []) &lt;span class="mention"&gt;or&lt;/span&gt; []:&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;if&lt;/span&gt; getattr(item, &lt;span class="mention"&gt;"type"&lt;/span&gt;, &lt;span class="mention"&gt;None&lt;/span&gt;) == &lt;span class="mention"&gt;"function_call_output"&lt;/span&gt;:&lt;br&gt;&lt;br&gt;
            &lt;span class="mention"&gt;try&lt;/span&gt;:&lt;br&gt;&lt;br&gt;
                tool_context.append({&lt;br&gt;&lt;br&gt;
                    &lt;span class="mention"&gt;"call_id"&lt;/span&gt;: getattr(item, &lt;span class="mention"&gt;"call_id"&lt;/span&gt;, &lt;span class="mention"&gt;None&lt;/span&gt;),&lt;br&gt;&lt;br&gt;
                    &lt;span class="mention"&gt;"output"&lt;/span&gt;: json.loads(getattr(item, &lt;span class="mention"&gt;"output"&lt;/span&gt;, &lt;span class="mention"&gt;""&lt;/span&gt;)),&lt;br&gt;&lt;br&gt;
                })&lt;br&gt;&lt;br&gt;
            &lt;span class="mention"&gt;except&lt;/span&gt; Exception:&lt;br&gt;&lt;br&gt;
                tool_context.append({&lt;br&gt;&lt;br&gt;
                    &lt;span class="mention"&gt;"call_id"&lt;/span&gt;: getattr(item, &lt;span class="mention"&gt;"call_id"&lt;/span&gt;, &lt;span class="mention"&gt;None&lt;/span&gt;),&lt;br&gt;&lt;br&gt;
                    &lt;span class="mention"&gt;"output"&lt;/span&gt;: getattr(item, &lt;span class="mention"&gt;"output"&lt;/span&gt;, &lt;span class="mention"&gt;""&lt;/span&gt;),&lt;br&gt;&lt;br&gt;
                })&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;return&lt;/span&gt; {&lt;span class="mention"&gt;"answer"&lt;/span&gt;: answer_text.strip(), &lt;span class="mention"&gt;"tool_context"&lt;/span&gt;: tool_context}&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;With the help of extract_answer_and_sources we can build the whole chat loop to orchestrate a two&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;‑&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;phase, tool&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;‑&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;calling conversation with the OpenAI Responses API. The chat_loop() function runs an interactive CLI: it collects the user’s question, sends a first request with system instructions and the search_business_docs tool, and then inspects any function_call items the model emits. For each function call, it executes our local RAG tool (call_rag_tool, which wraps search_embeddings) and appends the result back to the conversation as a function_call_output. It then makes a second request asking the model to use those tool outputs to produce a grounded answer, parses that answer via extract_answer_and_sources, and prints it. The loop maintains running context in input_items so each turn can build on prior messages and tool results.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;def&lt;/span&gt; &lt;span class="mention"&gt;chat_loop&lt;/span&gt;&lt;span class="mention"&gt;()&lt;/span&gt; -&amp;gt; &lt;span class="mention"&gt;None&lt;/span&gt;:&lt;br&gt;&lt;br&gt;
    """Run an interactive CLI chat loop using the OpenAI Responses API.&lt;br&gt;&lt;br&gt;
    The loop supports multi-step tool-calling:&lt;br&gt;&lt;br&gt;
    - First call may return one or more &lt;code&gt;function_call&lt;/code&gt; items&lt;br&gt;&lt;br&gt;
    - We execute those locally (e.g., call search_embeddings)&lt;br&gt;&lt;br&gt;
    - We send the tool outputs back in a second &lt;code&gt;responses.create&lt;/code&gt; call&lt;br&gt;&lt;br&gt;
    - Then we print the model's final, grounded answer&lt;br&gt;&lt;br&gt;
    """&lt;br&gt;&lt;br&gt;
    key = os.getenv(&lt;span class="mention"&gt;"OPENAI_API_KEY"&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;if&lt;/span&gt; &lt;span class="mention"&gt;not&lt;/span&gt; key:&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;raise&lt;/span&gt; RuntimeError(&lt;span class="mention"&gt;"OPENAI_API_KEY is not set in the environment."&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
    client = OpenAI(api_key=key)&lt;br&gt;&lt;br&gt;
    print(&lt;span class="mention"&gt;"\nBusiness Service RAG Chat"&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
    print(&lt;span class="mention"&gt;"Type 'exit' or 'quit' to stop.\n"&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;# Running list of inputs (messages + tool calls + tool outputs) for context&lt;/span&gt;&lt;br&gt;&lt;br&gt;
    input_items: List[Dict[str, Any]] = []&lt;br&gt;&lt;br&gt;
    &lt;span class="mention"&gt;while&lt;/span&gt; &lt;span class="mention"&gt;True&lt;/span&gt;:&lt;br&gt;&lt;br&gt;
        user_input = input(&lt;span class="mention"&gt;"You: "&lt;/span&gt;).strip()&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;if&lt;/span&gt; &lt;span class="mention"&gt;not&lt;/span&gt; user_input:&lt;br&gt;&lt;br&gt;
            &lt;span class="mention"&gt;continue&lt;/span&gt;&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;if&lt;/span&gt; user_input.lower() &lt;span class="mention"&gt;in&lt;/span&gt; {&lt;span class="mention"&gt;"exit"&lt;/span&gt;, &lt;span class="mention"&gt;"quit"&lt;/span&gt;}:&lt;br&gt;&lt;br&gt;
            print(&lt;span class="mention"&gt;"Goodbye."&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
            &lt;span class="mention"&gt;break&lt;/span&gt;&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;# Add user message&lt;/span&gt;&lt;br&gt;&lt;br&gt;
        input_items.append({&lt;span class="mention"&gt;"role"&lt;/span&gt;: &lt;span class="mention"&gt;"user"&lt;/span&gt;, &lt;span class="mention"&gt;"content"&lt;/span&gt;: user_input})&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;# 1) First call: let the model decide whether to call tools&lt;/span&gt;&lt;br&gt;&lt;br&gt;
        response = client.responses.create(&lt;br&gt;&lt;br&gt;
            model=MODEL,&lt;br&gt;&lt;br&gt;
            instructions=SYSTEM_INSTRUCTIONS,&lt;br&gt;&lt;br&gt;
            tools=TOOLS,&lt;br&gt;&lt;br&gt;
            input=input_items,&lt;br&gt;&lt;br&gt;
        )&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;# Save model output items to our running conversation&lt;/span&gt;&lt;br&gt;&lt;br&gt;
        input_items += response.output&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;# 2) Execute any function calls&lt;/span&gt;&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;# The Responses API returns &lt;code&gt;function_call&lt;/code&gt; items in &lt;code&gt;response.output&lt;/code&gt;.&lt;/span&gt;&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;for&lt;/span&gt; item &lt;span class="mention"&gt;in&lt;/span&gt; response.output:&lt;br&gt;&lt;br&gt;
            &lt;span class="mention"&gt;if&lt;/span&gt; getattr(item, &lt;span class="mention"&gt;"type"&lt;/span&gt;, &lt;span class="mention"&gt;None&lt;/span&gt;) != &lt;span class="mention"&gt;"function_call"&lt;/span&gt;:&lt;br&gt;&lt;br&gt;
                &lt;span class="mention"&gt;continue&lt;/span&gt;&lt;br&gt;&lt;br&gt;
            name = getattr(item, &lt;span class="mention"&gt;"name"&lt;/span&gt;, &lt;span class="mention"&gt;None&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
            raw_args = getattr(item, &lt;span class="mention"&gt;"arguments"&lt;/span&gt;, &lt;span class="mention"&gt;"{}"&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
            &lt;span class="mention"&gt;try&lt;/span&gt;:&lt;br&gt;&lt;br&gt;
                args = json.loads(raw_args) &lt;span class="mention"&gt;if&lt;/span&gt; isinstance(raw_args, str) &lt;span class="mention"&gt;else&lt;/span&gt; raw_args&lt;br&gt;&lt;br&gt;
            &lt;span class="mention"&gt;except&lt;/span&gt; json.JSONDecodeError:&lt;br&gt;&lt;br&gt;
                args = {&lt;span class="mention"&gt;"query"&lt;/span&gt;: user_input}&lt;br&gt;&lt;br&gt;
            result_str = call_rag_tool(name, args &lt;span class="mention"&gt;or&lt;/span&gt; {})&lt;br&gt;&lt;br&gt;
            &lt;span class="mention"&gt;# Append tool result back as function_call_output&lt;/span&gt;&lt;br&gt;&lt;br&gt;
            input_items.append(&lt;br&gt;&lt;br&gt;
                {&lt;br&gt;&lt;br&gt;
                    &lt;span class="mention"&gt;"type"&lt;/span&gt;: &lt;span class="mention"&gt;"function_call_output"&lt;/span&gt;,&lt;br&gt;&lt;br&gt;
                    &lt;span class="mention"&gt;"call_id"&lt;/span&gt;: getattr(item, &lt;span class="mention"&gt;"call_id"&lt;/span&gt;, &lt;span class="mention"&gt;None&lt;/span&gt;),&lt;br&gt;&lt;br&gt;
                    &lt;span class="mention"&gt;"output"&lt;/span&gt;: result_str,&lt;br&gt;&lt;br&gt;
                }&lt;br&gt;&lt;br&gt;
            )&lt;br&gt;&lt;br&gt;
        &lt;span class="mention"&gt;# 3) Second call: ask the model to answer using tool outputs&lt;/span&gt;&lt;br&gt;&lt;br&gt;
        followup = client.responses.create(&lt;br&gt;&lt;br&gt;
            model=MODEL,&lt;br&gt;&lt;br&gt;
            instructions=(&lt;br&gt;&lt;br&gt;
                SYSTEM_INSTRUCTIONS&lt;br&gt;&lt;br&gt;
                + &lt;span class="mention"&gt;"\n\nYou have just received outputs from your tools. "&lt;/span&gt;&lt;br&gt;&lt;br&gt;
                + &lt;span class="mention"&gt;"Use them to give a concise, well-structured answer."&lt;/span&gt;&lt;br&gt;&lt;br&gt;
            ),&lt;br&gt;&lt;br&gt;
            tools=TOOLS,&lt;br&gt;&lt;br&gt;
            input=input_items,&lt;br&gt;&lt;br&gt;
        )&lt;br&gt;&lt;br&gt;
        structured = extract_answer_and_sources(followup)&lt;br&gt;&lt;br&gt;
        print(&lt;span class="mention"&gt;"Agent:\n"&lt;/span&gt; + structured[&lt;span class="mention"&gt;"answer"&lt;/span&gt;] + &lt;span class="mention"&gt;"\n"&lt;/span&gt;)&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;That’s it! You’ve built a complete RAG pipeline powered by IRIS Vector Search. While this example focused on a simple use case, IRIS Vector Search opens the door to many more possibilities:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li&gt;&lt;span&gt;&lt;span&gt;Knowledge store for more complex customer support agents&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;

&lt;li&gt;&lt;span&gt;&lt;span&gt;Conversational context storage for hyper-personalized agents &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;

&lt;li&gt;&lt;span&gt;&lt;span&gt;Anomaly detection in textual data&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;

&lt;li&gt;&lt;span&gt;&lt;span&gt;Clustering analysis for textual data&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;

&lt;/ul&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;I hope this walkthrough gave you a solid starting point for exploring vector search and building your own AI-driven applications with InterSystems IRIS!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;span&gt;The full codebase can be found here:&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;

&lt;li&gt;&lt;a href="https://openexchange.intersystems.com/portal/products/IRISVectorSearchRAGExample" rel="noopener noreferrer"&gt;&lt;span&gt;&lt;span&gt;&lt;a href="https://openexchange.intersystems.com/portal/products/IRISVectorSearchRAGExample" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://openexchange.intersystems.com/portal/products/IRISVectorSearchRAGExample" rel="noopener noreferrer"&gt;https://openexchange.intersystems.com/portal/products/IRISVectorSearchRAGExample&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;

&lt;li&gt;&lt;a href="https://github.com/isc-epolakie/IRISVectorSearchRAGExample" rel="noopener noreferrer"&gt;&lt;span&gt;&lt;span&gt;&lt;a href="https://github.com/isc-epolakie/IRISVectorSearchRAGExample" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://github.com/isc-epolakie/IRISVectorSearchRAGExample" rel="noopener noreferrer"&gt;https://github.com/isc-epolakie/IRISVectorSearchRAGExample&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>python</category>
      <category>sql</category>
    </item>
    <item>
      <title>Virtualizing large databases - VMware CPU capacity planning</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Wed, 25 Mar 2026 19:39:06 +0000</pubDate>
      <link>https://dev.to/intersystems/virtualizing-large-databases-vmware-cpu-capacity-planning-13b6</link>
      <guid>https://dev.to/intersystems/virtualizing-large-databases-vmware-cpu-capacity-planning-13b6</guid>
      <description>&lt;p&gt;I am often asked by customers, vendors or internal teams to explain CPU capacity planning for &lt;em&gt;large production databases&lt;/em&gt;  running on VMware vSphere. &lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;This post was originally written in 2017, I am updating the post in February 2026. For context I have kept the original post, but highlighted changes. This post was originally written for ESXi 6.0. The core principles remain valid for vSphere 7.x and 8.x, though there have been improvements to vNUMA handling, CPU scheduling (particularly for AMD EPYC), and CPU Hot Add compatibility with vNUMA in vSphere 8. Always consult the Performance Best Practices guide for your specific vSphere version. For a deeper dive see the "Additional links" section at the end of the post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Changes are marked with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;UPDATE 2026:&lt;/strong&gt; ...&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;In summary there are a few simple best practices to follow for sizing CPU for large production databases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plan for one vCPU per physical CPU core.&lt;/li&gt;
&lt;li&gt;Consider NUMA and ideally size VMs to keep CPU and memory local to a NUMA node. &lt;/li&gt;
&lt;li&gt;Right-size virtual machines. Add vCPUs only when needed. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generally this leads to a couple of common questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Because of hyper-threading VMware lets me create VMs with 2x the number of physical CPUs. Doesn’t that double capacity? Shouldn’t I create VMs with as many CPUs as possible?&lt;/li&gt;
&lt;li&gt;What is a NUMA node? Should I care about NUMA?&lt;/li&gt;
&lt;li&gt;VMs should be right-sized, but how do I know when they are?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I answer these questions with examples below. Bust also remember, best practices are not written in stone. Sometimes you need to make compromises. For example, it is likely that large production database VMs will NOT fit in a NUMA node, and as we will see that’s OK. Best practices are guidelines that you will have to evaluate and validate for your applications and environment.&lt;/p&gt;

&lt;p&gt;Although I am writing this with examples for databases running on InterSystems data platforms, the concepts and rules apply generally for capacity and performance planning for any large (Monster) VMs.&lt;/p&gt;



&lt;br&gt;
For virtualisation best practices and more posts on performance and capacity planning;&lt;br&gt;
&lt;a href="https://community.intersystems.com/post/capacity-planning-and-performance-series-index" rel="noopener noreferrer"&gt;A list of other posts in the InterSystems Data Platforms and performance series is here.&lt;/a&gt;



&lt;h1&gt;
  
  
  Monster VMs
&lt;/h1&gt;

&lt;p&gt;This post is mostly about deploying &lt;em&gt;Monster VMs &lt;/em&gt;, sometimes called &lt;em&gt;Wide VMs&lt;/em&gt;. The CPU resource requirements of high transaction databases mean they are often deployed on Monster VMs. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A monster VM is a VM with more Virtual CPUs or memory than a physical NUMA node.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;h1&gt;
  
  
  CPU architecture and NUMA
&lt;/h1&gt;

&lt;p&gt;Current Intel processor architecture has Non-Uniform Memory Architecture (NUMA) architecture. For example, the servers I am using to run tests for this post have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two CPU sockets, each with a processor with 12 cores (Intel E5-2680 v3). &lt;/li&gt;
&lt;li&gt;256 GB memory (16 x 16GB RDIMM)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each 12-core processor has its own local memory (128GB of RDIMMs and local cache) and can also access memory on other processors in the same host.  Each 12-core package of CPU, CPU cache and 128 GB RDIMM memory is a NUMA node. To access memory on another processor NUMA nodes are connected by a fast inter-connect.&lt;/p&gt;

&lt;p&gt;Processes running on a processor accessing local RDIMM and Cache memory have lower latency than going across the interconnect to access remote memory on another processor. Access across the interconnect increases latency, so performance is non-uniform. The same design applies to servers with more than two sockets. A four socket Intel server has four NUMA nodes.&lt;/p&gt;

&lt;p&gt;ESXi understands physical NUMA and the ESXi CPU scheduler is designed to optimise performance on NUMA systems. One of the ways ESXi maximises performance is to create data locality on a physical NUMA node. In our example if you have a VM with 12 vCPU and less than 128GB memory, ESXi will assign that VM to run on one of the physical NUMA nodes. Which leads to the rule;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If possible size VMs to keep CPU and memory local to a NUMA node. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you need a Monster VM larger than a NUMA node that is OK, ESXi does a very good job of automatically calculating and managing requirements. For example,  ESXi will create virtual NUMA nodes (vNUMA) that intelligently schedule onto the physical NUMA nodes for optimal performance. The vNUMA structure is exposed to the operating system. For example, if you have a host server with two 12-core processors and a VM with 16 vCPUs ESXi may use eight physical cores on on each of two processors to schedule VM vCPUs, the operating system (Linux or Windows) will see two NUMA nodes. &lt;/p&gt;

&lt;p&gt;It is also important to right-size your VMs and not allocate more resources than are needed as that can lead to wasted resources and loss of performance. As well as helping you size for NUMA, it is more efficient and will result in better performance, to have a 12 vCPU VM with high (but safe) CPU utilisation than a 24 vCPU VM with low or middling VM CPU utilisation, especially if there are other VMs on this host needing to be scheduled and competing for resources. This also re-enforces the rule;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Right-size virtual machines.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; There are differences between Intel and AMD implementations of NUMA. AMD has multiple NUMA nodes per processor. It’s been a while since I have seen AMD processors in a customer server, but if you have them review NUMA layout as part of your planning. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;UPDATE 2026:&lt;/strong&gt; Note: AMD EPYC processors are now common in datacenter environments and have a different NUMA architecture than Intel. EPYC processors can have multiple NUMA nodes per socket, configured via NPS (NUMA Per Socket) BIOS settings. Starting with vSphere 7.0 Update 2, the ESXi CPU scheduler includes significant optimizations for AMD EPYC that can achieve up to 50% better performance out-of-the-box. For AMD EPYC, the default BIOS settings (NPS-1, CCX-as-NUMA disabled) provide optimal performance for most virtualization workloads. Review AMD's VMware vSphere Tuning Guides for your specific EPYC generation (7003, 8004, 9004 series) for detailed recommendations.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;h2&gt;
  
  
  Wide VMs and Licencing
&lt;/h2&gt;

&lt;p&gt;For best NUMA scheduling configure wide VMs;&lt;br&gt;
Correction June 2017:  Configure VMs with 1 vCPU per socket. &lt;br&gt;
For example, by default a VM with 24 vCPUs should be configured as 24 CPU sockets each with one core. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Follow VMware best practice rules .&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Please see &lt;a href="https://blogs.vmware.com/performance/2017/03/virtual-machine-vcpu-and-vnuma-rightsizing-rules-of-thumb.html" rel="noopener noreferrer"&gt;this post on the VMware blogs for examples. &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The VMware blog post goes into detail, but the author, Mark Achtemichuk, recommends the following rules of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;While there are many advanced vNUMA settings, only in rare cases do they need to be changed from defaults.&lt;/li&gt;
&lt;li&gt;Always configure the virtual machine vCPU count to be reflected as Cores per Socket, until you exceed the physical core count of a single physical NUMA node.&lt;/li&gt;
&lt;li&gt;When you need to configure more vCPUs than there are physical cores in the NUMA node, evenly divide the vCPU count across the minimum number of NUMA nodes.&lt;/li&gt;
&lt;li&gt;Don’t assign an odd number of vCPUs when the size of your virtual machine exceeds a physical NUMA node.&lt;/li&gt;
&lt;li&gt;Don’t enable vCPU Hot Add unless you’re okay with vNUMA being disabled.&lt;/li&gt;
&lt;li&gt;Don’t create a VM larger than the total number of physical cores of your host.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;UPDATE 2026:&lt;/strong&gt; Starting with vSphere 6.5, vNUMA behavior was decoupled from the Cores per Socket setting. ESXi now automatically calculates and presents the optimal vNUMA topology to the guest OS. For most workloads, leaving the default settings is recommended. vSphere 8.0 introduced an enhanced virtual topology feature that automatically selects optimal coresPerSocket values for VMs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UPDATE 2026:&lt;/strong&gt; For vSphere 8 and later: The limitation where CPU Hot Add disabled vNUMA has been lifted in vSphere 8 for VMs using virtual hardware version 20. VMs can now be configured to expose vNUMA topology even with CPU Hot-Add enabled. However, this requires the VM to use the latest virtual hardware compatibility and the new vSphere 8 API property to be configured. **For earlier vSphere versions, the original guidance still applies: do not enable Hot Add for monster VMs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;IRIS licensing counts cores so this is not a problem, however for software or databases other than IRIS specifying that a VM has 24 sockets could make a difference to software licensing so you must check with vendors. &lt;/p&gt;



&lt;h1&gt;
  
  
  Hyper-threading and the CPU schedular
&lt;/h1&gt;

&lt;p&gt;Hyper-threading (HT) often comes up in discussions, I hear; “hyper-threading doubles the number of CPU cores”. Which obviously at the physical level it can’t — you have as many physical cores as you have. Hyper-threading should be enabled and will increase system performance. An expectation is maybe 20%-30% or more application performance increase, but the actual amount is dependant on the application and the workload. But certainly not double. &lt;/p&gt;

&lt;p&gt;As I posted in the &lt;a href="https://community.intersystems.com/post/intersystems-data-platforms-and-performance-%E2%80%93-part-9-cach%C3%A9-vmware-best-practice-guide" rel="noopener noreferrer"&gt;VMware best practice post&lt;/a&gt;, a good starting point for sizing &lt;em&gt;large production database VMs&lt;/em&gt; is to assume is that the vCPU has full physical core dedication on the server —basically ignore hyper-threading when capacity planning. For example; &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For a 24-core host server plan for a total of up to 24 vCPU for production database VMs knowing there may be available headroom.  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you have spent time monitoring the application, operating system and VMware performance during peak processing times you can decide if higher VM consolidation is possible. In the best practice post I stated the rule as;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One physical CPU (includes hyper-threading) = One vCPU (includes hyper-threading).&lt;/p&gt;
&lt;/blockquote&gt;



&lt;h2&gt;
  
  
  Why Hyper-threading does not double CPU
&lt;/h2&gt;

&lt;p&gt;HT on Intel Xeon processors is a way of creating two &lt;em&gt;logical&lt;/em&gt; CPUs on one physical core. The operating system can efficiently schedule against the two logical processors — if a process or thread on a logical processor is waiting, for example for IO, the physical CPU resources can be used by the other logical processor. Only one logical processor can be progressing at any point in time, so although the physical core is more efficiently utilised &lt;em&gt;performance is not doubled&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;With HT enabled in the host BIOS, when creating a VM you can configure a vCPU per HT logical processor. For example, on a 24-physical core server with HT enabled you can create a VM with up to 48 vCPUS. The ESXi CPU scheduler will optimise processing by running VMs processes on separate physical cores first (while still considering NUMA). I explore later in the post whether allocating more vCPUs than physical cores on a Monster database VM helps scaling.&lt;/p&gt;

&lt;h3&gt;
  
  
  co-stop and CPU scheduling
&lt;/h3&gt;

&lt;p&gt;After monitoring host and application performance you may decide that some overcommitment of host CPU resources is possible. Whether this is a good idea will be very dependant on the applications and workloads. An understanding of the schedular and a key metric to monitor can help you be sure that you are not over committing host resources.&lt;/p&gt;

&lt;p&gt;I sometimes hear; for a VM to be progressing there must be the same number of free logical CPUs as there are vCPUs in the VM. For example, a 12 vCPU VM must ‘wait’ for 12 logical CPUs to be ‘available’ before execution progresses. However it should be noted that ESXi after version 3 this is not the case. ESXi uses relaxed co-scheduling for CPU for better application performance.&lt;/p&gt;

&lt;p&gt;Because multiple cooperating threads or processes frequently synchronise with each other not scheduling them together can increase latency in their operations. For example a thread waiting to be scheduled by another thread in a spin loop. For best performance ESXi tries to schedule as many sibling vCPUs together as possible. But the CPU scheduler can flexibly schedule vCPUs when there a multiple VMs competing for CPU resources in a consolidated environment. If there is too much time difference as some vCPUs make progress while siblings don’t (the time difference is called skew) then the leading vCPU will decide whether to stop itself (co-stop). Note that it is vCPUs that co-stop (or co-start), not the entire VM. This works very well when even when there is some over commitment of resources, however as you would expect; too much over commitment of CPU resources will inevitably impact performance. I show an example of over commitment and co-stop later in Example 2.&lt;/p&gt;

&lt;p&gt;Remember it is not a flat-out race for CPU resources between VMs; the ESXi CPU scheduler’s job is to ensure that policies such as CPU shares, reservations and limits are followed while maximising CPU utilisation and to ensure fairness, throughput, responsiveness and scalability. A discussion of using reservations and shares to prioritise production workloads is beyond the scope of this post and dependant on your application and workload mix. I may revisit this at a later time if I find any IRIS specific recommendations. There are many factors that come into play with the CPU scheduler, this section just skims the surface. For a deep dive see the VMware white paper and other links in the references at the end of the post. &lt;/p&gt;



&lt;h1&gt;
  
  
  Examples
&lt;/h1&gt;

&lt;p&gt;To illustrate the different vCPU configurations, I ran a series of benchmarks using a high transaction rate browser based Hospital Information System application. A similar concept to the DVD Store database benchmark developed by VMware.&lt;/p&gt;

&lt;p&gt;The scripts for the benchmark are created based on observations and metrics from live hospital implementations and include high use workflows, transactions and components that use the highest system resources. Driver VMs on other hosts simulate web sessions (users) by executing scripts with randomised input data at set workflow transaction rates. A benchmark with a rate of 1x is the baseline. Rates can be scaled up and down in increments. &lt;/p&gt;

&lt;p&gt;Along with the database and operating system metrics a good metric to gauge how the benchmark database VM is performing is component (also could be a transaction) response time as measured on the server. An example of a component is part of an end user screen. An increase in component response time means users would start to see a change for the worse in application response time. A well performing database system must provide &lt;em&gt;consistent&lt;/em&gt; high performance for end users. In the following charts, I am measuring against consistent test performance and an indication of end user experience by averaging the response time of the 10 slowest high-use components. Average component response time is expected to be  sub-second, a user screen may be made up of one component, or complex screens may have many components. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember you are always sizing for peak workload, plus a buffer for unexpected spikes in activity. I usually aim for average 80% peak CPU utilisation. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A full list of benchmark hardware and software is at the end of the post.  &lt;/p&gt;



&lt;h2&gt;
  
  
  Example 1. Right-sizing - single monster VM per host
&lt;/h2&gt;

&lt;p&gt;It is possible to create a database VM that is sized to use all the physical cores of a host server, for example a 24 vCPU VM on the 24 physical core host. Rather than run the server “bare-metal” in a IRIS database mirror for HA or introduce the complication of operating system failover clustering, the database VM is included in a vSphere cluster for management and HA, for example DRS and VMware HA. &lt;/p&gt;

&lt;p&gt;I have seen customers follow old-school thinking and size a primary database VM for expected capacity at the end of five years hardware life, but as we know from above it is better to right-size; you will get better performance and consolidation if your VMs are not oversized and managing HA will be easier; think Tetris if there is maintenance or host failure and the database monster VM has to migrate or restart on another host. If transaction rate is forecast to increase significantly vCPUs can be added ahead of time during planned maintenance. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note, 'hot add' CPU option disables vNUMA so do not use it for monster VMs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Consider the following chart showing a series of tests on the 24-core host. 3x transaction rate is the sweet spot and the capacity planning target for this 24-core system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A single VM is running on the host.&lt;/li&gt;
&lt;li&gt;Four VM sizes were used to show performance at 12, 24, 36 and 48 vCPU. &lt;/li&gt;
&lt;li&gt;Transaction rates (1x, 2x, 3x, 4x, 5x) were run for each VM size (if possible).&lt;/li&gt;
&lt;li&gt;Performance/user experience is shown as component response time (bars).&lt;/li&gt;
&lt;li&gt;Average CPU% utilisation in the guest VM (lines).&lt;/li&gt;
&lt;li&gt;Host CPU utilisation reached 100% (red dashed line) at 4x rate for all VM sizes.&lt;/li&gt;
&lt;/ul&gt;



&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcommunity.intersystems.com%2Fsites%2Fdefault%2Ffiles%2Finline%2Fimages%2Fsingle_guest_vm.png" title="Single Guest VM" alt="24 Physical Core Host&amp;lt;br&amp;gt;
Single guest VM average CPU% and Component Response time " width="800" height="383"&gt;&lt;br&gt;


&lt;p&gt;There is a lot going on in this chart, but we can focus on a couple of interesting things. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The 24 vCPU VM (orange) scaled up smoothly to the target 3x transaction rate. At 3x rate the in-guest VM is averaging 76% CPU (peaks were around 91%).  Host CPU utilisation is not much more than the guest VM. Component response time is pretty much flat up to 3x, so users are happy. As far as our target transaction rate — &lt;em&gt;this VM is right-sized&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So much for right-sizing, what about increasing vCPUs, that means using hyper threads. Is it possible to double performance and scalability? The short answer is &lt;em&gt;No!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this case the answer can be seen by looking at component response time from 4x onwards. While the performance is ‘better’ with more logical cores (vCPUs) allocated, it is still not flat and as consistent as it was up to 3x. Users will be reporting slower response times at 4x no matter how many vCPUs are allocated. Remember at 4x the &lt;em&gt;host &lt;/em&gt; is already flat-lined at 100% CPU utilisation as reported by vSphere. At higher vCPU counts even though in-guest CPU metrics (vmstat) are reporting less than 100% utilisation this is not the case for physical resources. Remember the guest operating system does not know it is virtualised and is just reporting on resources presented to it. Also note the guest operating system does not see HT threads, all vCPUs are presented as physical cores.&lt;/p&gt;

&lt;p&gt;The point is that database processes (there are more than 200 IRIS processes at 3x transaction rate) are very busy and make very efficient use of processors, there is not a lot of slack for logical processors to schedule more work, or consolidate more VMs to this host. For example, a large part of IRIS processing is happening in-memory so there is not a lot of wait on IO. So while you can allocate more vCPUs than physical cores there is not a lot to be gained because the host is already 100% utilised.&lt;/p&gt;

&lt;p&gt;IRIS is very good at handling high workloads. Even when the host and VM are at 100% CPU utilisation the application is still running, and transaction rate is still increasing — scaling is not linear, and as we can see response times are getting longer and user experience will suffer — but the application does not ‘fall off a cliff’ and although not a good place to be users can still work. If you have an application that is not so sensitive to response times it is good to know you can push to the edge, and beyond, and IRIS still works safely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember you do not want to run your database VM or your host at 100% CPU. You need capacity for unexpected spikes and growth in the VM, and ESXi hypervisor needs resources for all the networking, storage and other activities it does. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I always plan for peaks of 80% CPU utilisation. Even then sizing vCPU only up to the number of physical cores leaves some headroom for ESXi hypervisor on logical threads even in extreme situations.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you are running a hyper-converged (HCI) solution you MUST also factor in HCI CPU requirements at the host level. See my &lt;a href="https://community.intersystems.com/post/intersystems-data-platforms-and-performance-%E2%80%93-part-8-hyper-converged-infrastructure-capacity" rel="noopener noreferrer"&gt;previous post on HCI&lt;/a&gt; for more details. Basic CPU sizing of VMs deployed on HCI is the same as other VMs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Remember, You must validate and test everything in your own environment and with your applications.&lt;/p&gt;



&lt;h2&gt;
  
  
  Example 2. Over committed resources
&lt;/h2&gt;

&lt;p&gt;I have seen customer sites reporting ‘slow’ application performance while the guest operating system reports there are CPU resources to spare. &lt;/p&gt;

&lt;p&gt;Remember the guest operating system does not know it is virtualised. Unfortunately in-guest metrics, for example as reported by vmstat (for example in pButtons) can be deceiving, you must also get host level metrics and ESXi metrics (for example &lt;code&gt;esxtop&lt;/code&gt;) to truly understand system health and capacity. &lt;/p&gt;

&lt;p&gt;As you can see in the chart above when the host is reporting 100% utilisation the guest VM can be reporting a lower utilisation. The 36 vCPU VM (red) is reporting 80% average CPU utilisation at 4x rate while the host is reporting 100%. Even a right-sized VM can be starved of resources, if for example, after go-live other VMs are migrated on to the host, or resources are over-committed through badly configured DRS rules.&lt;/p&gt;

&lt;p&gt;To show key metrics, for this series of tests I configured the following;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two database VMs running on the host.&lt;/li&gt;
&lt;li&gt; - a 24vCPU running at a constant 2x transaction rate (not shown on chart).&lt;/li&gt;
&lt;li&gt;- a 24vCPU running at 1x, 2x, 3x (these metrics are shown on chart).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With another database using resources;  at 3x rate, the guest OS (RHEL 7) vmstat is only reporting 86% average CPU utilisation and the run queue is only averaging 25. However, users of this system will be complaining loudly as the component response time shot up as processes are slowed.&lt;/p&gt;

&lt;p&gt;As shown in the following chart Co-stop and Ready Time tell the story why user performance is so bad. Ready Time (&lt;code&gt;%RDY&lt;/code&gt;) and CoStop (&lt;code&gt;%CoStop&lt;/code&gt;) metrics show CPU resources are massively over committed at the target 3x rate. This should not really be a surprise as the &lt;em&gt;host&lt;/em&gt; is running 2x (other VM) &lt;em&gt;and&lt;/em&gt; this database VMs 3x rate. &lt;/p&gt;



&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcommunity.intersystems.com%2Fsites%2Fdefault%2Ffiles%2Finline%2Fimages%2Fovercommit_3.png" title="Over-committed host" width="800" height="492"&gt;&lt;br&gt;


&lt;p&gt;The chart shows Ready time increases when total CPU load on the host increases.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ready time is time that a VM is ready to run but cannot because CPU resources are not available. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Co-stop also increases. There are not enough free logical CPUs to allow the database VM to progress (as I detailed in the HT section above). The end result is processing is delayed due to contention for physical CPU resources. &lt;/p&gt;

&lt;p&gt;I have seen exactly this situation at a customer site where our support view from pButtons and vmstat only showed the virtualised operating system. While vmstat reported CPU headroom user performance experience was terrible. &lt;/p&gt;

&lt;p&gt;The lesson here is it was not until ESXi metrics and a host level view was made available that the real problem was diagnosed; over committed CPU resources caused by general cluster CPU resource shortage and to make the situation worse bad DRS rules causing high transaction database VMs to migrate together and overwhelm host resources. &lt;/p&gt;



&lt;h2&gt;
  
  
  Example 3. Over committed resources
&lt;/h2&gt;

&lt;p&gt;In this example I used a baseline 24 vCPU database VM running at 3x transaction rate, then two 24 vCPU database VMs at a constant 3x transaction rate. &lt;/p&gt;

&lt;p&gt;The average baseline CPU utilisation (see Example 1 above) was 76% for the VM and 85% for the host. A single 24 vCPU database VM is using all 24 physical processors. Running two 24 vCPU VMs means the VMs are competing for resources and are using all 48 logical execution threads on the server. &lt;/p&gt;



&lt;br&gt;
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcommunity.intersystems.com%2Fsites%2Fdefault%2Ffiles%2Finline%2Fimages%2Fovercommit_2vm.png" title="Over-committed host" width="773" height="405"&gt;&lt;br&gt;


&lt;p&gt;Remembering that the host was not 100% utilised with a single VM, we can still see a significant drop in throughput and performance as two very busy 24 vCPU VMs attempt to use the 24 physical cores on the host (even with HT). Although IRIS is very efficient using the available CPU resources there is still a 16% drop in database throughput per VM, and more importantly a more than 50% increase in component (user) response time. &lt;/p&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;My aim for this post is to answer the common questions. See the reference section below for a deeper dive into CPU host resources and the VMware CPU schedular.&lt;/p&gt;

&lt;p&gt;Even though there are many levels of nerd-knob twiddling and ESXi rat holes to go down to squeeze the last drop of performance out of your system, the basic rules are pretty simple.&lt;/p&gt;

&lt;p&gt;For &lt;em&gt;large production databases&lt;/em&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plan for one vCPU per physical CPU core.&lt;/li&gt;
&lt;li&gt;Consider NUMA and ideally size VMs to keep CPU and memory local to a NUMA node. &lt;/li&gt;
&lt;li&gt;Right-size virtual machines. Add vCPUs only when needed. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to consolidate VMs remember large databases are very busy and will heavily utilise CPUs (physical and logical) at peak times. Don't oversubscribe them until your monitoring tells you it is safe.&lt;/p&gt;



&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blogs.vmware.com/vsphere/2014/02/overcommit-vcpupcpu-monster-vms.html" rel="noopener noreferrer"&gt;VMware Blog - When to Overcommit vCPU:pCPU for Monster VMs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://frankdenneman.nl/2016/07/06/introduction-2016-numa-deep-dive-series" rel="noopener noreferrer"&gt;Introduction 2016 NUMA Deep Dive Series&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.vmware.com/content/dam/digitalmarketing/vmware/en/pdf/techpaper/vmware-vsphere-cpu-sched-performance-white-paper.pdf" rel="noopener noreferrer"&gt;The CPU Scheduler in VMware vSphere 5.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;h2&gt;
  
  
  Tests
&lt;/h2&gt;

&lt;p&gt;I ran the examples in this post on a vSphere cluster made up of two processor Dell R730’s attached to an all flash array. During the examples there was no bottlenecks on the network or storage.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IRIS 2016.2.1.803.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PowerEdge R730&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2x Intel(R) Xeon(R) CPU E5-2680 v3 @ 2.50GHz&lt;/li&gt;
&lt;li&gt;16x 16GB RDIMM, 2133 MT/s, Dual Rank, x4 Data Width&lt;/li&gt;
&lt;li&gt;SAS 12Gbps HBA External Controller &lt;/li&gt;
&lt;li&gt;HyperThreading (HT) on&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PowerVault MD3420, 12G SAS, 2U-24 drive &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;24x 24 960GB Solid State Drive SAS Read Intensive MLC 12Gbps 2.5in Hot-plug Drive, PX04SR &lt;/li&gt;
&lt;li&gt;2 Controller, 12G SAS, 2U MD34xx, 8G Cache &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;VMware ESXi 6.0.0 build-2494585&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VMs are configured for best practice; VMXNET3, PVSCSI, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RHEL 7&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Baseline 1x rate averaged 700,000 glorefs/second (database access/second). 5x rate averaged more than 3,000,000 glorefs/second for 24 vCPUs. The tests were allowed to burn in until constant performance is achieved and then 15 minute samples were taken and averaged. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These examples only to show the theory, you MUST validate with your own application!&lt;/p&gt;
&lt;/blockquote&gt;





&lt;h1&gt;
  
  
  Additional Links (Feb 2026)
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.vmware.com/docs/vsphere-esxi-vcenter-server-80U3-performance-best-practices" rel="noopener noreferrer"&gt;Performance Best Practices for VMware vSphere 8.0 Update 3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.vmware.com/docs/vsphere80-virtual-topology-perf" rel="noopener noreferrer"&gt;VMware vSphere 8.0 Virtual Topology Performance Study&lt;/a&gt; (also referenced in the &lt;a href="https://blogs.vmware.com/cloud-foundation/2022/11/10/extreme-performance-series-automatic-vtopology-for-vms-vsphere8/" rel="noopener noreferrer"&gt;Extreme Performance Series blog post&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.vmware.com/techpapers/2021/vsphere70u2-cpu-sched-amd-epyc.html" rel="noopener noreferrer"&gt;Performance Optimizations in VMware vSphere 7.0 U2 CPU Scheduler for AMD EPYC Processors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blogs.vmware.com/cloud-foundation/2017/03/09/virtual-machine-vcpu-and-vnuma-rightsizing-rules-of-thumb/" rel="noopener noreferrer"&gt;Virtual Machine vCPU and vNUMA Rightsizing – Guidelines (Mark Achtemichuk)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://frankdenneman.nl/2021/12/02/vsphere-7-cores-per-socket-and-virtual-numa/" rel="noopener noreferrer"&gt;vSphere 7 Cores per Socket and Virtual NUMA (Frank Denneman, 2021)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://frankdenneman.nl/2022/11/03/vsphere-8-cpu-topology-for-large-memory-footprint-vms-exceeding-numa-boundaries/" rel="noopener noreferrer"&gt;vSphere 8 CPU Topology for Large Memory Footprint VMs (Frank Denneman, 2022)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://frankdenneman.nl/2016/12/12/decoupling-cores-per-socket-virtual-numa-topology-vsphere-6-5/" rel="noopener noreferrer"&gt;Decoupling of Cores per Socket from Virtual NUMA Topology in vSphere 6.5 (Frank Denneman)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/tuning-guides/58003_amd-epyc-9004-tg-vmware-vsphere.pdf" rel="noopener noreferrer"&gt;AMD EPYC 9004 VMware vSphere Tuning Guide (AMD)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.vmware.com/docs/perf-latency-tuning-vsphere8" rel="noopener noreferrer"&gt;Performance Tuning for Latency-Sensitive Workloads in vSphere 8 (January 2025)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://williamlam.com/2022/11/virtual-numa-vnuma-and-cpu-hot-add-support-in-vsphere-8.html" rel="noopener noreferrer"&gt;vNUMA and CPU Hot-Add support in vSphere 8 (William Lam)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://frankdenneman.nl/category/numa/" rel="noopener noreferrer"&gt;NUMA Deep Dive Series (Frank Denneman)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>redis</category>
      <category>a11y</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
