<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: InterSystems</title>
    <description>The latest articles on DEV Community by InterSystems (intersystems).</description>
    <link>https://dev.to/intersystems</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2450%2F5c611adb-602d-4948-b84b-5fe47046fd5c.png</url>
      <title>DEV Community: InterSystems</title>
      <link>https://dev.to/intersystems</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/intersystems"/>
    <language>en</language>
    <item>
      <title>Business Rules Deep Dive: Dynamic Objects and Property Access Pitfalls - Part (2)</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Thu, 24 Sep 2026 15:46:30 +0000</pubDate>
      <link>https://dev.to/intersystems/business-rules-deep-dive-dynamic-objects-and-property-access-pitfalls-part-2-ggb</link>
      <guid>https://dev.to/intersystems/business-rules-deep-dive-dynamic-objects-and-property-access-pitfalls-part-2-ggb</guid>
      <description>&lt;p&gt;Hi, Community!&lt;/p&gt;

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

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

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




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

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




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

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




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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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




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

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

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

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

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

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




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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;






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

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

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

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




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

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




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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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




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

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

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




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

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




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

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




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

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

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




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

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

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

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




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

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

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




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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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




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

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

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




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

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




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

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

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

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

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

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

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

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

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

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

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




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

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




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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Return response.Content

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

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

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

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

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

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

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


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

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

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

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

While iter.%GetNext(.key, .value, .type) 
{
    Write !, key, " = ", value, " (", type, ")"
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;%GetNext()&lt;/code&gt; skips unassigned elements automatically, which is exactly why it is preferred. For an object, &lt;code&gt;key&lt;/code&gt; is the property name. For an array, &lt;code&gt;key&lt;/code&gt; is the index. To walk a nested structure, recurse whenever &lt;code&gt;$IsObject(value)&lt;/code&gt; is true, as that will return true for both sub-objects and sub-arrays.&lt;/p&gt;
&lt;p&gt;That third argument, &lt;code&gt;.type&lt;/code&gt;, is easy to ignore, but worth understanding. When present it does two useful things. First, it returns the element's original JSON datatype as a string. Second — and this is the practical part — it changes the conversion rules so you avoid &lt;code&gt;&amp;lt;MAXSTRING&amp;gt;&lt;/code&gt; errors: a very long JSON string is handed back as a read-only stream object instead of being force-fit into an ObjectScript string, and JSON numbers are returned in their original textual form rather than being coerced. If you're processing arbitrary external JSON, passing &lt;code&gt;.type&lt;/code&gt; is cheap insurance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiyzrzoat3xl0ld0dcdfn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiyzrzoat3xl0ld0dcdfn.png" alt=" " width="799" height="135"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2&gt;Type discovery inside a dynamic entity&lt;/h2&gt;
&lt;p&gt;Because dynamic entities are untyped containers, IRIS gives you tools to interrogate them. &lt;code&gt;%GetTypeOf(key)&lt;/code&gt; reports what a value actually is: &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;object&lt;/code&gt;, &lt;code&gt;array&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;oref&lt;/code&gt;, or &lt;code&gt;unassigned&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set a = [1, "test", true, {"v":1}, [1,2,3]]&lt;br&gt;
// the indexes are:&lt;br&gt;
//       0,      1,    2,       3,       4

&lt;/code&gt;&lt;p&gt;&lt;code&gt;Write a.%GetTypeOf(2)   // true: boolean&lt;br&gt;&lt;br&gt;
Write a.%GetTypeOf(3)   // {"v":1}: object&lt;br&gt;&lt;br&gt;
Write a.%GetTypeOf(9)   // unassigned - it finishes at index 4&lt;br&gt;&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;This matters because ObjectScript flattens JSON's richer type system on the way in. JSON &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;, and &lt;code&gt;null&lt;/code&gt; all become ObjectScript-friendly values &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, and &lt;code&gt;""&lt;/code&gt; when you read them with dot syntax or &lt;code&gt;%Get()&lt;/code&gt;. If you need to tell a genuine &lt;code&gt;null&lt;/code&gt; apart from an empty string apart from a key that was never set, &lt;code&gt;%GetTypeOf()&lt;/code&gt; is the reliable discriminator. There is also &lt;code&gt;%IsDefined()&lt;/code&gt;, but it returns false for unassigned members and true for both &lt;code&gt;""&lt;/code&gt; and &amp;nbsp;&lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;


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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4jhf0ls9hk7gn9uwchx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4jhf0ls9hk7gn9uwchx.png" alt=" " width="387" height="167"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyvqmadnf8atqn423j41k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyvqmadnf8atqn423j41k.png" alt=" " width="500" height="377"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;The date gotcha&lt;/h3&gt;
&lt;p&gt;Type flattening is behind one of the community's recurring puzzles. Export a persistent object whose &lt;code&gt;DOB&lt;/code&gt; is &lt;code&gt;1926-12-11&lt;/code&gt; and you may see &lt;code&gt;"DOB":31390&lt;/code&gt; in the result — that &lt;code&gt;31390&lt;/code&gt; is the internal &lt;code&gt;$HOROLOG&lt;/code&gt; day count, not a corrupted value. The same logic bites the other direction in dynamic SQL: if you pass a query a literal like &lt;code&gt;'1926-12-11'&lt;/code&gt; and get zero rows, it's usually because the column expects &lt;code&gt;$HOROLOG&lt;/code&gt; internal format. The fix is to convert on the way in with &lt;code&gt;$ZDATEH("1926-12-11", 3)&lt;/code&gt;. Whenever a date crosses the boundary between JSON, SQL, and stored objects, ask which representation each side expects.&lt;/p&gt;
&lt;h2&gt;Two things both called "array"&lt;/h2&gt;
&lt;p&gt;Here is a distinction that quietly causes bugs. &lt;code&gt;%DynamicArray&lt;/code&gt; is a &lt;em&gt;positional&lt;/em&gt; list, indexed from 0. But ObjectScript also has typed &lt;strong&gt;collection&lt;/strong&gt; properties, and the &lt;code&gt;array of&lt;/code&gt; collection is not a positional array at all — it's a &lt;strong&gt;dictionary&lt;/strong&gt; (a keyed map). Compare:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Property Tags As array of %String;   // a dictionary: key -&amp;gt; value&lt;br&gt;
Property Notes As list of %String;   // an ordered, positional list&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You access an &lt;code&gt;array of&lt;/code&gt; collection by key, not by position:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Do obj.Tags.SetAt("high", "priority")

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

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

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

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


</description>
      <category>sql</category>
      <category>tooling</category>
      <category>json</category>
      <category>javascript</category>
    </item>
    <item>
      <title>InterSystems for dummies – IRIS Vector Search (Part II)</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 30 Aug 2026 07:30:53 +0000</pubDate>
      <link>https://dev.to/intersystems/intersystems-for-dummies-iris-vector-search-part-ii-3l0o</link>
      <guid>https://dev.to/intersystems/intersystems-for-dummies-iris-vector-search-part-ii-3l0o</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fimukpqyxi2f37vaw87wj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fimukpqyxi2f37vaw87wj.png" alt=" " width="500" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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




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



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

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

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

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

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

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

&lt;/div&gt;



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




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




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

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

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

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

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

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

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

&lt;/div&gt;






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




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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



&lt;p&gt;This search gives us the following results:&lt;/p&gt;

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

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

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

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

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

&lt;/div&gt;



&lt;p&gt;This search gives us the following results:&lt;/p&gt;

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

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

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

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

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

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

&lt;/div&gt;



&lt;p&gt;Oh… surprise!! It gave us back the same results:&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;The created vector (reducing the text for obvious reasons) will resemble the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[-0.020053215324878693, 0.08766797184944153, 0.04118209332227707, 0.027171766385436058, -0.026769554242491722, -0.045022152364254,
…..
 0.038541924208402634, 0.018977241590619087, 0.03448185324668884, 0.10012426972389221, 0.08426760882139206, -0.09713190793991089]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if we use it to perform the search as we did previously, we will get the result below:&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;Here you have the result of searching for it in French &lt;em&gt;“Aujourd'hui, je suis très heureux”&lt;/em&gt;:&lt;/p&gt;

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

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

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

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

&lt;/div&gt;



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

&lt;p&gt;This is the result of using this model with the search in Spanish:&lt;/p&gt;

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

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

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

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




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

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




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

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




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




&lt;p&gt;The first time you ask about a feeling, it may take a little while, because it caches the model for future queries.&lt;/p&gt;

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

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

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

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

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

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

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

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




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




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

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

&lt;/div&gt;



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

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

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

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



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




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

&lt;/div&gt;


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

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

&lt;/div&gt;






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




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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

</description>
      <category>beginners</category>
      <category>python</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>InterSystems for dummies – IRIS Vector Search (Part I)</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 23 Aug 2026 15:35:13 +0000</pubDate>
      <link>https://dev.to/intersystems/intersystems-for-dummies-iris-vector-search-part-i-2995</link>
      <guid>https://dev.to/intersystems/intersystems-for-dummies-iris-vector-search-part-i-2995</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnwkbmacfzn2gnwsau9wj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnwkbmacfzn2gnwsau9wj.png" alt=" " width="500" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;p&gt;In a two-dimensional system, a vector is represented by its start (origin) and end (destination) coordinates on the X and Y axes.&lt;/p&gt;

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

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

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

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

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

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

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

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

&lt;p&gt;Conversely, "Bob Dylan" or "Genesis" will point in the opposite direction. They are still musical groups, but their styles do not have "the same direction".[&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;To determine if two vectors are close, we should measure the “Euclidean distance”, which is the straight-line length between two points in Euclidean space. It equals the square root of the sum of the squared differences between the point coordinates:&lt;/p&gt;

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

&lt;p&gt;In an n-dimensional space, it is measured as the difference between each of the dimensions squared.&lt;/p&gt;

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

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

&lt;p&gt;If we compare it with other vectors in the same space, those with the smallest values will be approximate vectors.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ybe3grtc5kntyap56xa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ybe3grtc5kntyap56xa.png" alt=" " width="800" height="580"&gt;&lt;/a&gt;&lt;br&gt;
The distance between v1 and v3 is less than the distance between v1 and v2.&lt;/p&gt;

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

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

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

&lt;p&gt;It is the distance from the vector base to its endpoint. In our case, all vectors originate at point 0, so the magnitude of a vector would be calculated using the following formula:&lt;/p&gt;

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

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

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

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

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

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

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

&lt;p&gt;It is calculated by multiplying the sum of the coordinates in each dimension of the vectors to be compared.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;It is calculated using the following formula:&lt;/p&gt;

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

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

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

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

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

&lt;/div&gt;



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




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




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

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

&lt;p&gt;As Foghorn Leghorn once said, “It’s math, son!! The numbers don’t lie.”&lt;/p&gt;

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

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

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

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

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

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




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




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

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

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

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

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

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

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

# Return results
return output
&lt;/code&gt;&lt;/pre&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;p&gt;And we can also use this to activate a PyProd adapterless Business Service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from intersystems_pyprod import director 

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

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

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

# Activate Business Service
service.process_input(n_stars)
&lt;/code&gt;&lt;/pre&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;h3&gt;Flask hosting&lt;/h3&gt;
&lt;p&gt;WSGI applications can be hosted through the Management Portal at System -&amp;gt; Security -&amp;gt; Applications:&lt;/p&gt;


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

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

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

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

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

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

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

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

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

</description>
      <category>python</category>
      <category>resources</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>iris-agentic-dev -- Give Your AI a Live Connection to IRIS, Part 1: The Problem, the Tool, and Getting Started</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Thu, 13 Aug 2026 09:35:07 +0000</pubDate>
      <link>https://dev.to/intersystems/iris-agentic-dev-give-your-ai-a-live-connection-to-iris-part-1-the-problem-the-tool-and-1kc0</link>
      <guid>https://dev.to/intersystems/iris-agentic-dev-give-your-ai-a-live-connection-to-iris-part-1-the-problem-the-tool-and-1kc0</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 1 of a series. Part 2 covers the full tool catalog. Part 3 covers ObjectScript skills. Part 4 covers benchmarking and measuring what actually improves.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem Hiding in the Comments
&lt;/h2&gt;

&lt;p&gt;Thomas Mazur's post &lt;a href="https://community.intersystems.com/post/frogs-chickens-ai-and-vs-code" rel="noopener noreferrer"&gt;&lt;em&gt;"Frogs, Chickens, AI, and VS Code" &lt;/em&gt;&lt;/a&gt;on VS Code productivity — Peacock, scoped workspace files, Copilot Agent mode — drew a sharper problem in the comments. Pietro Di Leo and Mike.W pointed out that when you work server-side in VS Code, the &lt;code&gt;isfs://&lt;/code&gt; workspace most production IRIS shops use, Copilot can only see the files &lt;strong&gt;open in your editor&lt;/strong&gt;. It cannot index the virtual filesystem. On a mature IRIS application with thousands of classes, the AI works through a keyhole.&lt;/p&gt;

&lt;p&gt;John Murray pointed people at a project I've been building — &lt;a href="https://github.com/intersystems-community/iris-agentic-dev" rel="noopener noreferrer"&gt;iris-agentic-dev&lt;/a&gt; — and noted no Developer Community article existed for it yet. So here it is: why the problem exists, how the tool addresses it, and how to get it running in about five minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the AI Can't See Your Namespace
&lt;/h2&gt;

&lt;p&gt;When you open an &lt;code&gt;isfs://&lt;/code&gt; workspace, your IRIS classes live on the server, not on disk. The VS Code ObjectScript extension streams them to you on demand via the Atelier API — open a class, it fetches it; save it, it writes back. This works beautifully for editing.&lt;/p&gt;

&lt;p&gt;AI assistants such as Copilot work differently. They need a picture of the code around the file you're editing. Who calls this method? What inherits from this class? What other code touches this global? On a local project, the assistant can scan the files to answer those questions. An &lt;code&gt;isfs://&lt;/code&gt; workspace materializes files only when you open them, so there is nothing complete to scan.&lt;/p&gt;

&lt;p&gt;For a new project with a handful of classes, that may be tolerable. For a production IRIS system — ten thousand classes, Ensemble productions, custom &lt;code&gt;%Library&lt;/code&gt; subclasses, business logic accumulated across years of development — the AI becomes nearly useless for the hard questions. It can help you write a new method if you paste in the surrounding context yourself. It cannot help you understand the system.&lt;/p&gt;

&lt;p&gt;Give the AI a different kind of connection, one that can ask IRIS directly instead of crawling the disk.&lt;/p&gt;




&lt;h2&gt;
  
  
  What iris-agentic-dev Is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;iris-agentic-dev&lt;/code&gt; is an &lt;strong&gt;MCP server&lt;/strong&gt; — a background process that gives AI assistants a set of tools they can call to interact with a live IRIS instance. It works with GitHub Copilot (via the VS Code extension), Claude Code, Cursor, and OpenCode. The IRIS instance can run natively on Windows or Linux, or in Docker.&lt;/p&gt;

&lt;p&gt;Once configured, the MCP server's tools are available directly from chat. VS Code 1.99 and later support MCP for Copilot Agent mode; Claude Code and OpenCode have supported it since launch.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;iris-agentic-dev&lt;/code&gt; connects to IRIS through the same Atelier REST API used by the ObjectScript extension. The assistant can then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search the entire namespace&lt;/strong&gt; — full-text, regex, by category, without opening anything&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compile classes&lt;/strong&gt; and get errors back with line numbers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run ObjectScript&lt;/strong&gt; and see the output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute SQL queries&lt;/strong&gt; against any namespace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Introspect class definitions&lt;/strong&gt; — properties, methods, parameters, inheritance chains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect Ensemble productions&lt;/strong&gt; — which items are running, what's wired to what, message bodies, business rule logic, and drift between the running config and source control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run unit tests&lt;/strong&gt; and report results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug&lt;/strong&gt; — map INT line numbers back to original source lines, pull error logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Part 2 covers the complete tool catalog. Instead of guessing from a few open tabs, the assistant can ask IRIS about the namespace itself.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Built With the Community
&lt;/h2&gt;

&lt;p&gt;I started the project after running into this limitation repeatedly in my own IRIS work. Community contributions have shaped it since — often from the same people who show up more than once. John Murray, who pointed people at this project in the Frogs and Chickens thread, also built the Server Manager authentication integration you'll use in Step 2 below: instead of typing credentials into a config file, the MCP server reads them straight from the OS keychain through the same &lt;code&gt;AuthenticationProvider&lt;/code&gt; the Server Manager extension itself uses. Dorian TETU has contributed fixes across search accuracy, source control elicitation, and surgical-edit diffs.&lt;/p&gt;

&lt;p&gt;The project is open source under the &lt;code&gt;intersystems-community&lt;/code&gt; GitHub organization. Contributions and bug reports are welcome, including "It doesn't work on my setup."&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started: VS Code + GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;If you already use VS Code with the InterSystems ObjectScript extension, this is the fastest path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;: VS Code, GitHub Copilot subscription, and the &lt;a href="https://marketplace.visualstudio.com/items?itemName=intersystems-community.vscode-objectscript" rel="noopener noreferrer"&gt;InterSystems ObjectScript extension&lt;/a&gt; (which you almost certainly already have).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Install the VS Code extension
&lt;/h3&gt;

&lt;p&gt;Search for &lt;strong&gt;iris-agentic-dev&lt;/strong&gt; in the VS Code Marketplace and install it. On first activation, the extension locates or downloads the MCP server binary: if you already have it on PATH (e.g. via &lt;code&gt;brew install iris-agentic-dev&lt;/code&gt;), it uses that; otherwise it downloads the right binary for your platform automatically. Either way, it registers itself with Copilot — no manual wiring required.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;The iris-agentic-dev tool set appears in Copilot's Agent mode after installation.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Verify the connection
&lt;/h3&gt;

&lt;p&gt;Open Copilot Chat and switch to &lt;strong&gt;Agent mode&lt;/strong&gt;. Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Call check_config and show me the result."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You should see your IRIS connection details — host, port, namespace, Atelier API version. If the &lt;a href="https://marketplace.visualstudio.com/items?itemName=intersystems-community.servermanager" rel="noopener noreferrer"&gt;InterSystems Server Manager&lt;/a&gt; extension is installed, &lt;code&gt;iris-agentic-dev&lt;/code&gt; finds your server configuration and retrieves credentials from the OS keychain automatically. The VS Code extension follows the active &lt;code&gt;objectscript.conn&lt;/code&gt;, so developers with several Server Manager entries keep using the connection selected for that workspace. When running the MCP server outside the VS Code extension, set &lt;code&gt;IRIS_SERVER_NAME&lt;/code&gt; to the corresponding key from &lt;code&gt;intersystems.servers&lt;/code&gt; if more than one server is configured. The &lt;code&gt;check_config&lt;/code&gt; result shows which connection is active and which other servers were detected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7k1t7tguy0e9a0cyca9o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7k1t7tguy0e9a0cyca9o.png" alt="Copilot displaying the result of the iris-agentic-dev check\_config tool" width="693" height="594"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;check_config&lt;/code&gt;&lt;em&gt; confirms the IRIS host, port, namespace, and connection source Copilot is using.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Ask something that requires the whole namespace
&lt;/h3&gt;

&lt;p&gt;Now try a question that would be difficult to answer from open tabs alone:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Search for all classes in this namespace that extend &lt;/em&gt;&lt;code&gt;%Persistent&lt;/code&gt;&lt;em&gt;. How many are there?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"What are the properties and methods on &lt;/em&gt;&lt;code&gt;MyApp.SomeClass&lt;/code&gt;&lt;em&gt;?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Compile &lt;/em&gt;&lt;code&gt;MyApp.*.cls&lt;/code&gt;&lt;em&gt; and show me any errors."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;None of these requires you to open the relevant files first. The assistant gets the answers from IRIS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started: Claude Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Install the binary&lt;/strong&gt; (Mac):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap intersystems-community/tap
brew &lt;span class="nb"&gt;install &lt;/span&gt;iris-agentic-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or download directly from the &lt;a href="https://github.com/intersystems-community/iris-agentic-dev/releases/latest" rel="noopener noreferrer"&gt;releases page&lt;/a&gt; for Mac Intel, Linux, or Windows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configure the connection.&lt;/strong&gt; Create &lt;code&gt;~/.iris-agentic-dev.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;host = "localhost"
web_port = 52773
username = "_SYSTEM"
password = "SYS"
namespace = "USER"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Register with Claude Code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--scope&lt;/span&gt; user iris-agentic-dev &lt;span class="nt"&gt;--&lt;/span&gt; iris-agentic-dev mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Call check_config and show me the result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Example: How &lt;em&gt;iris-agentic-dev&lt;/em&gt; tools support analyzing an IRIS Interoperability application
&lt;/h2&gt;

&lt;p&gt;Here is a real exchange against the &lt;a href="https://github.com/intersystems/irisdemo-demo-readmission" rel="noopener noreferrer"&gt;irisdemo-demo-readmission&lt;/a&gt; production — a health interop demo that processes hospital discharge events and scores patients for readmission risk.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"How does an ADT A03 discharge message flow through this production?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Step 1: find what's compiled.&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;iris_symbols("IRISDemo.*")
→ 31 classes: BO.*, BP.*, BS.*, DTL.*, Util.*, and more
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key classes: &lt;code&gt;IRISDemo.BP.ReadmissionRisk.Process&lt;/code&gt;, &lt;code&gt;IRISDemo.DTL.HL7Discharge&lt;/code&gt;, &lt;code&gt;IRISDemo.DTL.HL7Update&lt;/code&gt;, &lt;code&gt;IRISDemo.HISHL7v2FileFeedRoutingRule&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: find the router's rule.&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;extract_message_map_routing("IRISDemo.HISHL7v2FileFeedRoutingRule")
→ NOT_FOUND — Ens.Rule.Definition, not a routing table class
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Ens.Rule.Definition&lt;/code&gt; classes hold routing logic in XData. The tool can't map that structure, so read the class source directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iris_doc("IRISDemo.HISHL7v2FileFeedRoutingRule.cls") → XData rules:
  Rule 1: docName=ADT_A01 or ADT_A08  → transform DTL.HL7Update, target Readmission Risk Process
  Rule 2: docName=ADT_A03             → transform DTL.HL7Update, target Readmission Risk Process
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A03 discharges go through &lt;code&gt;IRISDemo.DTL.HL7Update&lt;/code&gt;, which stamps &lt;code&gt;UpdateMessageType="A03"&lt;/code&gt; on the request — that field is what lets the business process branch differently for discharges vs. admits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: map the business process.&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;extract_message_map_routing("IRISDemo.BP.ReadmissionRisk.Process")
→ kind: bpl, 4 outbound calls:
    Update Encounter          → LACE SOAP Operation
    Calculate Risk with LACE  → LACE SOAP Operation
    Calculate Risk with ML    → Readmission ML Model Consumer
    EMR Readmission Update    → HisDB Encounter Update Operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: get the full step tree.&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;docs_introspect("IRISDemo.BP.ReadmissionRisk.Process") → xdata_flow:
  Call:  Update Encounter          → LACE SOAP Operation
  Call:  Calculate Risk with LACE  → LACE SOAP Operation
  Call:  Calculate Risk with ML    → Readmission ML Model Consumer
  Call:  EMR Readmission Update    → HisDB Encounter Update Operation  [async]
  If:    Discharge OK?
           (request.UpdateMessageType = "A03") &amp;amp;&amp;amp; (context.UpdateEncounterResult = 1)
    If:  Risk Alert?
           (context.RiskScore &amp;gt; 11) || (context.MLReadmissionRisk &amp;gt; 0.15)
      assign: Compose Alert Message
      Call:  Add Patient to Risk Program  → Care Team  [async]
      Call:  Alert Care Team              → Risk Alert Email Operation
      sync:  Follow up SLA 2 days
      If:    No follow up?  (synctimedout)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session also flagged that &lt;code&gt;IRISDemo.DTL.HL7Discharge&lt;/code&gt; exists and maps 9 HL7 fields to a &lt;code&gt;DischargeRequest&lt;/code&gt; — but the routing rule never sends A03 through it. Dead code, spotted without opening a file.&lt;/p&gt;

&lt;p&gt;The full exchange — every tool call, response, and reasoning step — is in this &lt;a href="https://gist.github.com/isc-tdyar/58a19b90f604fa786eab555815283dde" rel="noopener noreferrer"&gt;GitHub Gist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In four steps the assistant answered the question: A03 discharges hit the router, get transformed into an &lt;code&gt;UpdateEncounterRequest&lt;/code&gt; with the trigger event stamped as the branch signal, and the business process runs LACE and ML risk scoring in sequence — alerting the care team and starting a 2-day follow-up if either score exceeds the threshold. No files were open. Everything came from IRIS.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in the Next Parts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Part 2 — The Tools&lt;/strong&gt;: A practical walkthrough of the tool catalog: what each tool does, when to use it, and which IRIS-specific problems it solves. The search, introspection, and Ensemble tools are especially useful for questions that open editor buffers cannot answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3 — Skills&lt;/strong&gt;: A live connection does not fix an AI model's weak grasp of ObjectScript: subtle syntax differences, &lt;code&gt;%Status&lt;/code&gt; propagation, &lt;code&gt;$$$&lt;/code&gt; macros, and COS-specific idioms that are scarce in general training data. Skills are short instruction files that target these weaknesses. On my 22-task ObjectScript repair suite, a 205-word checklist called &lt;code&gt;objectscript-review&lt;/code&gt; took the pass rate from 73% to 100% against Claude Sonnet 4.6 — a single run on a small public suite, with all the caveats that implies. Part 3 covers what the skills do; Part 4 covers how much to trust the number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4 — Benchmarking&lt;/strong&gt;: How the benchmark harness works, how to run it, and what the numbers mean. That includes where skills help, where they have no effect, and at least one that appears to &lt;em&gt;hurt&lt;/em&gt; performance when loaded globally — more instructions are not always better. It also covers the limits of a suite this size: contamination risk from public tasks, single-run variance, and why a lift measured on one model says little about another.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/intersystems-community/iris-agentic-dev" rel="noopener noreferrer"&gt;intersystems-community/iris-agentic-dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VS Code extension&lt;/strong&gt;: &lt;a href="https://marketplace.visualstudio.com/items?itemName=intersystems-community.vscode-iris-agentic-dev" rel="noopener noreferrer"&gt;iris-agentic-dev for IRIS on the Marketplace&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binaries&lt;/strong&gt; (Mac, Linux, Windows): &lt;a href="https://github.com/intersystems-community/iris-agentic-dev/releases/latest" rel="noopener noreferrer"&gt;releases page&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original thread&lt;/strong&gt;: &lt;a href="https://community.intersystems.com/post/frogs-chickens-ai-and-vs-code" rel="noopener noreferrer"&gt;Frogs, Chickens, AI, and VS Code&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Thomas Dyar — Sr. Manager AI Platform &amp;amp; Ecosystem, InterSystems, &lt;/em&gt;&lt;code&gt;iris-agentic-dev&lt;/code&gt;&lt;em&gt; is open source under the intersystems-community GitHub organization.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>ai</category>
      <category>coding</category>
      <category>tooling</category>
    </item>
    <item>
      <title>AI-Assisted Development on IRIS: Beginner tips and guidance</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Thu, 13 Aug 2026 09:32:54 +0000</pubDate>
      <link>https://dev.to/intersystems/ai-assisted-development-on-iris-beginner-tips-and-guidance-15id</link>
      <guid>https://dev.to/intersystems/ai-assisted-development-on-iris-beginner-tips-and-guidance-15id</guid>
      <description>&lt;blockquote&gt;&lt;p&gt;This article provides an AI agent-agnostic view of developing with AI coding assistants, with an IRIS-specific focus. This guidance is based on standards that can be used whether you use Claude Code, GitHub Copilot, Codex, or one of the other coding agents. As such, it doesn't cover setup instructions. Instead, it covers key concepts and how they have improved the performance of AI for my work.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Hi everyone, I wanted to share some thoughts, advice and examples of using AI for IRIS development. Before starting though, I have lots of opinions on AI, far more than I can fit in this article. I am not an AI evangelist. I started as a skeptic and still hold on to a lot of skepticism, along with an intense dislike for slop. I am, however, an absolute believer in the ability of these tools if you can learn to use them correctly.&lt;/p&gt;
&lt;p&gt;I use AI regularly, but make a conscious effort to follow, understand and review any code that isn't a toy project or personal tool. My opinions on AI coding softened dramatically when I first tried vibe-coding for a personal tool (a presentation app which displays web-pages directly alongside slides) and was amazed by success. I recommend next time you think "I wish I had an application/vs code extension/tool which does x", try asking an AI agent for it.&lt;/p&gt;
&lt;p&gt;Of course, the correct approach will vary for each user and each scenario. People working with sensitive data in production database should be far more careful about using AI than I am creating demo projects in isolated containers. As such, I can't claim my experience will be the same as anyone else, but I do suggest taking some time to explore approaches that might work for you.&lt;/p&gt;
&lt;h2&gt;"I tried using AI to write ObjectScript but it hallucinated all the methods"&lt;/h2&gt;
&lt;p&gt;AI agents have improved a lot, even since I started at InterSystems in September 2025. Developer circles often talk about the &lt;a href="https://martinfowler.com/bliki/NovemberInflection.html" rel="noopener nofollow noreferrer"&gt;November inflection point&lt;/a&gt; where the release of Claude Opus 4.5 and GPT-5.2 changed the game with agentic coding. Suddenly models had the ability (and context window) to chain together tool calls to develop, reason, and fix bugs. These models have been incrementally improving with each release cycle since and may well continue to do so.&lt;/p&gt;
&lt;p&gt;In this time, ObjectScript coding from LLMs has improved &lt;strong&gt;dramatically&lt;/strong&gt;. I recommend you re-evaluate any belief about the ability of AI in coding matters that pre-dates these releases, and try again with one of the latest (and most powerful) models in a coding harness.&lt;/p&gt;
&lt;h2&gt;Improving your agent's skillset&lt;/h2&gt;
&lt;p&gt;Frontier AI models (and their lesser counterparts) have a pretty good very good knowledge of most matters, essentially because they have consumed the entire internet. It is true that their ObjectScript knowledge is often less than other languages, primarily because there is less ObjectScript available on the web than other languages.&lt;/p&gt;
&lt;p&gt;This is where &lt;a href="https://agentskills.io/home" rel="noopener nofollow noreferrer"&gt;agent skills&lt;/a&gt; come in.&lt;/p&gt;
&lt;p&gt;Skills are markdown files with some YAML front-matter, it's (basically) that simple. In the metadata, you give the skill a name, and a description, which crucially should say when an agent should use the skill. The name and description are loaded into the agent's context window, and when they need the specific guidance the skill provides, they can activate the skill and use the markdown contents. This is great for specific knowledge, or advice, for example on a lesser used part of a coding language they are a bit ropey at.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/intersystems-community/iris-agentic-dev/" rel="noopener nofollow noreferrer"&gt;iris-agentic-dev&lt;/a&gt;, the MCP server that &lt;span&gt;&lt;span&gt;@tomd&lt;/span&gt;&lt;/span&gt; recently &lt;a href="https://community.intersystems.com/post/iris-agentic-dev-give-your-ai-live-connection-iris-part-1-problem-tool-and-getting-started" rel="noopener nofollow noreferrer"&gt;shared with the community&lt;/a&gt; includes a skill library. One of the skills, &lt;span&gt;&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/timothy"&gt;@timothy&lt;/a&gt;.Leavitt&lt;/span&gt;&lt;/span&gt;'s &lt;a href="https://github.com/intersystems-community/iris-agentic-dev/tree/master/skills/skills/objectscript-review" rel="noopener nofollow noreferrer"&gt;objectscript-review&lt;/a&gt;, only includes a checklist of the 10 most common ObjectScript errors, and yet dramatically includes benchmark performance. This skill obviously required extensive ObjectScript knowledge in the first place, alongside experience reviewing agent written ObjectScript to know the most common mistakes. But the benefit is the agent should now correct these mistakes itself, and the future code it produces will be better as a result.&lt;/p&gt;
&lt;p&gt;Skills are reusable pockets of information. If you have a repeatable task an agent struggles with, let it struggle the first time, then when it gets to the right answer (maybe with your guidance) say "write a skill that covers the mistakes you've made here". I recommend reviewing and editing the generated skills carefully because agents tend to put in project specific information which doesn't generalise. But as long as the problems can be generalisable, the solutions can be reusable. Next time, the agents won't make the same mistakes (...&lt;em&gt;probably&lt;/em&gt;, there's no certainty with non-deterministic models).&lt;/p&gt;
&lt;h2&gt;Improving your agent's toolset&lt;/h2&gt;
&lt;p&gt;Skills are great to prevent repeated errors, but they don't help the agent actually do things for you. This is where tools come in. Tools, in the context of AI agents, are functions which the agent can call using structured (JSON) responses. The standard protocol for adding tools to agents is the Model Context Protocol (MCP). This has been written about extensively on the community including articles by &lt;a href="https://community.intersystems.com/post/introduction-ai-hub-part-2-custom-mcp-servers" rel="noopener nofollow noreferrer"&gt;me&lt;/a&gt;, &lt;a href="https://community.intersystems.com/post/model-context-protocol-mcp-intersystems-iris-zero-hero" rel="noopener nofollow noreferrer"&gt;Pietro Di Leo&lt;/a&gt; and &lt;a href="https://community.intersystems.com/post/iris-agentic-dev-give-your-ai-live-connection-iris-part-1-problem-tool-and-getting-started" rel="noopener nofollow noreferrer"&gt;Tom&lt;/a&gt;. I will also once again plug this &lt;a href="https://www.youtube.com/watch?v=pieK0dog66Q" rel="noopener noreferrer"&gt;great intro video&lt;/a&gt; from InterSystems President Don Woodlock.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;MCP servers can be used to expose business logic to external agents or connectors (see my &lt;a href="https://community.intersystems.com/post/introduction-ai-hub-part-2-custom-mcp-servers" rel="noopener nofollow noreferrer"&gt;introduction to MCP servers in AI Hub&lt;/a&gt;). However, to date at least, MCP servers are most commonly used as developer tools. This is the context I want to discuss here.&lt;/p&gt;
&lt;p&gt;If you tell an LLM to write some ObjectScript using an obscure class, it will likely hallucinate how a statistically average version of how the class might look. Instead, if you give an agent with access to a tool to search the class reference documentation, it will likely look up the proper syntax before writing. If you give it access to an ObjectScript shell (or execution environment), it may even test the function syntax before writing.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Try to imagine you were asked to code something from scratch using an obscure library and think about what you would need to do a good job. It would probably include documentation and a feedback loop to test the code you've written. Coding agents require exactly the same. &amp;nbsp;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;This is the value of MCP servers. Agents can autonomously search for proper syntax, test functions and code, and try to compile classes. When they see an error, they can read the error trace and investigate. They can execute more code to find the bugs, and search for other classes for proper syntax.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/intersystems-community/iris-agentic-dev" rel="noopener nofollow noreferrer"&gt;iris-agentic-dev&lt;/a&gt; is an MCP server which provides &lt;a href="https://github.com/intersystems-community/iris-agentic-dev/blob/master/docs/tools.md" rel="noopener nofollow noreferrer"&gt;many, many tools&lt;/a&gt; to do different functions in IRIS. If you are a system administrator managing multiple IRIS servers, it has tools to view and edit the roles, servers and logs. If you are a developer, the tools to execute ObjectScript, search existing classes or read production logs, might be more relevant to you.&lt;/p&gt;
&lt;h2&gt;Customisation&lt;/h2&gt;
&lt;p&gt;Many people have their own golden rules for using AI coding agents, like "tests are essential", "create and review specification docs before touching code", "always review the output", "anything intended to be read by a human should be written by a human". These are all valid (and good advice), but for me, there's one golden rule that is more important.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Customisation is key.&lt;/strong&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;For agents to be useful for your use case, they need to be customised to your use case. Let's look at some ways to do this.&amp;nbsp;&lt;/p&gt;
&lt;h4&gt;Skills&lt;/h4&gt;
&lt;p&gt;Skills are a great way to customise. I have a library of agent skills to cover my own preferences. For example I have skills make code more readable, to write check scripts for interactive tutorials, and to use PyProd to write productions in Python (this one has made it into &lt;a href="" rel="noopener nofollow"&gt;iris-agentic-dev's skill library&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I also have skills which other's have recommended, e.g. &lt;a href="https://github.com/obra/superpowers" rel="noopener nofollow noreferrer"&gt;obra/superpowers&lt;/a&gt;, that have been customised to my preferences. They are just markdown files after all. Take the bits you like, and add/remove sections to get the instructions right for your work.&lt;/p&gt;
&lt;h4&gt;iris-agentic-dev&lt;/h4&gt;
&lt;p&gt;For iris-agentic-dev, this customisation may come in the form of restricting tools that aren't generally useful to you with the &lt;code&gt;IRIS_DISABLED_TOOLS&lt;/code&gt; argument. For example, I have turned off server-side source control tools because I always do source control on the client side, and otherwise agents see it and think that means they should use it. I've also turned off the system admin tools because 95% of my IRIS usage is done with SuperUser so role management is totally not required for my use case. For others, managing different users and using server-side source control will be incredibly valuable whereas giving an agent execution power might be too risky, so they might make the opposite decisions. &amp;nbsp;&lt;/p&gt;
&lt;p&gt;I've also created custom iris-agentic-dev instructions in the form of a skill, with the description "Use this skill when you want to connect to a running IRIS instance". This skill includes the line &lt;code&gt;VS CODE COMPILES THE CLASS ON SAVE, YOU DO NOT NEED TO MANUALLY COMPILE CLASSES&lt;/code&gt;, because I've watched agents go in circles trying to copy a file into a docker container then compile it, only to find out its "suspiciously" already up-to-date. This use case-specific customisation informed by watching agents proves valuable in the long term.&lt;/p&gt;
&lt;h4&gt;AGENTS.MD&lt;/h4&gt;
&lt;p&gt;Finally, you can also customise your agents with AGENTS.MD (or CLAUDE.MD) files, either at a project root or the global config. The contents of these are automatically sent to the agent with every new conversation.&lt;/p&gt;
&lt;p&gt;Project AGENTS.MD are good for project specific information, e.g. &lt;code&gt;This project does x, the source files are in ./src/package, port 52773 on IRIS is mapped to port 62783&lt;/code&gt;. That way, each new conversation I have with an agent doesn't require an introduction, and the agent doesn't have to search through the whole project to find the relevant files.&lt;/p&gt;
&lt;p&gt;Global versions are for rules you want the agent always to abide, for example my global CLAUDE.MD includes &lt;code&gt;NEVER USE EMOJIS ANYWHERE NOT EVEN TICK MARKS&lt;/code&gt; about 3 times. I've also included some more useful guidance, e.g. &lt;code&gt;Give a (very brief) explanation of tool calls or series of tool calls to ensure I can follow what you are trying to do&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Conclusions&lt;/h4&gt;
&lt;p&gt;For any customisation, you need to try using agents. See what works and what doesn't, what an agent is good at and bad at. If an agent does something you don't like, add instructions not to do it again in one of the places mentioned above.&lt;/p&gt;
&lt;p&gt;Finally, in order to customise, you also need to know what your use case requires. You need to learn with the agent and know what you are directing the agent to do. There are times when letting the agent run autonomously is incredibly powerful, but in general, I prefer keeping a close eye on what the agent is doing, learning from the outputs, and course-correcting when it inevitably does something stupid.&lt;/p&gt;
&lt;h2&gt;Final notes&lt;/h2&gt;
&lt;p&gt;This article has been long, opinionated, and maybe a touch ramble-y, but I hope it has been interesting to some readers. If you would like to hear more on these topics, on opinionated guidance for using AI-assisted coding, or just need help getting started with skills, agents or iris-agentic-dev, feel free to reach out in the comments.&lt;/p&gt;


</description>
      <category>ai</category>
      <category>programming</category>
      <category>ux</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a Pure Python Healthcare Interoperability Production with PyProd and the openFDA API</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Fri, 31 Jul 2026 20:45:47 +0000</pubDate>
      <link>https://dev.to/intersystems/building-a-pure-python-healthcare-interoperability-production-with-pyprod-and-the-openfda-api-28nl</link>
      <guid>https://dev.to/intersystems/building-a-pure-python-healthcare-interoperability-production-with-pyprod-and-the-openfda-api-28nl</guid>
      <description>&lt;p&gt;Hi Developers!&lt;/p&gt;
&lt;p&gt;In my previous article, &lt;a href="https://community.intersystems.com/post/45-second-production-testing-chatgpt%E2%80%99s-limits-intersystems-iris-and-pyprod" rel="noopener noreferrer"&gt;&lt;strong&gt;Electric Utility PyProd&lt;/strong&gt;&lt;/a&gt;, we built a simple interoperability production that processed CSV files from a local directory using the PyProd framework. While that example introduced the basic building blocks of a production implemented entirely in Python, real-world interoperability solutions often need to communicate with external systems.&lt;/p&gt;
&lt;p&gt;In this article, we'll build a more advanced production that retrieves healthcare data from the public &lt;a href="https://api.fda.gov/drug/event.json" rel="noopener noreferrer"&gt;&lt;strong&gt;openFDA Drug Adverse Event API&lt;/strong&gt;&lt;/a&gt;, analyzes the returned data, and stores healthcare analytics in InterSystems 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkljhunyf57bgjx8f440e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkljhunyf57bgjx8f440e.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Along the way, we'll see how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;call an external REST API from a PyProd Inbound Adapter&lt;/li&gt;
&lt;li&gt;work with large JSON payloads efficiently&lt;/li&gt;
&lt;li&gt;perform batch-level healthcare analytics&lt;/li&gt;
&lt;li&gt;persist the results in an IRIS SQL table&lt;/li&gt;
&lt;li&gt;implement an entire interoperability production using only Python&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Setting up&lt;/h1&gt;
&lt;p&gt;Clone the repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone &lt;a href="https://github.com/Gra-ach/openfda-healthcare-pyprod.git" rel="noopener noreferrer"&gt;https://github.com/Gra-ach/openfda-healthcare-pyprod.git&lt;/a&gt;&lt;br&gt;
cd openfda-healthcare-pyprod&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the container:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up --build -d&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the container is running, load the production:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;intersystems_pyprod src/healthcare-openfda-pyprod/openfda_adverse_events.py&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Step 1 – Create the Inbound Adapter&lt;/h1&gt;
&lt;p&gt;Unlike the previous example, this production doesn't wait for files to appear in a directory. Instead, the custom &lt;code&gt;OpenFDAInboundAdapter&lt;/code&gt; periodically connects to the public &lt;code&gt;openFDA Drug Adverse Event API&lt;/code&gt;. Rather than querying the same medication every time, the adapter rotates through a configurable list of common medications:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ASPIRIN&lt;br&gt;
IBUPROFEN&lt;br&gt;
ACETAMINOPHEN&lt;br&gt;
NAPROXEN&lt;br&gt;
LORATADINE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each polling cycle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;selects the next medication&lt;/li&gt;
&lt;li&gt;builds the REST query&lt;/li&gt;
&lt;li&gt;downloads the latest adverse-event reports&lt;/li&gt;
&lt;li&gt;saves the response as a JSON file&lt;/li&gt;
&lt;li&gt;sends a lightweight production message&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Saving the payload to disk instead of passing it through the production keeps messages small and avoids IRIS string-size limitations.&lt;/p&gt;


&lt;pre&gt;&lt;code&gt;class OpenFDAInboundAdapter(InboundAdapter):
    api_base_url: str = IRISProperty(
        description="openFDA Drug Adverse Event API URL",
        settings="API Settings"
    )
    result_limit: int = IRISProperty(
        description="Number of records to retrieve per poll",
        settings="API Settings"
    )
    api_key: str = IRISProperty(
        description="Optional openFDA API key",
        settings="API Settings"
    )
    payload_dir: str = IRISProperty(
        description="Directory where raw openFDA JSON payloads are saved",
        settings="File Settings"
    )
    medication_names: str = IRISProperty(
        description="Comma-separated medication names to rotate through",
        settings="API Settings"
    )  

    def on_task(self):
        os.makedirs(self.payload_dir, exist_ok=True)

        meds = [
            med.strip().upper()
            for med in str(self.medication_names or "").split(",")
                if med.strip()
        ]

        if not meds:
            meds = ["ASPIRIN", "IBUPROFEN", "ACETAMINOPHEN", "NAPROXEN", "LORATADINE"]

        state_file = f"{OPENFDA_ROOT}/last_med_index.txt"
        os.makedirs(OPENFDA_ROOT, exist_ok=True)

        try:
            with open(state_file, "r", encoding="utf-8") as f:
                last_index = int(f.read().strip())
        except Exception:
            last_index = -1

        next_index = (last_index + 1) % len(meds)
        selected_med = meds[next_index]

        with open(state_file, "w", encoding="utf-8") as f:
            f.write(str(next_index))

        search_query = f'patient.drug.medicinalproduct:"{selected_med}"'

        params = {
            "search": search_query,
            "limit": int(self.result_limit or 50),
            "sort": "receiptdate:desc",
        }
        if self.api_key:
            params["api_key"] = self.api_key

        url = f"{self.api_base_url}?{urllib.parse.urlencode(params)}"
        pulled_at = datetime.utcnow().isoformat(timespec="seconds")
        safe_timestamp = pulled_at.replace(":", "").replace("-", "")
        pulled_at = pulled_at.replace("T", " ")
        payload_file_path = os.path.join(
            self.payload_dir,
            f"openfda_adverse_events_{safe_timestamp}.json",
        )

        IRISLog.Info(f"Calling openFDA API: {url}")

        try:
            with urllib.request.urlopen(url, timeout=30) as response:
                payload_bytes = response.read()

            with open(payload_file_path, "wb") as payload_file:
                payload_file.write(payload_bytes)
        except Exception as ex:
            IRISLog.Error(f"openFDA API call or payload write failed: {ex}")
            return Status.Error()

        msg = AdverseEventFileMessage(
            api_url=url,
            search_query=search_query,
            pulled_at=pulled_at,
            payload_file_path=payload_file_path,
            medicine = selected_med
        )
        self.business_host_process_input(msg)
        return Status.OK()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see the events in the log for the inbound adapter, each time info about a different medicine is requested:&lt;/p&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/yben0tbb988bqfkld7xd.png)

&lt;h1&gt;Step 2 – Create the Business Service&lt;/h1&gt;
&lt;p&gt;The Business Service is intentionally simple. Its only responsibility is forwarding the metadata produced by the adapter to the Business Process.&lt;/p&gt;
&lt;p&gt;Notice that we're no longer passing the entire JSON document between production components. The message &lt;code&gt;AdverseEventFileMessage&lt;/code&gt; that is forwarded between the &lt;code&gt;OpenFDAEventService&lt;/code&gt; and &lt;code&gt;OpenFDAAnalysisProcess&lt;/code&gt; &amp;nbsp;contains only:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API URL&lt;/li&gt;
&lt;li&gt;search query&lt;/li&gt;
&lt;li&gt;selected medication&lt;/li&gt;
&lt;li&gt;timestamp&lt;/li&gt;
&lt;li&gt;payload file path&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class AdverseEventFileMessage(JsonSerialize): 
    api_url: str = Column() 
    search_query: str = Column() 
    pulled_at: str = Column() 
    payload_file_path: str = Column() 
    medicine: str = Column()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps production efficient while preserving access to the original API response.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class OpenFDAEventService(BusinessService): 
    ADAPTER: str = IRISParameter( 
        value="HealthOps.OpenFDAInboundAdapter", 
        description="Pure Python CSV polling adapter" 
    ) 
    process_target: str = IRISProperty( 
        description="Business process target", 
        settings="Target Settings" 
    ) 

    def on_process_input(self, input): 
        return self.send_request_async(self.process_target, input)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here are the messages in the Message Viewer:&lt;/p&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/bx3sbri80t9ugrl041q4.png)
&lt;h1&gt;Step 3 – Create the Business Process&lt;/h1&gt;
&lt;p&gt;The Business Process performs the analytics. Using the payload file path, it reloads the JSON response and analyzes the complete batch of adverse-event reports.&lt;/p&gt;
&lt;p&gt;For every API batch it calculates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;total number of reports&lt;/li&gt;
&lt;li&gt;serious adverse-event count&lt;/li&gt;
&lt;li&gt;serious adverse-event percentage&lt;/li&gt;
&lt;li&gt;death count&lt;/li&gt;
&lt;li&gt;hospitalization count&lt;/li&gt;
&lt;li&gt;average patient age&lt;/li&gt;
&lt;li&gt;most frequently reported reaction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unlike the Electric Utility example, which generated one database row per input record, this production generates one analytics message representing the entire batch.&lt;/p&gt;
&lt;p&gt;This greatly reduces message traffic while still preserving all meaningful statistics.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class OpenFDAAnalysisProcess(BusinessProcess): 
    operation_target: str = IRISProperty( 
        description="Operation that persists rows and analysis results", 
        settings="Target Settings" 
    ) 

    def _age_to_years(self, age_value, age_unit) -&amp;gt; Optional[float]: 
        if age_value in ("", None): 
            return None 
        try: 
            age = float(age_value) 
        except Exception: 
            return None 
        unit = str(age_unit or "") 
        if unit == "801": 
            return age 
        if unit == "802": 
            return age / 12.0 
        if unit == "803": 
            return age / 52.0 
        if unit == "804": 
            return age / 365.0 
        if unit == "805": 
            return age / 8760.0 
        return age 

    def _load_records(self, payload_file_path: str): 
        with open(payload_file_path, "r", encoding="utf-8") as payload_file: 
            payload = json.load(payload_file) 
        return payload.get("results", []) 

    def on_request(self, request): 
        try: 
            records = self._load_records(request.payload_file_path) 
        except Exception as ex: 
            IRISLog.Error(f"Failed to read openFDA payload file {request.payload_file_path}: {ex}") 
            return Status.Error() 

        record_count = len(records) 
        serious_count = 0 
        death_count = 0 
        hospitalization_count = 0 
        ages: List[float] = [] 
        reaction_counts: Dict[str, int] = {} 

        for record in records: 
            if str(record.get("serious", "")) == "1": 
                serious_count += 1 
            if str(record.get("seriousnessdeath", "")) == "1": 
                death_count += 1 
            if str(record.get("seriousnesshospitalization", "")) == "1": 
                hospitalization_count += 1 

            patient = record.get("patient", {}) or {} 

            age_years = self._age_to_years( 
                patient.get("patientonsetage"), 
                patient.get("patientonsetageunit"), 
            ) 
            if age_years is not None: 
                ages.append(age_years) 

            for reaction in patient.get("reaction", []) or []: 
                term = reaction.get("reactionmeddrapt") 
                if term: 
                    reaction_counts[term] = reaction_counts.get(term, 0) + 1 

        serious_rate_pct = round((serious_count / record_count) * 100, 2) if record_count else 0.0 
        avg_patient_age_years = round(sum(ages) / len(ages), 2) if ages else 0.0 

        top_reaction = "" 
        top_reaction_count = 0 
        if reaction_counts: 
            top_reaction, top_reaction_count = max(reaction_counts.items(), key=lambda item: item[1]) 

        analysis = AdverseEventAnalysisMessage( 
            api_url=request.api_url, 
            search_query=request.search_query, 
            pulled_at=request.pulled_at, 
            payload_file_path=request.payload_file_path, 
            record_count=record_count, 
            serious_count=serious_count, 
            serious_rate_pct=serious_rate_pct, 
            death_count=death_count, 
            hospitalization_count=hospitalization_count, 
            avg_patient_age_years=avg_patient_age_years, 
            top_reaction=top_reaction, 
            top_reaction_count=top_reaction_count, 
            medicine = request.medicine 
        ) 

        IRISLog.Info( 
            f"Batch analysis complete: " 
            f"medicine={request.medicine}, " 
            f"records={record_count}, " 
            f"serious={serious_count}, " 
            f"serious_rate={serious_rate_pct}%, " 
            f"deaths={death_count}, " 
            f"hospitalizations={hospitalization_count}, " 
            f"avg_age={avg_patient_age_years}, " 
            f"top_reaction={top_reaction}" 
        ) 

        return self.send_request_async(self.operation_target, analysis, response_required=0)&lt;/code&gt;
&lt;/pre&gt;

&lt;p&gt;Here we can see the incoming and outgoing messages for the Business Process:&lt;/p&gt;

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

&lt;p&gt;Here's the trace of messages for the whole cycle:&lt;/p&gt;

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

&lt;p&gt;The message &lt;code&gt;AdverseEventAnalysisMessage&lt;/code&gt; contains aggregated info about the received info about the concrete medicine:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;class AdverseEventAnalysisMessage(JsonSerialize): 
    api_url: str = Column() 
    search_query: str = Column() 
    pulled_at: str = Column() 
    payload_file_path: str = Column() 
    record_count: int = Column() 
    serious_count: int = Column() 
    serious_rate_pct: float = Column() 
    death_count: int = Column() 
    hospitalization_count: int = Column() 
    avg_patient_age_years: float = Column() 
    top_reaction: str = Column() 
    top_reaction_count: int = Column() 
    medicine: str = Column()&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And here's the log of the Business Process with the results of calculations:&lt;/p&gt;

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

&lt;h1&gt;Step 4 – Create the Business Operation&lt;/h1&gt;
&lt;p&gt;The Business Operation receives the batch analysis and stores it in InterSystems IRIS. If the SQL table doesn't exist yet, it is created automatically.&lt;/p&gt;
&lt;p&gt;Each API request produces one row containing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;medication name&lt;/li&gt;
&lt;li&gt;search query&lt;/li&gt;
&lt;li&gt;API URL&lt;/li&gt;
&lt;li&gt;payload file path&lt;/li&gt;
&lt;li&gt;number of retrieved reports&lt;/li&gt;
&lt;li&gt;serious-event statistics&lt;/li&gt;
&lt;li&gt;hospitalization statistics&lt;/li&gt;
&lt;li&gt;average patient age&lt;/li&gt;
&lt;li&gt;top reported reaction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once the insert succeeds, the JSON payload is moved into the archive directory.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class OpenFDADBOperation(BusinessOperation): 
    archive_payload_dir: str = IRISProperty( 
        description="Directory where successfully processed JSON payloads are archived", 
        settings="Operation Settings" 
    ) 

    message_map = { 
        f"{iris_package_name}.AdverseEventAnalysisMessage": "save_events" 
    } 

    def _ensure_table(self): 
        ddl = """ 
        CREATE TABLE IF NOT EXISTS HealthOps.OpenFDAAdverseEvents ( 
            id INTEGER IDENTITY PRIMARY KEY,             
            payload_file_path VARCHAR(1000), 
            api_url LONGVARCHAR, 
            search_query VARCHAR(1000), 
            pulled_at TIMESTAMP, 
            batch_record_count INTEGER, 
            batch_serious_count INTEGER, 
            batch_serious_rate_pct NUMERIC(6,2), 
            batch_death_count INTEGER, 
            batch_hospitalization_count INTEGER, 
            batch_avg_patient_age_years NUMERIC(10,2), 
            batch_top_reaction VARCHAR(255), 
            batch_top_reaction_count INTEGER, 
            medicine VARCHAR(60), 
            inserted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 
        ) 
        """ 
        iris.sql.prepare(ddl).execute()     
    
    def save_events(self, request): 
        self._ensure_table()       

        insert_sql = """ 
        INSERT INTO HealthOps.OpenFDAAdverseEvents ( 
            payload_file_path, 
            api_url, search_query, pulled_at, batch_record_count, 
            batch_serious_count, batch_serious_rate_pct, batch_death_count, 
            batch_hospitalization_count, batch_avg_patient_age_years, 
            batch_top_reaction, batch_top_reaction_count, medicine 
        ) 
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 
        """ 

        stmt = iris.sql.prepare(insert_sql) 
        
        stmt.execute( 
            request.payload_file_path, 
            request.api_url, 
            request.search_query, 
            request.pulled_at, 
            int(request.record_count), 
            int(request.serious_count), 
            float(request.serious_rate_pct), 
            int(request.death_count), 
            int(request.hospitalization_count), 
            float(request.avg_patient_age_years), 
            request.top_reaction, 
            int(request.top_reaction_count), 
            request.medicine 
        ) 
        
        os.makedirs(self.archive_payload_dir, exist_ok=True) 
        archive_path = os.path.join(self.archive_payload_dir, os.path.basename(request.payload_file_path)) 
        try: 
            os.replace(request.payload_file_path, archive_path) 
        except Exception as ex: 
            IRISLog.Warning( 
                f"Inserted records but could not archive payload file {request.payload_file_path}: {ex}" 
            ) 

        IRISLog.Info( 
            f"Inserted info about {request.medicine}: " 
            f"serious_rate={request.serious_rate_pct}%, " 
            f"death_count={request.death_count}, " 
            f"hospitalization_count={request.hospitalization_count}, " 
            f"avg_age={request.avg_patient_age_years}, " 
            f"top_reaction={request.top_reaction}" 
        ) 
        return Status.OK()&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here's the log of the Business Operation:&lt;/p&gt;


![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/xt1y9jjnlei5kkg50es2.png)

&lt;h1&gt;Step 5 – Define the Production&lt;/h1&gt;

&lt;p&gt;As with the previous example, the entire production is assembled directly in Python. It consists of:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;OpenFDAInboundAdapter
        ↓
OpenFDAEventService
        ↓
OpenFDAAnalysisProcess
        ↓
OpenFDADBOperation&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because everything is defined in Python, no manual production configuration is required after loading the project.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class OpenFDAHealthcareProduction(Production): 
    services = [ 
        ServiceItem( 
            "OpenFDAEventService", 
            "HealthOps.OpenFDAEventService", 
            host_settings={"process_target": "OpenFDAAnalysisProcess"}, 
            adapter_settings={ 
                "api_base_url": "https://api.fda.gov/drug/event.json", 
                "medication_names": "ASPIRIN,IBUPROFEN,ACETAMINOPHEN,NAPROXEN,LORATADINE", 
                "result_limit": 30, 
                "api_key": "", 
                "payload_dir": f"{OPENFDA_ROOT}/payloads" 
            } 
        ) 
    ] 
    processes = [ 
        ProcessItem( 
            "OpenFDAAnalysisProcess", 
            "HealthOps.OpenFDAAnalysisProcess", 
            host_settings={"operation_target": "OpenFDADBOperation"} 
        ) 
    ] 
    operations = [ 
        OperationItem( 
            "OpenFDADBOperation", 
            "HealthOps.OpenFDADBOperation", 
            host_settings={"archive_payload_dir": f"{OPENFDA_ROOT}/archive"} 
        ) 
    ] &lt;/code&gt;&lt;/pre&gt;

&lt;h1&gt;Step 6 – Run the Production&lt;/h1&gt;

&lt;p&gt;Start the production:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;python controls.py start HealthOps.OpenFDAHealthcareProduction
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Or from the IRIS terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zn "ENSEMBLE"
do ##class(Ens.Director).StartProduction("HealthOps.OpenFDAHealthcareProduction")
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or in the UI in Management Portal:&lt;/p&gt;

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

&lt;p&gt;Every polling cycle automatically selects the next medication and downloads a fresh batch of adverse-event reports.&lt;/p&gt;

&lt;h1&gt;Results&lt;/h1&gt;

&lt;p&gt;The production stores one summary row for every API call. For example:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;SELECT
    medicine,
    batch_record_count,
    batch_serious_rate_pct,
    batch_death_count,
    batch_hospitalization_count,
    batch_avg_patient_age_years,
    batch_top_reaction
FROM HealthOps.OpenFDAAdverseEvents
ORDER BY inserted_at DESC
&lt;/code&gt;&lt;/pre&gt;

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

&lt;p&gt;This makes it easy to compare medications over time and observe trends without storing thousands of individual adverse-event records.&lt;/p&gt;
&lt;h1&gt;A Note About Large JSON Payloads&lt;/h1&gt;
&lt;p&gt;One interesting challenge when integrating with public APIs is message size. Initially, I passed the complete JSON response between production components. While this works for small payloads, larger responses can exceed IRIS string limits and I got the &amp;lt;MAXSTRING&amp;gt; error. The solution used in this project is to store the API response as a JSON file and send only its location through the production.&lt;/p&gt;
&lt;p&gt;This approach has several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;avoids large interoperability messages&lt;/li&gt;
&lt;li&gt;preserves the original API response&lt;/li&gt;
&lt;li&gt;makes troubleshooting easier&lt;/li&gt;
&lt;li&gt;keeps the production lightweight&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's a useful design pattern whenever you're processing large REST responses or documents.&lt;/p&gt;
&lt;h1&gt;Summary&lt;/h1&gt;
&lt;p&gt;In this article, we've built a complete healthcare interoperability production in pure Python using PyProd.&lt;/p&gt;
&lt;p&gt;Compared to the previous &lt;strong&gt;Electric Utility PyProd&lt;/strong&gt; sample, this project demonstrates a more realistic integration scenario by communicating with an external REST service instead of processing local files.&lt;/p&gt;

&lt;p&gt;Along the way, we've seen how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;integrate with the public openFDA API&lt;/li&gt;
&lt;li&gt;rotate automatically between multiple medications&lt;/li&gt;
&lt;li&gt;process large JSON payloads efficiently&lt;/li&gt;
&lt;li&gt;calculate batch-level healthcare analytics&lt;/li&gt;
&lt;li&gt;archive source payloads&lt;/li&gt;
&lt;li&gt;create SQL tables automatically&lt;/li&gt;
&lt;li&gt;build an entire InterSystems IRIS production using only Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Although this example focuses on the openFDA API, the same architecture can easily be adapted to FHIR servers, public health services, hospital APIs, or any REST-based healthcare integration.&lt;/p&gt;

&lt;p&gt;I hope this project serves as a useful starting point for building more advanced Python interoperability solutions with PyProd.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>programming</category>
      <category>sql</category>
    </item>
    <item>
      <title>45-Second Production: Testing ChatGPT’s Limits with InterSystems IRIS and PyProd</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 28 Jul 2026 19:12:16 +0000</pubDate>
      <link>https://dev.to/intersystems/45-second-production-testing-chatgpts-limits-with-intersystems-iris-and-pyprod-5a34</link>
      <guid>https://dev.to/intersystems/45-second-production-testing-chatgpts-limits-with-intersystems-iris-and-pyprod-5a34</guid>
      <description>&lt;p&gt;It all started on a train ride to visit my parents, while I was chatting with a neighbor in my compartment. As it usually goes, the talk turned to technology, and she threw out a highly specific question: *Could ChatGPT be used to analyze the human genome?* I was highly skeptical that it could pull off something that complex. But the question lingered, burrowing into my mind. By the time I walked through my front door, my skepticism had transformed into a challenge. I didn't have a genome sequencing dataset on hand, but I did want to see if standard ChatGPT could build a functional Interoperability Production from scratch using the &lt;a href="https://github.com/intersystems/pyprod" rel="noopener noreferrer"&gt;PyProd package&lt;/a&gt;. Besides, that would give me the chance to participate in the &lt;a href="https://community.intersystems.com/post/community-bounty-program-idea-application-%E2%80%94-round-1-live" rel="noopener noreferrer"&gt;1st round&lt;/a&gt; of the &lt;a href="https://community.intersystems.com/post/introducing-community-bounty-program-%E2%80%9Cidea-application%E2%80%9D" rel="noopener noreferrer"&gt;&lt;strong&gt;Community Bounty Program "Idea to Application"&lt;/strong&gt;&lt;/a&gt; implementing the &lt;a href="https://ideas.intersystems.com/ideas/DPI-I-955" rel="noopener noreferrer"&gt;third idea&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;I decided to test it with a multi-step prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Using info from the following articles and github repository, write code for a complete InterSystems Production in Python using PyProd package:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;step 1: come up with the domain for this production&amp;nbsp;&lt;/p&gt;
&lt;p&gt;step 2: create 4 csv files with 30 records in each in the step 1 domain&amp;nbsp;&lt;/p&gt;
&lt;p&gt;step 3: write sql create table statement with the structure from csv file&amp;nbsp;&lt;/p&gt;
&lt;p&gt;step 4: write inbound adapter in Python using PyProd package that reads the file&amp;nbsp;&lt;/p&gt;
&lt;p&gt;step 5: write a business process in Python that analyzes the structure of the step 2 file and makes 1 calculation that makes sense in the step 1 domain&amp;nbsp;&lt;/p&gt;
&lt;p&gt;step 6: write business operation in Python to save all the data read in 4 step and the result of calculation from 5 step to the table created in 3 step&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Here are the articles and github repositories you should use:&amp;nbsp;&lt;/p&gt;
&lt;p&gt;https://community.intersystems.com/post/pyprod-pure-python-iris-interoperability&amp;nbsp;&lt;/p&gt;
&lt;p&gt;https://community.intersystems.com/post/pyprod-creating-iris-interoperability-productions-programmatically-python&amp;nbsp;&lt;/p&gt;
&lt;p&gt;https://community.intersystems.com/post/csvgen-pyprod https://github.com/intersystems/pyprod&amp;nbsp;&lt;/p&gt;
&lt;p&gt;https://github.com/gabriel-ing/csvgen-pyprod&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I fed it all to ChatGPT and hit send. Then, I waited. For exactly 45 seconds.&lt;/p&gt;
&lt;h3&gt;The Delivery and the "Gotchas"&lt;/h3&gt;
&lt;p&gt;As a result, I got a &lt;code&gt;smart_grid_pyprod.zip&lt;/code&gt; containing the ready-to-use code. Naturally, I was dying to know whether it actually worked or was just a convincing hallucination. Playing the part of a complete novice, I asked it how to set everything up. ChatGPT promptly walked me through all necessary steps.&lt;/p&gt;
&lt;p&gt;1. Enabling interoperability in the `USER` namespace via the IRIS Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zn "%SYS"
do ##class(%EnsembleMgr).EnableNamespace("USER")&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2. Configuring the environment variables for Windows 11 and installing the package:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set IRISINSTALLDIR=C:\InterSystems\IRIS
set IRISUSERNAME=SuperUser
set IRISPASSWORD=SYS
set IRISNAMESPACE=USER
set PATH=%IRISINSTALLDIR%\mgr\python;%PATH%
python -m pip install intersystems_pyprod --target %IRISINSTALLDIR%\mgr\python --upgrade&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3. Compile the generated code using &lt;code&gt;intersystems_pyprod smart_grid.py&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;4. Open the Management Portal, and start the production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Did it work right out of the box? Well, no.&lt;/strong&gt; Mostly, the problems were about commas where they shouldn’t be and a couple of wrongly written async requests, among other things. It took me about half a day to iron out the creases (granted, I was watching &lt;em&gt;Landman &lt;/em&gt;in parallel while waiting for Docker to build, turned out to be quite apropos - pumping oil and electricity 😉). But, thanks to some basic Python knowledge and a &lt;a href="https://github.com/gabriel-ing/csvgen-pyprod" rel="noopener noreferrer"&gt;crucial reference example&lt;/a&gt; from &lt;span&gt;&lt;span&gt;&lt;a class="mentioned-user" href="https://dev.to/gabriel"&gt;@gabriel&lt;/a&gt;.Ing&lt;/span&gt;&lt;/span&gt;, I got it running.&lt;/p&gt;
&lt;h3&gt;Anatomy of an AI-Generated Production&lt;/h3&gt;
&lt;p&gt;Once the code was fixed, the production (mostly) written by ChatGPT functioned beautifully. It consists of three components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;business service&lt;/strong&gt;&lt;code&gt;SmartMeterFileService&lt;/code&gt; reads the input file&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;business process&lt;/strong&gt;&lt;code&gt;SmartMeterAnalysisProcess&lt;/code&gt; computes per-file total kWh, average kWh, peak meter id, and peak kWh&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;business operation&lt;/strong&gt;&lt;code&gt;SmartMeterDBOperation&lt;/code&gt; persists every CSV row with those calculation results&lt;/li&gt;
&lt;/ul&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/jmn3lraxygfa1sz48m84.png)&lt;p&gt;Looking at the &lt;strong&gt;Visual Trace&lt;/strong&gt; in the Management Portal, you can see messages flowing seamlessly from the service to the process and finally to the operation:&lt;/p&gt;

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

&lt;p&gt;And when queried in SQL Explorer&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * 
  FROM EnergyOps.SmartMeterReadings&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the data was there, properly calculated, and perfectly structured:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F32qp0nfi9qfz5g2lc6s3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F32qp0nfi9qfz5g2lc6s3.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt;The Verdict: Almost a Success&lt;/h3&gt;
&lt;p&gt;I would call this experiment an &lt;strong&gt;almost success&lt;/strong&gt;. Almost, because it required me to know at least something about how it works, so the complete novice would be stumped (or would need to ask a lot of follow-up questions).&amp;nbsp;&lt;/p&gt;
&lt;p&gt;However, if you have a bit of foundational knowledge, a willingness to troubleshoot, and good community examples to lean on, you can make it work. It proves that while AI might not be ready to architect complex, enterprise-grade genome sequencing pipelines entirely on its own just yet, it is an incredible tool for taking an example and expanding on it to get a prototype off the ground.&lt;/p&gt;


</description>
      <category>ai</category>
      <category>chatgpt</category>
      <category>python</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
