<?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>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>
    <item>
      <title>Introducing the InterSystems IRIS Document Store for Haystack</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Sun, 26 Jul 2026 16:34:15 +0000</pubDate>
      <link>https://dev.to/intersystems/introducing-the-intersystems-iris-document-store-for-haystack-16bf</link>
      <guid>https://dev.to/intersystems/introducing-the-intersystems-iris-document-store-for-haystack-16bf</guid>
      <description>&lt;p&gt;Artificial Intelligence applications are increasingly built around Retrieval-Augmented Generation (RAG), semantic search, and AI agents. As these applications move into production, choosing the right persistence layer becomes just as important as selecting the LLM.&lt;/p&gt;
&lt;p&gt;Today, I'm excited to announce the &lt;strong&gt;InterSystems IRIS Document Store for Haystack&lt;/strong&gt;, a new open-source integration that enables developers to use &lt;strong&gt;InterSystems IRIS&lt;/strong&gt; as a native Document Store within the Haystack AI framework.&lt;/p&gt;
&lt;h2&gt;Why Haystack?&lt;/h2&gt;
&lt;p&gt;Haystack has become one of the leading open-source frameworks for building production-ready AI applications. Its modular pipeline architecture makes it easy to create solutions for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Retrieval-Augmented Generation (RAG)&lt;/li&gt;
&lt;li&gt;Enterprise Search&lt;/li&gt;
&lt;li&gt;Question Answering&lt;/li&gt;
&lt;li&gt;AI Agents&lt;/li&gt;
&lt;li&gt;Semantic Search&lt;/li&gt;
&lt;li&gt;Knowledge Assistants&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Introducing the Integration&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;InterSystems IRIS Document Store&lt;/strong&gt; implements Haystack's Document Store interface, allowing it to integrate naturally into existing Haystack pipelines.&lt;/p&gt;
&lt;p&gt;Whether you're building a small proof of concept or a production RAG system, switching to IRIS as your persistence layer requires minimal changes to your application.&lt;/p&gt;
&lt;h2&gt;Installation&lt;/h2&gt;
&lt;p&gt;The package is available on PyPI.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install intersystems-iris-haystack&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Official Haystack Integration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://haystack.deepset.ai/integrations/intersystems-iris-document-store" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://haystack.deepset.ai/integrations/intersystems-iris-document-store" rel="noopener noreferrer"&gt;https://haystack.deepset.ai/integrations/intersystems-iris-document-store&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub Repository&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/s-c-ai/iris-haystack" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://github.com/s-c-ai/iris-haystack" rel="noopener noreferrer"&gt;https://github.com/s-c-ai/iris-haystack&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyPI Package&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pypi.org/project/intersystems-iris-haystack/" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://pypi.org/project/intersystems-iris-haystack/" rel="noopener noreferrer"&gt;https://pypi.org/project/intersystems-iris-haystack/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Example Architecture&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;                 Haystack Pipeline

&lt;p&gt;Converter → Splitter → Embedder&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            │
            ▼

  InterSystems IRIS Document Store

  • Documents
  • Metadata
  • Vector Search
  • SQL
  • Objects

            │
            ▼

  Retriever → LLM → Answer&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/code&gt;&lt;/pre&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>sql</category>
      <category>github</category>
    </item>
    <item>
      <title>Long Running SQL Queries: a sample exploration</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Fri, 24 Jul 2026 15:59:40 +0000</pubDate>
      <link>https://dev.to/intersystems/long-running-sql-queries-a-sample-exploration-34b4</link>
      <guid>https://dev.to/intersystems/long-running-sql-queries-a-sample-exploration-34b4</guid>
      <description>&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Here at InterSystems, we often deal with massive datasets of structured data. It’s not uncommon to see customers with tables spanning &amp;gt;100 fields and &amp;gt;1 billion rows, each table totaling hundred of GB of data. Now imagine joining two or three of these tables together, with a schema that wasn’t optimized for this specific use case. Just for fun, let’s say you have 10 years worth of EMR data from 20 different hospitals across your state, and you’ve been tasked with finding….&lt;br&gt;&amp;nbsp; &amp;nbsp;every clinician within your network&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; who has administered a specific drug&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; between the years of 2017-2019&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;&amp;nbsp;to patients who reside outside the state&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;nbsp;&amp;nbsp;and have one of the following conditions [diabetes, hypertension, asthma]&lt;br&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; where the cost was covered by Medicaid&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;I’ve seen our technology handle &lt;a href="https://www.intersystems.com/success-stories/a-billion-data-points-for-innovations-in-care-and-care-coordination/" rel="noopener noreferrer"&gt;these sort of cases just fine&lt;/a&gt;, but the query may still take a while to run. Can it be faster though? Let me walk you through a sample investigation. &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

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

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

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

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;strong&gt;The Need:&lt;/strong&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Find all patients who have had an outpatient encounter at a facility located in one of these counties in the year 2022 or 2023&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;strong&gt;The Query:&lt;/strong&gt;&lt;br&gt;SELECT DISTINCT enc.Patient-&amp;gt;PatientNumber &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;FROM EMR.Encounter as enc&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;INNER JOIN State_Facility.Address as fa on enc.Facility = fa.FacilityCode&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;INNER JOIN State_Geography.Cities as city ON city.Zip = fa.ZipCode&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;WHERE enc.EncounterTime BETWEEN '2022-01-01' AND '2023-12-31'&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;AND enc.EncounterType IN ('OP','Outpatient','O')&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;AND city.County IN ('Los Angeles County', 'Orange County', 'Riverside County', 'San Bernardino County', 'Ventura County')&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;strong&gt;The Performance:&lt;/strong&gt;&lt;br&gt;The query was taking &amp;gt;24 hours to complete&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;INVESTIGATION STEPS:&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;1) Review the tables that you’re querying. What relationships or foreign keys exist between them? What indices already exist? Is your SQL query making good use of the ones that already exist? Do the indices have Status = Selectable?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;We checked each field that was part of a WHERE, AND, or INNER JOIN. Most of them did have indices, including some bitmap indices. [NOTE: Further to the right of the screenshot page, the Status column shows that EncounterTypeIndex is Selectable]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

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

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fceqh5hpf4ydnwx2310b8.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%2Fceqh5hpf4ydnwx2310b8.png" alt=" " width="800" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2) Review the Query Plan. Does it make sense? Does it make use of the indices and relationships you expected it would? If not, does it seem more or less efficient?&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;Yes, the Query Plan showed effective use of the indices on EncounterType and StartTime. [NOTE: This screenshot is for a simplified version of the query that does not consider the zip code of the encounter facility]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzskc4vk2isx8kchxlq6q.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%2Fzskc4vk2isx8kchxlq6q.png" alt=" " width="799" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;3) Ensure the table statistics up to date by running &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/Doc.View.cls?KEY=GSOD_opttable" rel="noopener noreferrer"&gt;Tune Tables&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;strong&gt;4) Check whether the actual Query Plan at runtime matches the one you were shown. &lt;/strong&gt;The "Show Plan" Query Plan does not utilize the &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSOC_rtpc" rel="noopener noreferrer"&gt;Runtime Plan Choice (RTPC)&lt;/a&gt; optimization when it generates a Query Plan, but the RTPC is utilized when the query is actually run. That is why the Show Plan Query Plan and the runtime Query Plan can be different. The RTPC algorithm usually finds an optimal choice, but it can sometimes make a poor choice. If we find that the RTPC algorithm is making the wrong choice, it is possible to suppress the RTPC at runtime by using the &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_select#RSQL_select_args_keyword" rel="noopener noreferrer"&gt;%NORUNTIME keyword&lt;/a&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Once the query was running, we looked at the Processes page and found the process that was running the query. We found the cached query that it was running (the Routine). We went to that cached query and looked at its Query Plan. We found that it was using a Query Plan that was very different from the one we’d seem before, and it looked much less efficient.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fut51eitny82zkwr1pcy3.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%2Fut51eitny82zkwr1pcy3.png" alt=" " width="799" height="452"&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%2Fs951cou8nur54mt4rmzp.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%2Fs951cou8nur54mt4rmzp.png" alt=" " width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;b&gt;RECOMMENDATIONS:&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;We recommended that the customer take the following actions:&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;1) Use the &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_select#RSQL_select_args_keyword" rel="noopener noreferrer"&gt;%NORUNTIME keyword&lt;/a&gt; when executing the query, forcing it to use the more efficient Query Plan&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2) Build a new bitmap index called EncounterDate based on the EncounterTime field. Date-based indices can be faster than DateTime-based indices, and bitmap indices are often significantly faster than normal indices&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Once they implemented these two recommendations, their query was now completing in ~6 hours, a 75% improvement.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;FURTHER READING:&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check out &lt;a class="mentioned-user" href="https://dev.to/benjamin"&gt;@benjamin&lt;/a&gt;.Spead's excellent collection of resources, which includes links to&amp;nbsp;online documentation, InterSystems online learning courses, presentation slideshows, and Developer Community articles.&lt;br&gt;&lt;a href="https://community.intersystems.com/post/sql-performance-resources" rel="noopener noreferrer"&gt;https://community.intersystems.com/post/sql-performance-resources&lt;/a&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;

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

</description>
      <category>sql</category>
      <category>performance</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Monitoring InterSystems IRIS with Prometheus and Grafana</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Wed, 22 Jul 2026 17:17:48 +0000</pubDate>
      <link>https://dev.to/intersystems/monitoring-intersystems-iris-with-prometheus-and-grafana-37hh</link>
      <guid>https://dev.to/intersystems/monitoring-intersystems-iris-with-prometheus-and-grafana-37hh</guid>
      <description>&lt;p&gt;Monitoring your IRIS deployment is crucial. With the deprecation of&amp;nbsp;&lt;strong&gt;System Alert and Monitoring (SAM),&lt;/strong&gt;&amp;nbsp;a modern, scalable solution is necessary for&amp;nbsp;&lt;strong&gt;real-time insights, early issue detection, and operational efficiency.&lt;/strong&gt;&amp;nbsp;This guide covers setting up&amp;nbsp;&lt;strong&gt;Prometheus and Grafana&lt;/strong&gt;&amp;nbsp;in Kubernetes to monitor&amp;nbsp;&lt;strong&gt;InterSystems IRIS&amp;nbsp;&lt;/strong&gt;effectively.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;This guide assumes you already have an IRIS cluster deployed using the InterSystems Kubernetes Operator (IKO), which simplifies deployment, integration and mangement.&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%2Fwn8q07hsv4ubuvwxe6fl.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%2Fwn8q07hsv4ubuvwxe6fl.png" alt=" " width="800" height="368"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;&lt;strong&gt;Why Prometheus and Grafana?&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prometheus&lt;/strong&gt; and &lt;strong&gt;Grafana&lt;/strong&gt; are widely adopted tools for cloud-native monitoring and visualization. Here’s why they are a fit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalability:&lt;/strong&gt; Prometheus handles large-scale data ingestion efficiently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting:&lt;/strong&gt; Customizable alerts via &lt;strong&gt;Prometheus Alertmanager.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visualization:&lt;/strong&gt; Grafana offers rich, customizable dashboards for Kubernetes metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ease of Integration:&lt;/strong&gt; Seamlessly integrates with &lt;strong&gt;Kubernetes workloads.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Before starting, ensure you have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic knowledge of Kubernetes and Linux&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubectl&lt;/code&gt; and &lt;code&gt;helm&lt;/code&gt; installed.&lt;/li&gt;
&lt;li&gt;Familiarity with Prometheus concepts (refer to the &lt;a href="https://prometheus.io/docs/introduction/overview/" rel="noopener noreferrer"&gt;Prometheus documantion&lt;/a&gt; for more information).&lt;/li&gt;
&lt;li&gt;A deployed IRIS instance using the &lt;strong&gt;InterSystems Kubernetes Operator (IKO)&lt;/strong&gt;, refer to another article &lt;a href="https://docs.intersystems.com/components/csp/docbook/DocBook.UI.Page.cls?KEY=AIKO" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;





&lt;h2&gt;Step 1: Enable Metrics in InterSystems IRIS&lt;/h2&gt;

&lt;p&gt;InterSystems IRIS exposes metrics via &lt;code&gt;/api/monitor/&lt;/code&gt;&amp;nbsp;in the Prometheus format. Ensure this endpoint is enabled:&lt;/p&gt;

&lt;ol start="1"&gt;
&lt;li&gt;Open the Management Portal.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;System Administration &amp;gt; Security &amp;gt; Applications &amp;gt; Web Applications&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Ensure &lt;code&gt;/api/monitor/&lt;/code&gt; is enabled and accessible by Prometheus. You can check its status by navigating to the Management Portal, going to &lt;strong&gt;System Administration &amp;gt; Security &amp;gt; Applications &amp;gt; Web Applications&lt;/strong&gt;, and verifying that the endpoint is listed and enabled.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Verify its availability by accessing:&lt;/p&gt;

&lt;pre&gt;http://&amp;lt;IRIS_HOST&amp;gt;:&amp;lt;PORT&amp;gt;/api/monitor/metrics&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%2F4xrra3jgxqgg2420kviw.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%2F4xrra3jgxqgg2420kviw.png" alt=" " width="800" height="333"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;&lt;strong&gt;Step 2: Deploy Prometheus Using Helm&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;Deploying Prometheus using &lt;strong&gt;Helm&lt;/strong&gt; provides an easy-to-manage monitoring setup. We will use the &lt;code&gt;kube-prometheus-stack&lt;/code&gt; chart that includes Prometheus, Alertmanager, and Grafana.&lt;/p&gt;

&lt;ol start="1"&gt;
&lt;li&gt;
&lt;strong&gt;Prepare the configuration:&lt;/strong&gt; Create a &lt;code&gt;values.yaml&lt;/code&gt; file with the following settings:
&lt;pre&gt;prometheus:
&amp;nbsp; prometheusSpec:
&amp;nbsp; &amp;nbsp; additionalScrapeConfigs:
&amp;nbsp; &amp;nbsp; &amp;nbsp; - job_name: 'intersystems_iris_metrics'
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; metrics_path: '/api/monitor/metrics'
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; static_configs:
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - targets:
&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; - 'iris-app-compute-0.iris-svc.commerce.svc.cluster.local:80' # Replace with your IRIS service

      # To scrape custom metrics from the REST API created in IRIS
      - job_name: 'custom_iris_metrics'
        metrics_path: '/web/metrics'
        static_configs:
          - targets:
              - 'commerce-app-webgateway-0.iris-svc.commerce.svc.cluster.local:80'
        basic_auth:
          username: '_SYSTEM'
          password: 'SYS'&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explanation:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;iris-app-compute-0.iris-svc.commerce.svc.cluster.local:80&lt;/strong&gt;&lt;/code&gt;: The format of the target should follow this convention: &lt;code&gt;&amp;lt;pod-name&amp;gt;-iris-svc.&amp;lt;namespace&amp;gt;.svc.cluster.local:80&lt;/code&gt;. Replace &lt;code&gt;&amp;lt;pod-name&amp;gt;&lt;/code&gt;&amp;nbsp;with your IRIS pod, specify whether you want to scrape &lt;code&gt;compute&lt;/code&gt; or &lt;code&gt;data&lt;/code&gt; pods, and adjust the namespace as needed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&lt;strong&gt;basic_auth&lt;/strong&gt;&lt;/code&gt;** section**: If authentication is required to access the IRIS metrics endpoint, provide the necessary credentials.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the Helm repository:&lt;/strong&gt;
&lt;pre&gt;helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update&lt;/pre&gt;
&lt;/li&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/uyfgv31f7g9hkvfegxoe.png) &amp;nbsp;
&lt;li&gt;
&lt;strong&gt;Install Prometheus using Helm:&lt;/strong&gt;
&lt;pre&gt;helm install monitoring prometheus-community/kube-prometheus-stack -n monitoring --create-namespace -f values.yaml&lt;/pre&gt;
&lt;/li&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/sktubx1bti39hvr574la.png)
&lt;li&gt;
&lt;strong&gt;Verify the deployment:&lt;/strong&gt;
&lt;pre&gt;kubectl get pods -n monitoring&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5gw71yjc32t0091v91p4.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%2F5gw71yjc32t0091v91p4.png" alt=" " width="666" height="194"&gt;&lt;/a&gt; &amp;nbsp;&lt;/p&gt;





&lt;h2&gt;Step 3: Custom Metrics with REST API&lt;/h2&gt;

&lt;p&gt;You can create a&lt;strong&gt; custom metrics CSP&lt;/strong&gt; page that serves your application metrics. In this guide, I provide an example of a simple CSP page that extracts system metrics from IRIS itself, but you can totally build your own CSP page with your own custom metrics—just make sure they are in the &lt;strong&gt;Prometheus format.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;br&gt;
CustomMetrics.REST&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="hljs-keyword"&gt;Class&lt;/span&gt;&amp;nbsp;CustomMetrics.REST&amp;nbsp;&lt;span class="hljs-keyword"&gt;Extends&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;%CSP.REST&lt;/span&gt;
{ &lt;span class="hljs-keyword"&gt;Parameter&lt;/span&gt;&amp;nbsp;HandleCorsRequest = &lt;span class="hljs-number"&gt;1&lt;/span&gt;&lt;span class="hljs-comment"&gt;; ClassMethod&amp;nbsp;Metrics() As&amp;nbsp;%Status&lt;/span&gt;
{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Try&lt;/span&gt;&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Do&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;%response.SetHeader&lt;/span&gt;(&lt;span class="hljs-string"&gt;"Content-Type"&lt;/span&gt;, &lt;span class="hljs-string"&gt;"text/plain; version=0.0.4; charset=utf-8"&lt;/span&gt;)
 &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;New&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;$Namespace&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Set&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;$Namespace&lt;/span&gt;&amp;nbsp;= &lt;span class="hljs-string"&gt;"%SYS"&lt;/span&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Set&lt;/span&gt;&amp;nbsp;ref&amp;nbsp;= &lt;span class="hljs-keyword"&gt;##class&lt;/span&gt;(SYS.Stats.Dashboard).Sample()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# HELP iris_license_high Peak number of licenses used"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# TYPE iris_license_high gauge"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"iris_license_high "&lt;/span&gt;, ref.LicenseHigh, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# HELP iris_active_processes Number of active processes"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# TYPE iris_active_processes gauge"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"iris_active_processes "&lt;/span&gt;, ref.Processes, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# HELP iris_application_errors Number of application errors"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"# TYPE iris_application_errors counter"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"iris_application_errors "&lt;/span&gt;, ref.ApplicationErrors, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Return&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;$$$OK&lt;/span&gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&amp;nbsp;&lt;span class="hljs-keyword"&gt;Catch&lt;/span&gt;&amp;nbsp;ex&amp;nbsp;{
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Do&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;%response.SetHeader&lt;/span&gt;(&lt;span class="hljs-string"&gt;"Content-Type"&lt;/span&gt;, &lt;span class="hljs-string"&gt;"text/plain"&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Write&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-string"&gt;"Internal Server Error"&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;$CHAR&lt;/span&gt;(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Do&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;$System&lt;/span&gt;.Status.DisplayError(ex.AsStatus())
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;span class="hljs-keyword"&gt;Return&lt;/span&gt;&amp;nbsp;&lt;span class="hljs-built_in"&gt;$$$ERROR&lt;/span&gt;(&lt;span class="hljs-built_in"&gt;$$$GeneralError&lt;/span&gt;, &lt;span class="hljs-string"&gt;"Internal Server Error"&lt;/span&gt;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
} 
XData&amp;nbsp;UrlMap
{
&amp;lt;Routes&amp;gt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;lt;Route&amp;nbsp;Url=&lt;span class="hljs-string"&gt;"/metrics"&lt;/span&gt;&amp;nbsp;Method=&lt;span class="hljs-string"&gt;"GET"&lt;/span&gt;&amp;nbsp;Call=&lt;span class="hljs-string"&gt;"Metrics"&lt;/span&gt;/&amp;gt;
&amp;lt;/Routes&amp;gt;
} 
}&lt;/code&gt;&lt;/pre&gt;

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




&lt;p&gt;Deploy this as a REST service under a new web application called &lt;code&gt;&lt;strong&gt;metrics&lt;/strong&gt;&lt;/code&gt; in IRIS, and add its path to Prometheus for scraping.&lt;/p&gt;





&lt;h2&gt;Step 4: Verify Prometheus Setup&lt;/h2&gt;

&lt;ol start="1"&gt;
&lt;li&gt;Open the Prometheus UI (&lt;code&gt;http://&amp;lt;PROMETHEUS_HOST&amp;gt;:9090&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Status &amp;gt; Targets&lt;/strong&gt; and confirm IRIS metrics are being scraped.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5jutrsrfns2sl4w8z0w.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%2Fh5jutrsrfns2sl4w8z0w.png" alt=" " width="800" height="209"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h3&gt;Step 5: Access Grafana&lt;/h3&gt;

&lt;p&gt;With Prometheus scraping IRIS metrics, the next step is to visualize the data using Grafana.&lt;/p&gt;

&lt;p&gt;1.&amp;nbsp;&lt;strong&gt;Retrieve the Grafana service details:&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubectl get svc -n monitoring&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you’re using an ingress controller, you can access Grafana using the configured hostname (e.g., &lt;code&gt;http://grafana.example.com&lt;/code&gt;). Otherwise, you can use the following options:&lt;/p&gt;

&lt;ol start="1"&gt;
&lt;li&gt;
&lt;strong&gt;Port Forwarding&lt;/strong&gt;: Use &lt;code&gt;kubectl port-forward&lt;/code&gt; to access Grafana locally:
&lt;pre&gt;&lt;code&gt;kubectl port-forward svc/monitoring-grafana -n monitoring 3000:80&lt;/code&gt;&lt;/pre&gt;
Then, access Grafana at &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NodePort or ClusterIP&lt;/strong&gt;: Refer to the &lt;code&gt;NodePort&lt;/code&gt; or &lt;code&gt;ClusterIP&lt;/code&gt; service details from the command output to connect directly.&lt;/li&gt;
&lt;/ol&gt;





&lt;h3&gt;Step 6: Log In to Grafana&lt;/h3&gt;

&lt;p&gt;Use the default credentials to log in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Username&lt;/strong&gt;: &lt;code&gt;admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Password&lt;/strong&gt;: &lt;code&gt;prom-operator&lt;/code&gt; (or the password set during installation).&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%2Fxp4sph9cs735o8zsrioh.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%2Fxp4sph9cs735o8zsrioh.png" alt=" " width="577" height="553"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;



&lt;h2&gt;&lt;strong&gt;Step 7: Import a Custom Dashboard&lt;/strong&gt;&lt;/h2&gt;

&lt;p&gt;I’ve created a&lt;strong&gt; custom dashboard&lt;/strong&gt; specifically tailored for &lt;strong&gt;InterSystems IRIS&lt;/strong&gt; metrics, which you can use as a starting point for your monitoring needs. The JSON file for this dashboard is hosted on GitHub for easy access and import:&amp;nbsp;&lt;a href="https://raw.githubusercontent.com/sbendarsky/monitoring-iris/refs/heads/main/grafana-dashboard.json" rel="noopener noreferrer"&gt;Download the Custom Dashboard JSON&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To import the dashboard:&lt;/p&gt;

&lt;ol start="1"&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Dashboards &amp;gt; Import&lt;/strong&gt; in Grafana.&lt;/li&gt;
&lt;li&gt;Paste the URL of the JSON file into the &lt;strong&gt;Import via panel JSON&lt;/strong&gt; field or upload the file directly.&lt;/li&gt;
&lt;li&gt;Assign the dashboard to a folder and Prometheus data source when prompted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwhwn51qahzayp5trh8b4.jpg" 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%2Fwhwn51qahzayp5trh8b4.jpg" alt=" " width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once imported, you can edit the panels to &lt;strong&gt;include additional metrics&lt;/strong&gt;, customize the visualizations, or refine the layout for better insights into your &lt;strong&gt;IRIS &lt;/strong&gt;environment.&lt;/p&gt;





&lt;h3&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;By following this guide, we've successfully set up Prometheus to scrape InterSystems IRIS metrics and visualize them using Grafana. Additionally, you can explore other monitoring tools such as Loki to also monitor logs efficiently and configure alerts using Alertmanager or external services like PagerDuty and Slack. If you have any questions or feedback, feel free to reach out!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>api</category>
      <category>monitoring</category>
      <category>beginners</category>
    </item>
    <item>
      <title>pyprod: Pure Python IRIS Interoperability</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Mon, 20 Jul 2026 15:21:47 +0000</pubDate>
      <link>https://dev.to/intersystems/pyprod-pure-python-iris-interoperability-3kan</link>
      <guid>https://dev.to/intersystems/pyprod-pure-python-iris-interoperability-3kan</guid>
      <description>&lt;p&gt;Intersystems IRIS Productions provide a powerful framework for connecting disparate systems across various protocols and message formats in a reliable, observable, and scalable manner.&amp;nbsp;&lt;strong&gt;intersystems_pyprod&lt;/strong&gt;, short for &lt;em&gt;InterSystems Python Productions&lt;/em&gt;, is a Python library that enables developers to build these interoperability components entirely in Python. Designed for flexibility, it supports a hybrid approach: you can seamlessly mix new Python-based components with existing ObjectScript-based ones, leveraging your established IRIS infrastructure. Once defined, these Python components are managed just like any other; they can be added, configured, and connected using the IRIS Production Configuration page.&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;&lt;strong&gt;A Quick Primer on InterSystems IRIS Productions&lt;/strong&gt;&lt;/h2&gt;

![ ](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/uxrqudqxu71qyqd568s2.png)
&lt;p&gt;&lt;strong&gt;&lt;u&gt;Key Elements of a Production&lt;/u&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Image from Learning Services training material&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;An IRIS Production generally receives data from external interfaces, processes it through coordinated steps, and routes it to its destination. As messages move through the system, they are automatically persisted, making the entire flow fully traceable through IRIS’s visual trace and logging tools. The architecture relies on certain key elements:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Business Hosts:&lt;/strong&gt;&lt;/b&gt; These are the core building blocks—&lt;b&gt;&lt;strong&gt;Services&lt;/strong&gt;&lt;/b&gt;, &lt;b&gt;&lt;strong&gt;Processes&lt;/strong&gt;&lt;/b&gt;, and &lt;b&gt;&lt;strong&gt;Operations&lt;/strong&gt;&lt;/b&gt;—that pass persistable messages between one another.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Adapters:&lt;/strong&gt;&lt;/b&gt; Inbound and outbound adapters manage the interaction with the external world, handling the specific protocols needed to receive and send data.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Callbacks:&lt;/strong&gt;&lt;/b&gt; The engine uses specific callback methods to pass messages between hosts, either &lt;b&gt;&lt;strong&gt;synchronously or asynchronously&lt;/strong&gt;&lt;/b&gt;. These callbacks follow strict signatures and return a &lt;code&gt;Status&lt;/code&gt; object to ensure execution integrity.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Configuration Helpers:&lt;/strong&gt;&lt;/b&gt; Objects such as &lt;b&gt;&lt;strong&gt;Properties&lt;/strong&gt;&lt;/b&gt; and &lt;b&gt;&lt;strong&gt;Parameters&lt;/strong&gt;&lt;/b&gt; expose settings to the Production Configuration UI, allowing users to easily instantiate, configure, and save the state of these components.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;Workflow using pyprod&lt;/h2&gt;
&lt;p&gt;This is essentially a 3&amp;nbsp;step process.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write your production components&lt;/strong&gt; in a regular Python script. In that script, you import the required base classes from &lt;strong&gt;intersystems_pyprod&lt;/strong&gt;&amp;nbsp;and define your own components by subclassing them, just as you would with any other Python library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load them into InterSystems IRIS&lt;/strong&gt; by running the &lt;code&gt;&lt;strong&gt;intersystems_pyprod&lt;/strong&gt;&lt;/code&gt;&amp;nbsp;(same name as the library) command from the terminal and passing it the path to your Python script. This step links the Python classes with IRIS so that they appear as production components and can be configured and wired together using the standard Production Configuration UI.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create the production&lt;/strong&gt; using the Production Configuration page&amp;nbsp;and start the Production&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;NOTE: If you create all your components with all their Properties hardcoded within the python script, you only need to add them to the production and start the Production.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;You can connect&amp;nbsp;pyprod to your IRIS instance&amp;nbsp;by doing a&amp;nbsp;&lt;a href="https://github.com/intersystems/pyprod/blob/main/docs/installing.md" rel="noopener noreferrer"&gt;&lt;strong&gt;one time setup&lt;/strong&gt;&lt;/a&gt;.&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;&lt;strong&gt;Simple Example&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;In this example, we demonstrate a synchronous message flow where a request originates from a Service, moves through a Process, and is forwarded to an Operation. The resulting response then travels the same path in reverse, passing from the Operation back through the Process to the Service. Additionally, we showcase how to utilize the &lt;code&gt;IRISLog&lt;/code&gt; utility to write custom log entries.&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%2Ft3khatdp059r3gmgs5ee.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%2Ft3khatdp059r3gmgs5ee.png" alt=" " width="742" height="498"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt;Step 1&lt;/h3&gt;
&lt;p&gt;Create your Production components using pyprod in the file HelloWorld.py&lt;/p&gt;
&lt;p&gt;Here are some key parts of the code&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Package Naming:&lt;/strong&gt;&lt;/b&gt; We define &lt;code&gt;iris_&lt;/code&gt;&lt;code&gt;package_name&lt;/code&gt;, which prefixes all classes as they appear on the Production Configuration page&amp;nbsp;(If omitted, the script name is used as the default prefix).&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Persistable Messages:&lt;/strong&gt;&lt;/b&gt; We define &lt;code&gt;MyRequest&lt;/code&gt; and &lt;code&gt;MyResponse&lt;/code&gt;. These are the essential data structures for communication, as only persistable objects can be passed between Services, Processes, and Operations.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;The Inbound Adapter:&lt;/strong&gt;&lt;/b&gt; Our adapter passes a string to the Service using the &lt;code&gt;business_host_process_input&amp;nbsp;&lt;/code&gt;method.&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;The Business Service:&lt;/strong&gt;&lt;/b&gt;&amp;nbsp;Implemented with the help of &lt;strong&gt;OnProcessInput&lt;/strong&gt; callback.&lt;ul&gt;
&lt;li&gt;MyService receives data from the adapter and converts it into a &lt;code&gt;MyRequest&lt;/code&gt; message&lt;/li&gt;
&lt;li&gt;We use the &lt;code&gt;ADAPTER&lt;/code&gt;&lt;b&gt;&lt;strong&gt;IRISParameter&lt;/strong&gt;&lt;/b&gt; to link the Inbound Adapter to the Service. Note that this attribute must be named &lt;code&gt;ADAPTER&lt;/code&gt; in all caps to align with IRIS conventions.&lt;/li&gt;
&lt;li&gt;We define a &lt;code&gt;target&lt;/code&gt;&lt;b&gt;&lt;strong&gt;IRISProperty&lt;/strong&gt;&lt;/b&gt;, which allows users to select the destination component directly via the Configuration UI.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Business Process:&lt;/strong&gt; Implemented with the help of &lt;strong&gt;OnRequest&lt;/strong&gt; callback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Business Operation&lt;/strong&gt;: Implemented with the help of &lt;strong&gt;OnMessage&lt;/strong&gt; callback. (You can also define a MessageMap)&lt;/li&gt;
&lt;li&gt;
&lt;b&gt;&lt;strong&gt;Logic &amp;amp; Callbacks:&lt;/strong&gt;&lt;/b&gt; Finally, the hosts implement their core logic within standard callbacks like &lt;code&gt;OnProcessInput&lt;/code&gt; and &lt;code&gt;OnRequest&lt;/code&gt;, routing messages using the &lt;code&gt;SendRequestSync&lt;/code&gt; method.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can read more about each of these parts on the pyprod &lt;a href="https://github.com/intersystems/pyprod/blob/main/docs/apireference.md#pyprod--api-reference" rel="noopener noreferrer"&gt;&lt;strong&gt;API Reference page&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;&amp;nbsp;&lt;/strong&gt;and also using the &lt;a href="https://github.com/intersystems/pyprod/blob/main/docs/quickstart.md#quick-start-guide" rel="noopener noreferrer"&gt;&lt;strong&gt;Quick Start Guide&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;import&lt;/span&gt; time

&lt;p&gt;&lt;span&gt;from&lt;/span&gt; intersystems_pyprod &lt;span&gt;import&lt;/span&gt; (&lt;br&gt;
    InboundAdapter,BusinessService, BusinessProcess, &lt;br&gt;
    BusinessOperation, OutboundAdapter, JsonSerialize, &lt;br&gt;
    IRISProperty, IRISParameter, IRISLog, Status)&lt;/p&gt;

&lt;p&gt;iris_package_name = &lt;span&gt;"helloworld"&lt;/span&gt;&lt;br&gt;
&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyRequest&lt;/span&gt;&lt;span&gt;(JsonSerialize)&lt;/span&gt;:&lt;br&gt;
    content: str&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyResponse&lt;/span&gt;&lt;span&gt;(JsonSerialize)&lt;/span&gt;:&lt;br&gt;
    content: str&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyInAdapter&lt;/span&gt;&lt;span&gt;(InboundAdapter)&lt;/span&gt;:&lt;br&gt;
    &lt;span&gt;def&lt;/span&gt; &lt;span&gt;OnTask&lt;/span&gt;&lt;span&gt;(self)&lt;/span&gt;:&lt;br&gt;
        time.sleep(&lt;span&gt;0.5&lt;/span&gt;)&lt;br&gt;
        self.business_host_process_input(&lt;span&gt;"request message"&lt;/span&gt;)&lt;br&gt;
        &lt;span&gt;return&lt;/span&gt; Status.OK()&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyService&lt;/span&gt;&lt;span&gt;(BusinessService)&lt;/span&gt;:&lt;br&gt;
    ADAPTER = IRISParameter(&lt;span&gt;"helloworld.MyInAdapter"&lt;/span&gt;)&lt;br&gt;
    target = IRISProperty(settings=&lt;span&gt;"Target"&lt;/span&gt;)&lt;br&gt;
    &lt;span&gt;def&lt;/span&gt; &lt;span&gt;OnProcessInput&lt;/span&gt;&lt;span&gt;(self, input)&lt;/span&gt;:&lt;br&gt;
        persistent_message = MyRequest(input)&lt;br&gt;
        status, response = self.SendRequestSync(self.target, persistent_message)&lt;br&gt;
        IRISLog.Info(response.content)&lt;br&gt;
        &lt;span&gt;return&lt;/span&gt; status&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyProcess&lt;/span&gt;&lt;span&gt;(BusinessProcess)&lt;/span&gt;:&lt;br&gt;
    target = IRISProperty(settings=&lt;span&gt;"Target"&lt;/span&gt;)&lt;br&gt;
    &lt;span&gt;def&lt;/span&gt; &lt;span&gt;on_request&lt;/span&gt;&lt;span&gt;(self, input)&lt;/span&gt;:&lt;br&gt;
        status, response = self.SendRequestSync(self.target,input)&lt;br&gt;
        &lt;span&gt;return&lt;/span&gt; status, response&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyOperation&lt;/span&gt;&lt;span&gt;(BusinessOperation)&lt;/span&gt;:&lt;br&gt;
    ADAPTER = IRISParameter(&lt;span&gt;"helloworld.MyOutAdapter"&lt;/span&gt;)&lt;br&gt;
    &lt;span&gt;def&lt;/span&gt; &lt;span&gt;OnMessage&lt;/span&gt;&lt;span&gt;(self, input)&lt;/span&gt;:&lt;br&gt;
        status = self.ADAPTER.custom_method(input)&lt;br&gt;
        response = MyResponse(&lt;span&gt;"response message"&lt;/span&gt;)&lt;br&gt;
        &lt;span&gt;return&lt;/span&gt; status, response&lt;/p&gt;

&lt;p&gt;&lt;span&gt;class&lt;/span&gt; &lt;span&gt;MyOutAdapter&lt;/span&gt;&lt;span&gt;(OutboundAdapter)&lt;/span&gt;:&lt;br&gt;
    &lt;span&gt;def&lt;/span&gt; &lt;span&gt;custom_method&lt;/span&gt;&lt;span&gt;(self, input)&lt;/span&gt;:&lt;br&gt;
        IRISLog.Info(input.content)&lt;br&gt;
        &lt;span&gt;return&lt;/span&gt; Status.OK()&lt;/p&gt;

&lt;/code&gt;&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2&lt;/h3&gt;
&lt;p&gt;Once your code is ready, load the components to IRIS.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ intersystems_pyprod /full/path/to/HelloWorld.py

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Loading MyRequest to IRIS...
...
Load finished successfully.

Loading MyResponse to IRIS...
...
Load finished successfully.
...
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;h3&amp;gt;Step 3&amp;lt;/h3&amp;gt;&amp;lt;p&amp;gt;Add each host to the Production using the Production Configuration page.&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;The image below shows&amp;nbsp;&amp;lt;code data-index-in-node="42" data-path-to-node="3"&amp;gt;MyService&amp;lt;/code&amp;gt;&amp;nbsp;and its &amp;lt;code data-index-in-node="93" data-path-to-node="3"&amp;gt;target&amp;lt;/code&amp;gt; property&amp;nbsp;being configured through the UI. Follow the same process to add &amp;lt;code data-index-in-node="176" data-path-to-node="3"&amp;gt;MyProcess&amp;lt;/code&amp;gt; and &amp;lt;code data-index-in-node="190" data-path-to-node="3"&amp;gt;MyOperation&amp;lt;/code&amp;gt;. Once the setup is complete, simply start the production to see your messages in motion.&amp;lt;/p&amp;gt;
&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%2F3k3972p7c0w53tnajuez.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%2F3k3972p7c0w53tnajuez.png" alt=" " width="552" height="604"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;By combining the flexibility of the Python ecosystem with the industrial-grade reliability of InterSystems IRIS, &lt;b&gt;&lt;strong&gt;pyprod&lt;/strong&gt;&lt;/b&gt; offers a modern path for building interoperability solutions. Whether you are developing entirely new "Pure Python" productions or enhancing existing ObjectScript infrastructures with specialized Python libraries, &lt;b&gt;&lt;strong&gt;pyprod&lt;/strong&gt;&lt;/b&gt; ensures your components remain fully integrated, observable, and easy to configure.&amp;nbsp;We look forward to seeing what you build!&lt;/p&gt;

&lt;h2&gt;Quick Links&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/intersystems/pyprod" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://pypi.org/project/intersystems-pyprod/" rel="noopener noreferrer"&gt;PyPi Package&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;b&gt;&lt;strong&gt;Support the Project:&lt;/strong&gt;&lt;/b&gt; If you find this library useful, please&amp;nbsp;consider &lt;b&gt;&lt;strong&gt;giving us a ⭐ on GitHub&amp;nbsp;&lt;/strong&gt;&lt;/b&gt;and suggesting enhancements. It helps the project grow and makes it easier for other developers in the InterSystems community to discover it!&lt;/blockquote&gt;


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

</description>
      <category>programming</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Getting started with OAuth in your Web Apps</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 30 Jun 2026 16:45:19 +0000</pubDate>
      <link>https://dev.to/intersystems/getting-started-with-oauth-in-your-web-apps-2g76</link>
      <guid>https://dev.to/intersystems/getting-started-with-oauth-in-your-web-apps-2g76</guid>
      <description>&lt;p&gt;This article is intended as a beginner level article for people that want to learn how to use OAuth2 in their web applications natively.&lt;/p&gt;
&lt;p&gt;There is an accompanying video/demo that may be helpful here:&amp;nbsp;&lt;/p&gt;

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

&lt;p&gt;and you can reproduce this locally with the Open Exchange application attached.&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;&lt;u&gt;OAuth2 as a native authentication type for web applications&lt;/u&gt;&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;OAuth (&lt;strong&gt;O&lt;/strong&gt;pen &lt;strong&gt;Auth&lt;/strong&gt;orization) 2.0 is a standard way to let one application call another application’s API &lt;strong&gt;without&lt;/strong&gt; sharing a username and password. Instead of sending credentials on every request, the client sends an &lt;strong&gt;access token&lt;/strong&gt; (typically in an &lt;code&gt;Authorization: Bearer ...&lt;/code&gt; header).&lt;/p&gt;
&lt;p&gt;OAuth2 focuses on &lt;em&gt;authorization&lt;/em&gt; (what the client is allowed to do). If you also need user login and identity claims, OAuth2 is commonly paired with OpenID Connect (OIDC) — but in this article we’ll stay focused on OAuth2 access tokens and scopes.&lt;/p&gt;
&lt;p&gt;If you want a quick refresher, this short video is a good overview: &lt;a href="https://learning.intersystems.com/course/view.php?name=OAuth%202.0:%20An%20Overview" rel="noopener noreferrer"&gt;OAuth 2.0 An Overview&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;The problem OAuth2 solves (with a simple IRIS example)&lt;/h3&gt;
&lt;p&gt;Assume IRIS hosts a small REST API for a bank account &lt;code&gt;ACCT-1&lt;/code&gt; under &lt;code&gt;/bank&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GET&lt;/strong&gt;&lt;br&gt;&lt;code&gt;/bank/checkbalance&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &lt;span class="mention"&gt;"dollars"&lt;/span&gt;: &lt;span class="mention"&gt;5&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;POST&lt;/strong&gt;&lt;br&gt;&lt;code&gt;/bank/transfer&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &lt;span class="mention"&gt;"toAccount"&lt;/span&gt;: &lt;span class="mention"&gt;"ACCT-2"&lt;/span&gt;,
  &lt;span class="mention"&gt;"dollars"&lt;/span&gt;: &lt;span class="mention"&gt;2&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now suppose you want to allow a third-party app to monitor your balance. It should be allowed to call &lt;code&gt;/checkbalance&lt;/code&gt;, but it should &lt;strong&gt;not&lt;/strong&gt; be allowed to call &lt;code&gt;/transfer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is where OAuth2 fits well: instead of giving the third-party app your IRIS username/password, you grant it limited access via a token. That token can be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scoped&lt;/strong&gt; (e.g., “read balance” but not “transfer funds”)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-limited&lt;/strong&gt; (tokens expire)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocable&lt;/strong&gt; (you can withdraw access later)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;What’s new in IRIS&lt;/h3&gt;
&lt;p&gt;Starting in &lt;a href="https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GCRN_new20252#GCRN_new20252_speed" rel="noopener noreferrer"&gt;IRIS 2025.2&lt;/a&gt;, OAuth2 can be selected as a native authentication method for Web Applications — so enabling an OAuth2-protected web app is no longer a “DIY” exercise.&lt;/p&gt;
&lt;p&gt;Concretely, IRIS can validate an incoming access token for a CSP/Web Application request and then establish a user context (username + roles) based on that token, just like other authentication types do.&lt;/p&gt;
&lt;p&gt;(For reference on the older, more manual approach, see &lt;a class="mentioned-user" href="https://dev.to/daniel"&gt;@daniel&lt;/a&gt;.Kutac’s excellent &lt;a href="https://community.intersystems.com/post/intersystems-iris-open-authorization-framework-oauth-20-implementation-part-1" rel="noopener noreferrer"&gt;series of articles&lt;/a&gt;.)&lt;/p&gt;
&lt;h3&gt;The Characters&lt;/h3&gt;
&lt;p&gt;OAuth has a few “characters”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource Owner&lt;/strong&gt; (the user/owner of the bank account)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt; (the third-party app; in this demo we use Postman as the client)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization Server&lt;/strong&gt; (Keycloak; authenticates the user &amp;amp; authorizes the request, deciding what scopes the client can receive, and issues the token)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Server&lt;/strong&gt; (IRIS; hosts &lt;code&gt;/myBankInfo&lt;/code&gt;, validates the token, and enforces what the token is allowed to do). The third-party app never sees your IRIS password — it presents a token, and IRIS makes the allow/deny decision.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 0: Prerequisites (avoid issuer / hostname issues)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This demo uses HTTP to keep setup simple. In production you should use HTTPS (and real certificates), otherwise tokens and sessions can be intercepted.&lt;/p&gt;
&lt;p&gt;This Open Exchange demo runs multiple Docker containers. One important rule to remember is:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;
&lt;code&gt;&lt;strong&gt;localhost&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt; on your host is not the same as &lt;/strong&gt;&lt;code&gt;&lt;strong&gt;localhost&lt;/strong&gt;&lt;/code&gt;&lt;strong&gt; inside a container.&lt;/strong&gt;
&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;OAuth token validation checks the token’s &lt;strong&gt;issuer&lt;/strong&gt; claim (&lt;code&gt;iss&lt;/code&gt;). If Keycloak issues a token with an issuer like &lt;code&gt;http://localhost:8080/...&lt;/code&gt; but IRIS discovers/validates it using &lt;code&gt;http://keycloak:8080/...&lt;/code&gt;, IRIS will reject the token because those issuers do not match.&lt;/p&gt;
&lt;p&gt;To keep the issuer stable, this demo uses the hostname &lt;code&gt;&lt;strong&gt;keycloak&lt;/strong&gt;&lt;/code&gt; consistently from both the host and the containers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On Windows&lt;/strong&gt;, edit: &lt;em&gt;C:\Windows\System32\drivers\etc\hosts&lt;/em&gt; and add:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;127.0.0.1 keycloak&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;On Linux/Mac&lt;/strong&gt;, edit &lt;em&gt;/etc/hosts&lt;/em&gt; and add the same line (you’ll typically need sudo).&lt;/p&gt;
&lt;p&gt;From this point on, use &lt;strong&gt;http://keycloak:8080&lt;/strong&gt; (not &lt;code&gt;http://localhost:8080&lt;/code&gt;) when configuring Postman and IRIS.&lt;/p&gt;
&lt;h3&gt;Step 1: Configure the Authorization Server (Keycloak)&lt;/h3&gt;
&lt;p&gt;For the demo, the Authorization Server is Keycloak and it is already prepared for this use case (realm, clients, users, scopes). No work is needed here.&lt;/p&gt;
&lt;p&gt;You can access the Keycloak admin console at &lt;a href="http://keycloak:8080/keycloak/admin/master/console/" rel="noopener noreferrer"&gt;http://keycloak:8080/keycloak/admin/master/console/&lt;/a&gt; (username/password &lt;code&gt;admin/admin&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Explaining Keycloak itself is not in the scope for this article, but if you would like to read more you can find the docs &lt;a href="https://www.keycloak.org/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Step 2: Tell IRIS who the Authorization Server is&lt;/h3&gt;
&lt;p&gt;In the Management Portal, go to:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;System Administration &amp;gt; Security &amp;gt; OAuth 2.0 &amp;gt; Client&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Click &lt;em&gt;Create Server Description&lt;/em&gt;, set the Issuer URL (in the demo: &lt;code&gt;http://keycloak:8080/keycloak/realms/bank&lt;/code&gt;), then click &lt;em&gt;Discover&lt;/em&gt; and &lt;em&gt;Save&lt;/em&gt;. IRIS will pull the endpoints and metadata it needs from the server (authorization endpoint, token endpoint, JWKS URI, etc.).&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%2F1zv1wzzmxju2gdzptatd.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%2F1zv1wzzmxju2gdzptatd.png" alt=" " width="799" height="284"&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%2F7bxmy7spsjgv5cjvvzbi.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%2F7bxmy7spsjgv5cjvvzbi.png" alt=" " width="800" height="849"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt;Step 3: Configure IRIS as the Resource Server&lt;/h3&gt;
&lt;p&gt;Next, create a Resource Server entry so IRIS can validate tokens and enforce permissions:&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%2Fixmudlbnica1sdog3loe.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%2Fixmudlbnica1sdog3loe.png" alt=" " width="633" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;em&gt;Create Resource Server&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%2F2oo938k51v78c0msumyf.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%2F2oo938k51v78c0msumyf.png" alt=" " width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fill in the details of your resource server, for example:&lt;/p&gt;
&lt;p&gt;Name: IRIS Bank Resource Server&lt;/p&gt;
&lt;p&gt;Server Definition:&amp;nbsp;&lt;code&gt;http://keycloak:8080/keycloak/realms/bank&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Audiences: bank-demo, bank-monitor&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is “Audience”?&lt;/strong&gt; The token’s audience (&lt;code&gt;aud&lt;/code&gt;) is the “intended recipient” of the token. By configuring audiences here, you are telling IRIS to accept only tokens that were issued for this API (i.e., tokens whose &lt;code&gt;aud&lt;/code&gt; matches one of these values).&lt;/p&gt;
&lt;p&gt;Click save.&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%2Fdqfdroqs1b1k7is1p0d7.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%2Fdqfdroqs1b1k7is1p0d7.png" alt=" " width="710" height="746"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will set the Authenticator class in the next step. Note that this is not strictly necessary; you could use the &lt;a href="https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic.cls?LIBRARY=%25SYS&amp;amp;CLASSNAME=%25OAuth2.ResourceServer.SimpleAuthenticator" rel="noopener noreferrer"&gt;%OAuth2.ResourceServer.SimpleAuthenticator&lt;/a&gt; in your own implementations and just fill in what token property should be attributed to the role and user. However, for the sake of completeness we will create a simple custom authenticator class.&lt;/p&gt;
&lt;h3&gt;Step 4: Create your Authenticator Class&lt;/h3&gt;
&lt;p&gt;What should be authenticated? We will create a simple class &lt;code&gt;Bank.Authenticator&lt;/code&gt; that maps token claims/scopes into an IRIS username and IRIS roles.&lt;/p&gt;
&lt;p&gt;This is the key step that lets IRIS enforce “read-only” vs “transfer” behavior:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The token’s &lt;strong&gt;scopes&lt;/strong&gt; become &lt;strong&gt;IRIS roles&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Your &lt;strong&gt;web application&lt;/strong&gt; (and/or your REST endpoints) can require those roles.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, this is what makes &lt;code&gt;/checkbalance&lt;/code&gt;&amp;nbsp; succeed for a “monitor” token while &lt;code&gt;/transfer&lt;/code&gt; returns &lt;strong&gt;403 Forbidden&lt;/strong&gt; unless the token includes the transfer scope.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class="mention"&gt;Class&lt;/span&gt; Bank.Authenticator &lt;span class="mention"&gt;Extends&lt;/span&gt; &lt;span class="mention"&gt;%OAuth&lt;/span&gt;2.ResourceServer.Authenticator
{

&lt;span class="mention"&gt;ClassMethod&lt;/span&gt; HasScope(scopeStr &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%String&lt;/span&gt;, scope &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%String&lt;/span&gt;) &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%Boolean&lt;/span&gt;
{
    &lt;span class="mention"&gt;Quit&lt;/span&gt; ((&lt;span class="mention"&gt;" "&lt;/span&gt;_scopeStr_&lt;span class="mention"&gt;" "&lt;/span&gt;) [ (&lt;span class="mention"&gt;" "&lt;/span&gt;_scope_&lt;span class="mention"&gt;" "&lt;/span&gt;))
}

Method Authenticate(claims &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%DynamicObject&lt;/span&gt;, oidc &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%Boolean&lt;/span&gt;, Output properties &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%String&lt;/span&gt;) &lt;span class="mention"&gt;As&lt;/span&gt; &lt;span class="mention"&gt;%Status&lt;/span&gt;
{
    &lt;span class="mention"&gt;// Map token -&amp;gt; IRIS username&lt;/span&gt;
    &lt;span class="mention"&gt;Set&lt;/span&gt; properties(&lt;span class="mention"&gt;"Username"&lt;/span&gt;) = claims.&lt;span class="mention"&gt;"preferred_username"&lt;/span&gt;
    &lt;span class="mention"&gt;// Map scopes -&amp;gt; IRIS roles&lt;/span&gt;
    &lt;span class="mention"&gt;Set&lt;/span&gt; scopeStr = claims.scope
    &lt;span class="mention"&gt;Set&lt;/span&gt; roles = &lt;span class="mention"&gt;""&lt;/span&gt;
    &lt;span class="mention"&gt;If&lt;/span&gt; &lt;span class="mention"&gt;..HasScope&lt;/span&gt;(scopeStr,&lt;span class="mention"&gt;"bank.balance.read"&lt;/span&gt;) {
        &lt;span class="mention"&gt;Set&lt;/span&gt; roles = roles_&lt;span class="mention"&gt;",BankBalanceRead,%DB_USER"&lt;/span&gt;
    }
    &lt;span class="mention"&gt;If&lt;/span&gt; &lt;span class="mention"&gt;..HasScope&lt;/span&gt;(scopeStr,&lt;span class="mention"&gt;"bank.transfer.write"&lt;/span&gt;) {
        &lt;span class="mention"&gt;Set&lt;/span&gt; roles = roles_&lt;span class="mention"&gt;",BankTransferWrite,%DB_USER"&lt;/span&gt;
    }

    &lt;span class="mention"&gt;If&lt;/span&gt; &lt;span class="mention"&gt;$Extract&lt;/span&gt;(roles,&lt;span class="mention"&gt;1&lt;/span&gt;)=&lt;span class="mention"&gt;","&lt;/span&gt; &lt;span class="mention"&gt;Set&lt;/span&gt; roles=&lt;span class="mention"&gt;$Extract&lt;/span&gt;(roles,&lt;span class="mention"&gt;2&lt;/span&gt;,*)
    
    &lt;span class="mention"&gt;Set&lt;/span&gt; properties(&lt;span class="mention"&gt;"Roles"&lt;/span&gt;) = roles
    &lt;span class="mention"&gt;Quit&lt;/span&gt; &lt;span class="mention"&gt;$$$OK&lt;/span&gt;
}

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you compile the class you will be able to set your authenticator class in your resource server:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm3w2ivquudx3a3tbhjnd.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%2Fm3w2ivquudx3a3tbhjnd.png" alt=" " width="596" height="84"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Save your resource server.&lt;/p&gt;
&lt;h3&gt;Step 5: Enable OAuth2 on the Web Application&lt;/h3&gt;
&lt;p&gt;Before enabling OAuth2 for a web app, you must enable it at the System level:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;System Administration &amp;gt; Security &amp;gt; System Security &amp;gt; Authentication/Web Session Options&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%2Frtfo4ojcvhxceg6dzhto.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%2Frtfo4ojcvhxceg6dzhto.png" alt=" " width="800" height="592"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, on your Web Application definition, select &lt;strong&gt;OAuth2&lt;/strong&gt; as an allowed authentication method. The dispatch class will check that the client has the necessary roles.&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%2Fi7w9dmwujexq08s5ahig.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%2Fi7w9dmwujexq08s5ahig.png" alt=" " width="800" height="487"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;h3&gt;Step 6: Test it out&lt;/h3&gt;
&lt;p&gt;At this point, requests to your application can be authorized based on the presented token — so you can allow read-only access to &lt;code&gt;/checkbalance&lt;/code&gt;&amp;nbsp;while denying access to &lt;code&gt;/transfer&lt;/code&gt; using the OAuth2 framework.&lt;/p&gt;
&lt;p&gt;Load the Postman collection and environment. There are two demo users/passwords to have in mind: &lt;code&gt;user1/123&lt;/code&gt; and &lt;code&gt;user2/123&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;User 1 has account &lt;code&gt;ACCT-1&lt;/code&gt;, User 2 has account &lt;code&gt;ACCT-2&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%2Fuq8lfej5wq4onxplbsty.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%2Fuq8lfej5wq4onxplbsty.png" alt=" " width="542" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Postman, on Authorization click &lt;em&gt;Get New Access Token&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%2Fnw20i0mxxivw0yp78d9p.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%2Fnw20i0mxxivw0yp78d9p.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This brings up the login screen for our Authorization Server:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fggcyxhb9ciy4qptk5jwq.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%2Fggcyxhb9ciy4qptk5jwq.png" alt=" " width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Log in with &lt;code&gt;user1/123&lt;/code&gt;. Click proceed and then click &lt;em&gt;Use Token&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%2Fgpnxsyzjxw1z08r1dsxq.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%2Fgpnxsyzjxw1z08r1dsxq.png" alt=" " width="710" height="138"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Send your GET to &lt;code&gt;/checkbalance&lt;/code&gt;&amp;nbsp;and you should see it return 5 dollars:&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%2Fovkq7p71brjbwbqlt1sp.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%2Fovkq7p71brjbwbqlt1sp.png" alt=" " width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clear cookies and try logging in with user 2 and you should see them have 0 dollars in their balance.&lt;/p&gt;
&lt;p&gt;Now get a token for user 1 and try to transfer user 2 a couple dollars. It should fail with &lt;strong&gt;403 Forbidden&lt;/strong&gt;&amp;nbsp;as this “app” does not have the required scopes (it is only monitoring the bank account and should not be able to transfer money).&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%2Fagwxt6ligch3sdktggt4.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%2Fagwxt6ligch3sdktggt4.png" alt=" " width="799" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try again with requests 3 and 4 which simulate a client with full access and you should be able to both check your balance and transfer funds.&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%2Fupjdudkgdsbbvrkbfsjw.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%2Fupjdudkgdsbbvrkbfsjw.png" alt=" " width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The new OAuth2 native authentication type ensures it is intuitive to keep your web applications safe, and after all, that's what the I in IRIS is all about.&lt;/p&gt;

</description>
      <category>api</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Explainability in ML Models</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 30 Jun 2026 16:32:00 +0000</pubDate>
      <link>https://dev.to/intersystems/explainability-in-ml-models-4300</link>
      <guid>https://dev.to/intersystems/explainability-in-ml-models-4300</guid>
      <description>&lt;p&gt;This article introduces SHAP explainability methods as an approach to understand the reasons behind predictions in machine learning black-box models. It also includes a simple Jupyter notebook that you can use and modify to gain hands-on experience with these concepts:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.kaggle.com/code/jorgeivnjh/explainability-in-ml-models" rel="noopener noreferrer"&gt;https://www.kaggle.com/code/jorgeivnjh/explainability-in-ml-models&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/JorgeIvanJH/Explainability-in-ML-models" rel="noopener noreferrer"&gt;https://github.com/JorgeIvanJH/Explainability-in-ML-models&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will leverage these concepts for a future implementation in our Continuous Training Pipeline: &lt;a href="https://community.intersystems.com/post/complementing-iris-mlflow-continuous-training-ct-pipeline" rel="noopener noreferrer"&gt;https://community.intersystems.com/post/complementing-iris-mlflow-continuous-training-ct-pipeline&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;In this notebook, we provide intuition about explainability for black-box models. Black-box models are those that are too complex for a human to directly understand, such as neural networks and ensemble methods like gradient boosting (e.g. XGBoost, LightGBM, CatBoost).&lt;/p&gt;

&lt;p&gt;Before starting, it is worth clarifying the difference between interpretable models and explainable models:&lt;/p&gt;

&lt;p&gt;Interpretable models are those where we can directly understand how changes in the inputs affect the output, just by looking at the model itself.&lt;br&gt;
This is the case for linear regression, where each variable is associated with a coefficient that indicates how it influences the prediction. It is also true for a single decision tree, where, by following the branches, we can understand exactly how a prediction is made.&lt;/p&gt;

&lt;p&gt;In contrast, models such as random forests and gradient boosting (e.g. XGBoost, LightGBM), which combine many trees, or neural networks with thousands or millions of parameters, are too complex for this type of direct interpretation. In these cases, we rely on explainability methods to understand how the model is using the input features to produce its predictions.&lt;/p&gt;

&lt;p&gt;To provide explainability for such models, we typically take a trained model and analyse how changes in the input features affect its output. There are many approaches to do this (e.g. partial dependence plots, ICE plots, LIME), but one of the most widely used and mathematically grounded methods is SHAP.&lt;/p&gt;

&lt;p&gt;SHAP (SHapley Additive exPlanations) is based on game theory and computes Shapley values, which quantify how much each feature contributes to a model’s prediction. These contributions can be analysed both globally (across the dataset) and locally (for individual predictions).&lt;/p&gt;

&lt;p&gt;In this notebook, we use the SHAP python library to explore these ideas. We start with a simple, interpretable model (linear regression), and then move to a more complex model (LightGBM). Along the way, we introduce some of the most commonly used plots to explain model behaviour.&lt;/p&gt;

&lt;p&gt;Note: To run this notebook in Kaggle you must have logged in with your account and have access to internet (Settings - Turn on internet)&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shap&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;plotly.express&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lightgbm&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt; &lt;span class="c1"&gt;# for a quick lightgbm hyperparameter tuning
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filenames&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/kaggle/input&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;filenames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this exercise, we leverage the California Housing dataset as ground truth. Which takes the following variables associated with the price of the house:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MedInc (float): Median income in block&lt;/li&gt;
&lt;li&gt;HouseAge (float): Median house age in block&lt;/li&gt;
&lt;li&gt;AveRooms (float): Average rooms in dwelling&lt;/li&gt;
&lt;li&gt;AveBedrms (float): Average bedrooms in dwelling&lt;/li&gt;
&lt;li&gt;Population (float): Block population&lt;/li&gt;
&lt;li&gt;AveOccup (float): Average house occupancy&lt;/li&gt;
&lt;li&gt;Latitude (float): House block latitude&lt;/li&gt;
&lt;li&gt;Longitude (float): House block longitude&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To predict the variable of interest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MedHouseVal (float): Median House Value, expressed in units of $100,000 (e.g., a value of 4.526 represents $452,600)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datasets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;california&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_points&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# The dataset
&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Linear Regression (Interpretable model)
&lt;/h1&gt;

&lt;p&gt;Linear regression is a simple yet powerful model that is particularly easy to interpret. This is because, after the model is fit, each variable is associated with a parameter (coefficient) that it is multiplied by, transforming its value into the units of the variable of interest. The sum of all these transformed variables, plus an offset (intercept), is what produces the final prediction.&lt;/p&gt;

&lt;p&gt;For example, by analysing the parameters of an already fit house price prediction model, we can get an idea of how each variable influences the price of a house in our dataset:&lt;/p&gt;

&lt;p&gt;HousePrice = MedInc * (0.4) + AveBedrms * (5000) + Latitude * (-0.5) + 50000&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The offset suggests that the baseline value of a house, when all variables are zero, is 50,000.&lt;/li&gt;
&lt;li&gt;Median income in the block (MedInc) increases the price of the house at a rate of 0.4 (each additional dollar in income adds 0.4 dollars to the predicted house price), and each additional bedroom adds 5,000 dollars.&lt;/li&gt;
&lt;li&gt;Latitude has a negative coefficient, suggesting that as we move north (latitude increases), house prices decrease. Since latitude increases northward, the negative coefficient implies a downward effect on price.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This initial interpretation is useful; however, there is an important limitation: the scale of the parameters can be misleading without context. If variables are not normalised, those with large numerical scales will tend to have smaller coefficients, and vice versa.&lt;/p&gt;

&lt;p&gt;For example, if median house age ("HouseAge") were measured in seconds instead of years, its coefficient would be much smaller, simply because the input values are much larger. This could give the misleading impression that HouseAge is less important than other variables, when in reality the difference is only due to the units of measurement. In contrast, a variable like AveBedrms might have a larger coefficient simply because it operates on a smaller numerical scale.&lt;/p&gt;

&lt;p&gt;Now to the actual model on our dataset:&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&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linear_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LinearRegression&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model coefficients:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coef_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Intercept = &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intercept_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Model performance metrics:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;r2_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_valid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;mae&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean_absolute_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_valid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r2 score: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mae: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;mae&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model coefficients:

MedInc = 0.41174
HouseAge = 0.00932
AveRooms = -0.109
AveBedrms = 0.63208
Population = 4e-05
AveOccup = -0.25315
Latitude = -0.46534
Longitude = -0.46173
Intercept =  -37.84145

Model performance metrics:

r2 score:  0.6822806366957364
mae:  0.5473856962442348
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The analysis we did works well, but it provides a static, global understanding of how each variable affects the output across all samples. It fails to capture interactions between variables and is mainly limited to linear models. If we were using more complex models (e.g. neural networks or tree-based models), this type of interpretation would not be sufficient to understand the relationships being learned.&lt;/p&gt;

&lt;p&gt;This is where SHAP comes in. SHAP (Shapley values) is a method based on game theory that allows us to understand the marginal contribution of each feature to a model’s prediction. We will omit the theory behind it, but intuitively, it tells us how each feature steers a prediction away from the model’s average prediction. To make this easier to understand, we can compare it to linear regression. In linear regression, the intercept acts as a baseline, and each feature multiplied by its coefficient shifts the prediction away from that baseline. In SHAP, the baseline is the average prediction of the model across the dataset (the expected value), and the Shapley value of each feature represents how much that feature contributes to moving from this baseline to the final prediction for a given sample.&lt;/p&gt;

&lt;p&gt;Unlike linear regression coefficients, which provide a single global interpretation, SHAP allows us to compute per-sample explanations, showing how each feature contributes to an individual prediction, not just on average across the dataset.&lt;/p&gt;

&lt;p&gt;Below, we take a subsample of the data to use as a reference dataset for comparisons (we could use the full dataset, but that would be computationally expensive). We then create an explainer object for the linear regression model we trained, compute the SHAP values (which represent the contribution of each feature to each prediction), and select a specific sample (sample #20) to analyse the relationships captured by the model.&lt;/p&gt;

&lt;p&gt;Note: To understand the underlying SHAP algorithm, refer to: &lt;a href="https://christophm.github.io/interpretable-ml-book/shapley.html" rel="noopener noreferrer"&gt;https://christophm.github.io/interpretable-ml-book/shapley.html&lt;/a&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;X100&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Subsample to use as background dataset (SHAP needs one for its internal algorithm)
&lt;/span&gt;&lt;span class="n"&gt;explainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Explainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;shap_values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;explainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sample_ind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ExactExplainer explainer: 901it [00:12, 30.31it/s]                          
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having SHAP values computed allows us to draw different plots to better understand the reasons behind a model’s predictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependence plot (+ ICE lines)
&lt;/h2&gt;

&lt;p&gt;A dependence plot helps us understand how the model output changes as a feature varies, as well as how frequently different values of that feature occur in the data. More specifically, it shows how the model’s prediction evolves across the range of a feature, while also giving a sense of how common those values are. This helps us see not only the effect of a feature, but also how relevant that effect is in practice. As a result, values that have a strong effect but occur rarely may end up being less important overall than values that have a weaker effect but occur frequently.&lt;/p&gt;

&lt;p&gt;In the plot below, we overlay both the dependence plot and the Individual Conditional Expectation (ICE) lines, displaying one line per instance that shows how the instance’s prediction changes when a feature changes. We show the behaviour for the variable "Latitude":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The average value of the feature Latitude (grey vertical dashed line), at around 36&lt;/li&gt;
&lt;li&gt;The average model prediction for the price of a house (grey horizontal dashed line), at nearly 2 ($200.000).&lt;/li&gt;
&lt;li&gt;The bold blue line represents the average model prediction as we vary Latitude across its range (this is the partial dependence, i.e. the global trend)&lt;/li&gt;
&lt;li&gt;The lighter blue lines represent the model predictions for individual samples as we vary Latitude (ICE curves). Each line corresponds to one sample, where we change only the Latitude and keep the rest of the features fixed&lt;/li&gt;
&lt;li&gt;The red vertical segment marks the selected sample (sample_ind). It shows how that specific sample’s prediction shifts relative to the baseline (expected value), highlighting the contribution of Latitude for that instance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the blue lines are linear because the underlying model is a linear regression. We can clearly see a negative relationship: as Latitude increases (moving north), the predicted house price decreases across all samples.&lt;/p&gt;

&lt;p&gt;The spread of ICE lines does not indicate how important a feature is, but rather how consistent its effect is across samples. If the lines are tightly grouped, the feature has a similar effect across the dataset. If they are widely spread, the feature interacts with other variables, and its effect depends on the specific sample.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partial_dependence_plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;X100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# Change to false to see only general trend
&lt;/span&gt;    &lt;span class="n"&gt;model_expected_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;feature_expected_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sample_ind&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sample_ind&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&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%2Fqk8tz6w14jmds2ryaqsd.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%2Fqk8tz6w14jmds2ryaqsd.png" alt=" " width="624" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &amp;nbsp;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Scatter Plot
&lt;/h2&gt;

&lt;p&gt;Another way to visualise how a variable influences the model output is through a scatter plot. In this plot, we place the feature values on the x-axis and their corresponding SHAP values on the y-axis, showing how changes in the feature affect the prediction.&lt;/p&gt;

&lt;p&gt;By passing shap_values to the "color" argument, SHAP automatically selects the feature that is most strongly correlated (or interacting) with the SHAP values of the selected feature, and uses it to colour the points.&lt;/p&gt;

&lt;p&gt;In our example, we analyse Latitude, and SHAP identifies Longitude as the feature most related to it, which is then shown through the colour scale. (Please note that this is consistent with the wide spread on the lines in the ICE plot above associated with interaction with another variable)&lt;/p&gt;

&lt;p&gt;In this plot, we observe that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Points with lower latitude (further south) tend to have higher longitude values (red), meaning they are also located more to the east&lt;/li&gt;
&lt;li&gt;Points with higher latitude (further north) tend to have lower longitude values (blue), meaning they are also located more to the west&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern is consistent with the density of houses around the two main population centres in California: San Francisco (northwest) and Los Angeles (southeast).&lt;/p&gt;

&lt;p&gt;The SHAP values follow a clear negative linear trend (as expected from a linear model), showing that as latitude increases, its contribution to the prediction decreases.&lt;/p&gt;

&lt;p&gt;This implies that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Houses in the southeast (low latitude, high longitude) tend to have a positive contribution to the predicted price&lt;/li&gt;
&lt;li&gt;Houses in the northwest (high latitude, low longitude) tend to have a negative contribution to the predicted price&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, in this dataset, the model associates southeastern locations with higher predicted prices and northwestern locations with lower predicted prices.&lt;/p&gt;

&lt;p&gt;Note: It may be the case that houses in Los Angeles are more expensive than those in San Francisco, and that the geographic location of these cities is driving the pattern we observe. However, this analysis is purely observational, and we are not performing any hypothesis testing or causal inference here.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&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%2F7zbubtym4tj6400g1u9r.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%2F7zbubtym4tj6400g1u9r.png" alt=" " width="670" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Waterfall Plot
&lt;/h2&gt;

&lt;p&gt;This plot gives us a per-sample explanation of the model’s prediction, showing how each variable contributed to the final output for a single observation, rather than how a variable behaves across the entire dataset.&lt;/p&gt;

&lt;p&gt;In the plot below, we see how each feature contributes to moving the model’s expected value (the average prediction across all samples, E[f(X)]) to the final prediction for a specific sample (sample_ind).&lt;/p&gt;

&lt;p&gt;Starting from an expected house price of E[f(X)], each variable adds or subtracts from this baseline until we reach the final prediction for that sample, f(x). We can verify this by comparing the model prediction of that specific sample, and the one shown on the plot at f(x):&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&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;sample_ind&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;:])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;array([1.9473549])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the plot, we observe that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Variables shown in blue contribute to pulling the prediction downwards&lt;/li&gt;
&lt;li&gt;Variables shown in red contribute to pushing the prediction upwards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The magnitude of each bar represents how much that feature contributes to the prediction for this specific sample.&lt;/p&gt;

&lt;p&gt;These individual contributions are what we call SHAP values: they quantify how much each feature shifts the prediction away from the baseline E[f(X)] to reach the final output.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waterfall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sample_ind&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;max_display&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&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%2Fnd1ozg46lfxuzn9mltch.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%2Fnd1ozg46lfxuzn9mltch.png" alt=" " width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: Compare the waterfall plot and the dependence plot, and observe how the SHAP value for the Latitude variable is consistent in both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beeswarm Plot
&lt;/h2&gt;

&lt;p&gt;This plot shows the SHAP values of every variable across all samples. Each point represents a sample, positioned according to its SHAP value (impact on the model output), while the colour indicates the value of the feature (red = high value, blue = low value).&lt;/p&gt;

&lt;p&gt;This allows us to understand both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How the value of a feature influences the prediction&lt;/li&gt;
&lt;li&gt;How common different effects are (denser regions indicate more samples with similar contributions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below is the beeswarm plot for our California housing dataset:&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beeswarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&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%2F1q9jqfay992ivc9lwnm0.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%2F1q9jqfay992ivc9lwnm0.png" alt=" " width="744" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Analysing the plot, we can observe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MedInc and AveBedrms:
Both features show a right-skewed pattern in their SHAP values, with a few samples having very large positive contributions. In particular, higher values (red) are associated with strong positive SHAP values, meaning that higher income levels and a larger number of bedrooms tend to significantly increase the predicted house price.
These high-value observations are relatively rare but have a strong influence on the model’s predictions.&lt;/li&gt;
&lt;li&gt;AveOccup and AveRooms:
These features have SHAP values that are mostly concentrated around zero, indicating that for most samples they have a limited impact on the model’s prediction.
However, some high-value outliers (red points) show strong negative SHAP values, meaning that unusually high occupancy or number of rooms can significantly decrease the predicted house price.&lt;/li&gt;
&lt;li&gt;HouseAge and Population:
These features have SHAP values tightly clustered around zero, suggesting they have little to no impact on the model’s predictions overall.&lt;/li&gt;
&lt;li&gt;Latitude (North–South):
There is a clear pattern where higher latitude values (red, more northern locations) tend to have negative SHAP values, meaning they decrease the predicted house price.
Lower latitude values (blue, more southern locations) tend to have positive SHAP values, increasing the prediction.
This suggests that, in our dataset, houses further north tend to be cheaper, while those further south tend to be more expensive.&lt;/li&gt;
&lt;li&gt;Longitude (East–West):
We observe two main clusters of values. Lower longitude values (blue, more western locations) tend to have positive SHAP values, while higher longitude values (red, more eastern locations) tend to have negative SHAP values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This Latitude-Longitude behaviour is consistent with the geographic distribution of the main population centres in California. If we plot the data on a map, we can clearly see two dense clusters: one in the northwest (San Francisco area) and one in the southeast (Los Angeles area).&lt;/p&gt;

&lt;p&gt;Using a density map:&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;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;
&lt;span class="n"&gt;meanlat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Latitude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;meanlon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Longitude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;fig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;density_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Longitude&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;center&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;meanlat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;meanlon&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                    &lt;span class="n"&gt;zoom&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;map_style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open-street-map&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&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%2Fkgixwwe29i28qc9kyrar.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%2Fkgixwwe29i28qc9kyrar.png" alt=" " width="541" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  LightGBM (Explainable model)
&lt;/h1&gt;

&lt;p&gt;Now we switch to a more complex model, one based on gradient boosting: LightGBM, which should be able to detect more hidden and non-linear patterns in our dataset. To quickly find optimum hyperparameters, we will use the "Optuna" automatic hyperparameter optimisation framework for ML.&lt;/p&gt;

&lt;p&gt;We see that performance metrics have improved with the capacity of this model to capture non-linear patterns and interactions between features.&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;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_verbosity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;best_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;best_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-inf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;best_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;

    &lt;span class="n"&gt;train_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;free_raw_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;valid_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;train_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;free_raw_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;objective&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;regression&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metric&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean_squared_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boosting_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gbdt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verbosity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;boosting_type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gbdt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lambda_l1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lambda_l1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;lambda_l2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1e-8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;num_leaves&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;num_leaves&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;feature_fraction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;feature_fraction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bagging_fraction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bagging_fraction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bagging_freq&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bagging_freq&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;min_child_samples&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;trial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suggest_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;min_child_samples&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;train_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid_sets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;valid_data&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lgb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_evaluation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
    &lt;span class="n"&gt;preds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;r2_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;best_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;
        &lt;span class="n"&gt;best_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;


&lt;span class="n"&gt;study&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;optuna&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_study&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;maximize&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;study&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;optimize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_trials&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="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;best_model&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Model performance metrics:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;preds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r2 score:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;r2_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mae:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sklearn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean_absolute_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model performance metrics:

r2 score: 0.8105733216454589
mae: 0.39733995113249015
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;explainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Explainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;shap_values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;explainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ExactExplainer explainer: 901it [00:13, 16.52it/s]                         
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Dependence plot (+ ICE lines).
&lt;/h2&gt;

&lt;p&gt;With this new model, we observe a richer and more complex relationship between Latitude and house prices in California.&lt;/p&gt;

&lt;p&gt;There is a clear distinction across different latitude ranges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For lower latitude values (southern regions, around Los Angeles), the ICE lines tend to lie above the baseline, indicating a consistent positive contribution to the predicted house prices.&lt;/li&gt;
&lt;li&gt;For higher latitude values (northern regions, around and above San Francisco), the ICE lines tend to lie below the baseline, indicating a negative contribution to the predicted prices.&lt;/li&gt;
&lt;li&gt;In the intermediate range of latitude, the lines cluster around the baseline, suggesting that houses in this region have little to no impact on the prediction, keeping prices close to the dataset average.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, compared to the linear model, this plot shows that the effect of Latitude is no longer strictly linear, but varies depending on the region, capturing more nuanced geographic patterns.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partial_dependence_plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;X100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# Change to false to see only general trend
&lt;/span&gt;    &lt;span class="n"&gt;model_expected_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;feature_expected_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sample_ind&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sample_ind&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&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%2Fs2gjep33dl4urcdfs2og.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%2Fs2gjep33dl4urcdfs2og.png" alt=" " width="611" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Scatter Plot.
&lt;/h2&gt;

&lt;p&gt;Once again, we get a clearer view of the non-linearities and interactions in the data. In this scatter plot, SHAP identifies Longitude as the feature most strongly related to the SHAP values of Latitude, and uses it to colour the points.&lt;/p&gt;

&lt;p&gt;From the plot, we observe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Points in the southeast (low latitude, high longitude – shown in red) are more tightly clustered and have a positive contribution to house prices.&lt;/li&gt;
&lt;li&gt;Points in the northwest (high latitude, low longitude – shown in blue) are more spread out and have a negative contribution to house prices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also see clear non-linear transitions:&lt;/p&gt;

&lt;p&gt;Around latitude 34–35, the contribution of Latitude shifts from mostly positive to neutral/negative&lt;br&gt;
Around latitude 38, there is a sharper drop, after which Latitude has a strong negative impact on predicted prices&lt;/p&gt;

&lt;p&gt;There is a small region around latitude ~38 where some points show a slight positive contribution, but overall, the dominant effect in that range is negative.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&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%2F7ifmc49zawqx9iftp9yi.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%2F7ifmc49zawqx9iftp9yi.png" alt=" " width="694" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Waterfall Plot.
&lt;/h2&gt;

&lt;p&gt;In the waterfall plot, we see that although the direction of influence of most variables remains similar, the magnitude of their contributions changes significantly compared to the linear model.&lt;/p&gt;

&lt;p&gt;In this case, we observe that the influence of Latitude and Longitude, which previously dominated the prediction, is now more distributed across other variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Latitude still has a positive contribution, but its effect is noticeably smaller than in the linear model&lt;/li&gt;
&lt;li&gt;Longitude still has a negative contribution, but its magnitude is also reduced, and it is no longer the second most influential variable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, this model captures effects that the linear regression was not able to identify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average house occupancy (AveOccup) now shows a strong negative contribution for this sample, which was much weaker in the linear model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall, the model spreads the contribution across more features, reflecting a more complex set of relationships between the inputs and the predicted house price.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waterfall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sample_ind&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;max_display&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&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%2Fib5cxm5mlyzwugf7pf1n.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%2Fib5cxm5mlyzwugf7pf1n.png" alt=" " width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Beeswarm Plot.
&lt;/h2&gt;

&lt;p&gt;In this beeswarm plot, we observe that the overall direction of influence of the features is broadly consistent with what we saw in the linear regression model, but the distribution of their effects has changed significantly.&lt;/p&gt;

&lt;p&gt;We can see that LightGBM spreads the influence of feature values more evenly across the dataset. Unlike linear regression, we no longer observe a few extreme outliers dominating the predictions. This reflects one of the limitations of linear models, which can be highly sensitive to outliers.&lt;/p&gt;

&lt;p&gt;At the same time, the relative importance of the main variables remains similar, with features like Latitude, MedInc, and Longitude still playing a dominant role.&lt;/p&gt;

&lt;p&gt;However, we now observe that features that previously had little influence in the linear model contribute more meaningfully to the predictions. This is the case for variables such as HouseAge, AveBedrms, and Population, which now show a wider spread of SHAP values.&lt;/p&gt;

&lt;p&gt;Looking more closely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For Population, lower values can now lead to both positive and negative contributions, indicating that its effect depends on the context (i.e. interactions with other features)&lt;/li&gt;
&lt;li&gt;A similar pattern appears in AveRooms and Longitude, where both high and low values can produce different impacts depending on the sample&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This highlights the key difference with linear regression: the model is no longer assigning a single fixed effect to each feature, but instead capturing non-linear relationships and interactions between variables.&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;shap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beeswarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shap_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&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%2Fi3cqw72w39blqy6cs91m.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%2Fi3cqw72w39blqy6cs91m.png" alt=" " width="744" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Final Remarks
&lt;/h1&gt;

&lt;p&gt;Please bear in mind not to interpret these plots with causality in mind (i.e. do not draw strong conclusions from them). The plots we have seen show how a trained model responds to different inputs, but they are not a faithful representation of reality, nor a direct explanation of what truly happens inside the model. Rather, they provide an external approximation to help us understand behaviour that would otherwise be too complex to interpret.&lt;/p&gt;

&lt;p&gt;Content in this notebook is heavily inspired by the SHAP documentation:&lt;br&gt;
&lt;a href="https://shap.readthedocs.io/en/latest/example_notebooks/overviews/An%20introduction%20to%20explainable%20AI%20with%20Shapley%20values.html" rel="noopener noreferrer"&gt;https://shap.readthedocs.io/en/latest/example_notebooks/overviews/An%20introduction%20to%20explainable%20AI%20with%20Shapley%20values.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a deeper understanding of the theory behind explainability, see:&lt;br&gt;
&lt;a href="https://christophm.github.io/interpretable-ml-book/" rel="noopener noreferrer"&gt;https://christophm.github.io/interpretable-ml-book/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Please feel free to modify the code used in the plots above to analyse other variables in more detail and extract your own conclusions.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>python</category>
    </item>
    <item>
      <title>OMOP Odyssey - AWS HealthLake ( Strait of Messina )</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 23 Jun 2026 17:33:46 +0000</pubDate>
      <link>https://dev.to/intersystems/omop-odyssey-aws-healthlake-strait-of-messina--74g</link>
      <guid>https://dev.to/intersystems/omop-odyssey-aws-healthlake-strait-of-messina--74g</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%2Fj4s067xwn59cnj1xtvga.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%2Fj4s067xwn59cnj1xtvga.png" alt=" " width="799" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Nearline FHIR® Ingestion to InterSystems OMOP from AWS HealthLake&lt;/h2&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%2Fk3at9sdoqok0g9ahy0x4.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%2Fk3at9sdoqok0g9ahy0x4.png" alt=" " width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This part of the &lt;a href="https://community.intersystems.com/smartsearch?search=OMOP+Odyssey" rel="noopener noreferrer"&gt;OMOP Journey&lt;/a&gt;,&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;we reflect before attempting to challenge&amp;nbsp;&lt;em&gt;Scylla&amp;nbsp;&lt;/em&gt;on how fortunate we are that InterSystems OMOP transform is built on the Bulk FHIR Export&amp;nbsp;as the source payload.&amp;nbsp; This opens up &lt;strong&gt;hands off interoperability&lt;/strong&gt; with the InterSystems OMOP transform across several FHIR® vendors, including Amazon Web Services HealthLake.&lt;/p&gt;

&lt;h4&gt;HealthLake Bulk FHIR Export&lt;br&gt;&amp;nbsp;&lt;/h4&gt;

&lt;p&gt;Healthlake supports bulk fhir import/export from the cli or api, the premise is&amp;nbsp;simple and the docs are over exhaustive, we'll save a model the trouble of training on it again and &lt;a href="https://docs.aws.amazon.com/healthlake/latest/devguide/exporting-fhir-data.html" rel="noopener noreferrer"&gt;link&lt;/a&gt; it if interested.&amp;nbsp; The more valuable thing to understand of the heading of this paragraph is the implementation of the&amp;nbsp;&lt;a href="https://github.com/HL7/bulk-data" rel="noopener noreferrer"&gt;bulk fhir export standard&lt;/a&gt; itself.&lt;/p&gt;

&lt;h4&gt;
&lt;br&gt;Nearline?&lt;/h4&gt;

&lt;p&gt;Yeah, only "Nearline" ingestion, as the HealthLake&amp;nbsp;export is the whole data store, and does not have a feature to be incremental. Additionally it does not support a resource based trigger, so it has to be invoked at an interval or via some other means yet to be apparent to me at the resource activity level.&amp;nbsp; Still a great number of ways to poke the export throughout AWS, and without incremental exports you only want it to be triggered inside a tolerable processing window anyway for the whole datastore.&lt;/p&gt;

&lt;h4&gt;The Whole Datastore?&lt;/h4&gt;

&lt;p&gt;Yes, the job exports &lt;em&gt;&lt;strong&gt;all&lt;/strong&gt;&lt;/em&gt; the resources into a flat structure.&amp;nbsp; Though it may not be the cleanest process to import the same data to catch the incremental data, the InterSystems OMOP transform should handle it.&lt;br&gt;&amp;nbsp;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Walkthrough&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trying to make this short and to the point, the illustration below really encapsulates what a that a scheduled lambda can glue these two solutions together and automate your OMOP ingestion.&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%2F3o4z4qgxviodog9m1kme.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%2F3o4z4qgxviodog9m1kme.png" alt=" " width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Step One, AWS: Create Bucket&lt;/h4&gt;

&lt;p&gt;Create a bucket with a few of keys, one is &lt;strong&gt;shared&lt;/strong&gt; with InterSystems OMOP for ingesting into the FHIR Transformation, the others will support the automated ingestion.&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%2F8fcztplk9vpsod2pc890.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%2F8fcztplk9vpsod2pc890.png" alt=" " width="800" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Explanations of the keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;export - landing area for the raw resource ndjson from the job&lt;/li&gt;
&lt;li&gt;from-healthlake-to-intersystems-omop - landing area for the create .zip and integtration point with InterSystems OMOP&lt;/li&gt;
&lt;li&gt;output - job output&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;Step Two, InterSystems OMOP&lt;/h4&gt;

&lt;p&gt;Create the Deployment providing the arn of the bucket and the keys from above, ie: `&lt;em&gt;from-healthlake-to-intersystems-omop&lt;/em&gt;` key.&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%2F3v97dosxpsxopatw8vev.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%2F3v97dosxpsxopatw8vev.png" alt=" " width="721" height="803"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Snag the example policy from the post configuration step as indicated and apply it to the bucket in AWS.&amp;nbsp; There are some exhaustive examples of this in a previous post &lt;a href="https://community.intersystems.com/post/omop-odyssey-intersystems-omop-cloud-service-troy" rel="noopener noreferrer"&gt;OMOP Odyssey -&amp;nbsp;InterSystems OMOP Cloud Service (Troy)&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;Step Three, Schedule a HealthLake&amp;nbsp;Export to Expected InterSystems OMOP format 💫&lt;/h4&gt;

&lt;p&gt;The explanation of the flow of things is in the code itself as well, but I will also put it in the explanation in the form of a prompt so maybe you can land in the same spot with your own changes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In python, show me how to start a HealthLake export job, export it to a target location, and poll the status of the job until it is complete, then read all of the ndjson files it creates and into a zip them without the relative path included in the zip and upload it to another location in the same bucket, once the upload is complete, remove the exported files from the export job.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The resulting function and code are the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe3ancidcpa7y0g6z6a4c.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%2Fe3ancidcpa7y0g6z6a4c.png" alt=" " width="800" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; json
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; boto3
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; uuid
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; boto3
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; zipfile
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; io
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; os
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; time


&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;lambda_handler&lt;/span&gt;&lt;span&gt;(event, context)&lt;/span&gt;:&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Botos&lt;/span&gt;
    s3 = boto3.client(&lt;span class="hljs-string"&gt;'s3'&lt;/span&gt;)
    client = boto3.client(&lt;span class="hljs-string"&gt;'healthlake'&lt;/span&gt;)

    &lt;span class="hljs-comment"&gt;# Vars&lt;/span&gt;
    small_guid = uuid.uuid4().hex[:&lt;span class="hljs-number"&gt;8&lt;/span&gt;]
    bucket_name = &lt;span class="hljs-string"&gt;'intersystems-omop-fhir-bucket'&lt;/span&gt;
    prefix = &lt;span class="hljs-string"&gt;'export/'&lt;/span&gt;  &lt;span class="hljs-comment"&gt;# Make sure it ends with '/'&lt;/span&gt;
    output_zip_key = &lt;span class="hljs-string"&gt;'from-healthlake-to-intersystems-omop/healthlake_ndjson_'&lt;/span&gt; + small_guid + &lt;span class="hljs-string"&gt;'.zip'&lt;/span&gt;
    datastore_id = &lt;span class="hljs-string"&gt;'9ee0e51d987e#ai#8ca487e8e95b1d'&lt;/span&gt;
    response = client.start_fhir_export_job(
        JobName=&lt;span class="hljs-string"&gt;'FHIR2OMOPJob'&lt;/span&gt;,
        OutputDataConfig={
            &lt;span class="hljs-string"&gt;'S3Configuration'&lt;/span&gt;: {
                &lt;span class="hljs-string"&gt;'S3Uri'&lt;/span&gt;: &lt;span class="hljs-string"&gt;'s3://intersystems-omop-fhir-bucket/export/'&lt;/span&gt;,
                &lt;span class="hljs-string"&gt;'KmsKeyId'&lt;/span&gt;: &lt;span class="hljs-string"&gt;'arn:aws:kms:us-east-2:12345:key/54918bec-#ai#-4710-9c18-1a65d0d4590b'&lt;/span&gt;
            }
        },
        DatastoreId=datastore_id,
        DataAccessRoleArn=&lt;span class="hljs-string"&gt;'arn:aws:iam::12345:role/service-role/AWSHealthLake-Export-2-OMOP'&lt;/span&gt;,
        ClientToken=small_guid
    )

    job_id = response[&lt;span class="hljs-string"&gt;'JobId'&lt;/span&gt;]
    print(&lt;span class="hljs-string"&gt;f"Export job started: &lt;span&gt;{job_id}&lt;/span&gt;"&lt;/span&gt;)

    &lt;span class="hljs-comment"&gt;# Step 2: Poll until the job completes&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;while&lt;/span&gt; &lt;span class="hljs-keyword"&gt;True&lt;/span&gt;:
        status_response = client.describe_fhir_export_job(
            DatastoreId=datastore_id,
            JobId=job_id
        )

        status = status_response[&lt;span class="hljs-string"&gt;'ExportJobProperties'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'JobStatus'&lt;/span&gt;]
        print(&lt;span class="hljs-string"&gt;f"Job status: &lt;span&gt;{status}&lt;/span&gt;"&lt;/span&gt;)

        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; status &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; [&lt;span class="hljs-string"&gt;'COMPLETED'&lt;/span&gt;, &lt;span class="hljs-string"&gt;'FAILED'&lt;/span&gt;, &lt;span class="hljs-string"&gt;'CANCELLED'&lt;/span&gt;]:
            &lt;span class="hljs-keyword"&gt;break&lt;/span&gt;
        time.sleep(&lt;span class="hljs-number"&gt;10&lt;/span&gt;)  &lt;span class="hljs-comment"&gt;# wait before polling again&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Step 3: Final result&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; status == &lt;span class="hljs-string"&gt;'COMPLETED'&lt;/span&gt;:
        output_uri = status_response[&lt;span class="hljs-string"&gt;'ExportJobProperties'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'OutputDataConfig'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'S3Configuration'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'S3Uri'&lt;/span&gt;]
        print(&lt;span class="hljs-string"&gt;f"Export completed. Data available at: &lt;span&gt;{output_uri}&lt;/span&gt;"&lt;/span&gt;)

    &lt;span class="hljs-comment"&gt;# Get list of all objects with .ndjson extension under the prefix&lt;/span&gt;
    ndjson_keys = []
    paginator = s3.get_paginator(&lt;span class="hljs-string"&gt;'list_objects_v2'&lt;/span&gt;)
    &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; page &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; paginator.paginate(Bucket=bucket_name, Prefix=prefix):
        &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; obj &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; page.get(&lt;span class="hljs-string"&gt;'Contents'&lt;/span&gt;, []):
            key = obj[&lt;span class="hljs-string"&gt;'Key'&lt;/span&gt;]
            &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; key.endswith(&lt;span class="hljs-string"&gt;'.ndjson'&lt;/span&gt;):
                ndjson_keys.append(key)

    &lt;span class="hljs-comment"&gt;# Create ZIP in memory&lt;/span&gt;
    zip_buffer = io.BytesIO()
    &lt;span class="hljs-keyword"&gt;with&lt;/span&gt; zipfile.ZipFile(zip_buffer, &lt;span class="hljs-string"&gt;'w'&lt;/span&gt;, zipfile.ZIP_DEFLATED) &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; zf:
        &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; key &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; ndjson_keys:
            obj = s3.get_object(Bucket=bucket_name, Key=key)
            file_data = obj[&lt;span class="hljs-string"&gt;'Body'&lt;/span&gt;].read()
            arcname = os.path.basename(key)
            zf.writestr(arcname, file_data)

    zip_buffer.seek(&lt;span class="hljs-number"&gt;0&lt;/span&gt;)

    &lt;span class="hljs-comment"&gt;# Upload ZIP back to S3&lt;/span&gt;
    s3.put_object(
        Bucket=bucket_name,
        Key=output_zip_key,
        Body=zip_buffer.getvalue()
    )
    print(&lt;span class="hljs-string"&gt;f"Created ZIP with &lt;span&gt;{len(ndjson_keys)}&lt;/span&gt; files at s3://&lt;span&gt;{bucket_name}&lt;/span&gt;/&lt;span&gt;{output_zip_key}&lt;/span&gt;"&lt;/span&gt;)
    &lt;span class="hljs-comment"&gt;# Clean up&lt;/span&gt;
    paginator = s3.get_paginator(&lt;span class="hljs-string"&gt;'list_objects_v2'&lt;/span&gt;)
    pages = paginator.paginate(Bucket=bucket_name, Prefix=prefix)

    &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; page &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; pages:
        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; &lt;span class="hljs-string"&gt;'Contents'&lt;/span&gt; &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; page:
            &lt;span class="hljs-comment"&gt;# Exclude the folder marker itself if it exists&lt;/span&gt;
            delete_keys = [
                {&lt;span class="hljs-string"&gt;'Key'&lt;/span&gt;: obj[&lt;span class="hljs-string"&gt;'Key'&lt;/span&gt;]}
                &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; obj &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; page[&lt;span class="hljs-string"&gt;'Contents'&lt;/span&gt;]
                &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; obj[&lt;span class="hljs-string"&gt;'Key'&lt;/span&gt;] != prefix  &lt;span class="hljs-comment"&gt;# protect the folder key (e.g., 'folder1/')&lt;/span&gt;
            ]

            &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; delete_keys:
                s3.delete_objects(Bucket=bucket_name, Delete={&lt;span class="hljs-string"&gt;'Objects'&lt;/span&gt;: delete_keys})
                print(&lt;span class="hljs-string"&gt;f"Deleted &lt;span&gt;{len(delete_keys)}&lt;/span&gt; objects under &lt;span&gt;{prefix}&lt;/span&gt;"&lt;/span&gt;)
        &lt;span class="hljs-keyword"&gt;else&lt;/span&gt;:
            print(&lt;span class="hljs-string"&gt;f"No objects found under &lt;span&gt;{prefix}&lt;/span&gt;"&lt;/span&gt;)
    &lt;span class="hljs-keyword"&gt;else&lt;/span&gt;:
        print(&lt;span class="hljs-string"&gt;f"Export job did not complete successfully. Status: &lt;span&gt;{status}&lt;/span&gt;"&lt;/span&gt;)
    
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; {
        &lt;span class="hljs-string"&gt;'statusCode'&lt;/span&gt;: &lt;span class="hljs-number"&gt;200&lt;/span&gt;,
        &lt;span class="hljs-string"&gt;'body'&lt;/span&gt;: json.dumps(response)
    }


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

&lt;p&gt;This function fires at an interval of about every 10&amp;nbsp;minutes via an EventBridge schedule, this will have to be adjusted to meet your workload characteristics.&lt;br&gt;&amp;nbsp;&lt;/p&gt;

&lt;h4&gt;Step Four, Validate Ingestion&amp;nbsp;✔&lt;/h4&gt;

&lt;p&gt;LGTM! we can see the zips in the ingestion location are successfully getting picked up by the transform in InterSystems OMOP.&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%2Fio90ypyj0wxkh9s1hw30.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%2Fio90ypyj0wxkh9s1hw30.png" alt=" " width="798" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Step Five, Smoke Data&amp;nbsp;✔&lt;/h4&gt;

&lt;p&gt;LGTM! FHIR Organization Resource = OMOPCDM54 care_site.&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%2Fitymg2jrloynn6tbf2yk.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%2Fitymg2jrloynn6tbf2yk.png" alt=" " width="798" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>OMOP Odyssey - GCP Healthcare API Real Time FHIR® to OMOP Transformation ( RealTymus )</title>
      <dc:creator>InterSystems Developer</dc:creator>
      <pubDate>Tue, 23 Jun 2026 17:23:50 +0000</pubDate>
      <link>https://dev.to/intersystems/omop-odyssey-gcp-healthcare-api-real-time-fhirr-to-omop-transformation-realtymus--2o9b</link>
      <guid>https://dev.to/intersystems/omop-odyssey-gcp-healthcare-api-real-time-fhirr-to-omop-transformation-realtymus--2o9b</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%2Flr5jz1o924tom5fndntr.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%2Flr5jz1o924tom5fndntr.png" alt=" " width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Real Time FHIR® to OMOP Transformation&lt;/h2&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%2F4ysgejz2i0b8fuo0nk4k.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%2F4ysgejz2i0b8fuo0nk4k.png" alt=" " width="800" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This part of the&amp;nbsp;&lt;a href="https://community.intersystems.com/smartsearch?search=OMOP+Odyssey" rel="noopener noreferrer"&gt;OMOP Journey&lt;/a&gt;,&amp;nbsp;&amp;nbsp;we reflect before attempting to challenge&amp;nbsp;&lt;em&gt;Scylla&amp;nbsp;&lt;/em&gt;on how fortunate we are that InterSystems OMOP transform is built on the Bulk FHIR Export&amp;nbsp;as the source payload.&amp;nbsp; This opens up&amp;nbsp;&lt;strong&gt;hands off interoperability&lt;/strong&gt;&amp;nbsp;with the InterSystems OMOP transform across several FHIR® vendors, this time with the &lt;a href="https://cloud.google.com/healthcare-api?hl=en" rel="noopener noreferrer"&gt;Google Cloud Healthcare API.&lt;/a&gt;&lt;/p&gt;

&lt;h3 id="health-lake-bulk-f-h-i-r-export"&gt;Google Cloud Healthcare API FHIR® Export&lt;/h3&gt;

&lt;p&gt;GCP FHIR® Datastores support&amp;nbsp;bulk fhir import/export from the cli or api, the premise is&amp;nbsp;simple and the docs are over exhaustive, we'll save a model the trouble of training on it again and&amp;nbsp;&lt;a href="https://cloud.google.com/healthcare-api/docs/how-tos/fhir-import-export" rel="noopener noreferrer"&gt;link&lt;/a&gt;&amp;nbsp;it if interested.&amp;nbsp; The more valuable thing to understand of the heading of this paragraph is the implementation of the&amp;nbsp;&lt;a href="https://github.com/HL7/bulk-data" rel="noopener noreferrer"&gt;bulk fhir export standard&lt;/a&gt;&amp;nbsp;itself.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Important differentiators with Google's implementation of the FHIR®&amp;nbsp;Export are namely, Resource Change Notification via Pub/Sub and the ability to specify &lt;a href="https://cloud.google.com/healthcare-api/docs/how-tos/fhir-import-export#incremental-exports" rel="noopener noreferrer"&gt;incremental exports&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id="nearline"&gt;Real Time?&amp;nbsp;⏲&lt;/h3&gt;

&lt;p&gt;Yes! Ill die on this sword I guess.&amp;nbsp; Its not only my rap handle, but the mechanics are definitely there to back a good technical argument to be able to say...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"As a new Organization gets created to FHIR, we transform it, and add it to the InterSystems OMOP CDM in the same stroke as a care_site/location."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;&lt;strong&gt;Walkthrough&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Trying to make this short and to the point and encapsulates how a pub/sub notification coupled with a cloud&amp;nbsp;function can glue these two solutions together and automate your OMOP ingestion at a granular level.&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%2F00h4trni0vru5zg4ng7i.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%2F00h4trni0vru5zg4ng7i.png" alt=" " width="798" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3 id="step-one-a-w-s-create-bucket"&gt;Step One:&amp;nbsp;Wire Up InterSystems OMOP to AWS Bucket&lt;/h3&gt;

&lt;p&gt;This step is becoming a repetitive in posts in this community, so I will go warp speed through the steps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Procure AWS S3 Bucket&lt;/li&gt;
&lt;li&gt;Launch InterSystems OMOP, Add Bucket Configuration&lt;/li&gt;
&lt;li&gt;Eject Policy from InterSystems OMOP Deployment&lt;/li&gt;
&lt;li&gt;Apply Policy to the AWS S3 Bucket&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%2F3sdkcjbh6z6eqrat2x10.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%2F3sdkcjbh6z6eqrat2x10.png" alt=" " width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;&amp;nbsp;&lt;/h4&gt;

&lt;p&gt;I dunno, the steps and image seemed to work out better in my head, but maybe not.&amp;nbsp; Here are the &lt;a href="https://docs.intersystems.com/services/csp/docbook/DocBook.UI.Page.cls?KEY=PAGE_rdp#PAGE_rdp_create" rel="noopener noreferrer"&gt;docs&lt;/a&gt; and here is a more &lt;a href="https://community.intersystems.com/post/omop-odyssey-intersystems-omop-cloud-service-troy" rel="noopener noreferrer"&gt;in depth way&lt;/a&gt; to get this taken care of in this series with better examples.&lt;/p&gt;

&lt;h3 id="step-one-a-w-s-create-bucket"&gt;Step Two:&amp;nbsp;Add Pub/Sub Target in Google Cloud Healthcare API&lt;/h3&gt;

&lt;p&gt;As mentioned previous, a&amp;nbsp;foundational piece to making this work is the super great feature that notifies on Resource changes in the data store.&amp;nbsp; You will find this option on setup in the dialog and is also available post configuration.&amp;nbsp; I typically like to check both options to have as much data in the notification as possible to play with.&amp;nbsp; For instance with Deletes, you can include the deleted resource in the notification as well, really great for EMPI solutions.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fccaq8ipbkno4neibkqqo.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%2Fccaq8ipbkno4neibkqqo.png" alt=" " width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3 id="step-one-a-w-s-create-bucket"&gt;Step Three:&amp;nbsp;Cloud Function&amp;nbsp;⭐&lt;/h3&gt;

&lt;p&gt;The cloud function puts in the work, and the SOW for that&amp;nbsp;looks a little bit like this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Listen for FHIR resource change pub/sub notifications&amp;nbsp;of type Organization on the create method, and export the data store incrementally from the time the event fired.&amp;nbsp; Since the export function only supports a GCS target, read in the created export and create fhir export zip file that zips the&amp;nbsp;ndjson files into the root of the zip file and push the created zip file to an aws bucket.&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Re-stating the second&amp;nbsp;feature that makes this especially great, is the ability to export from an specific date and time, meaning we do not need to export the entire dataset.&amp;nbsp; For this we will use the time we received the event, tack a minute or so on it, in hopes the export, import and transform steps will be smaller and of course, more timely.&lt;/p&gt;

&lt;p&gt;&amp;nbsp;&lt;br&gt;
realtimefhir2omop.py&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; os, io, json, base64, time, zipfile, datetime
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; requests, boto3
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; google.cloud &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; storage
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; google.auth.transport.requests &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Request
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; google.auth
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; google.auth.transport.requests &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; AuthorizedSession
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; base64
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; functions_framework
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; pathlib
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; textwrap
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; json
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; datetime &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; datetime, timedelta, timezone



&lt;span class="hljs-comment"&gt;# Config&lt;/span&gt;
PROJECT_ID = &lt;span class="hljs-string"&gt;"pidtoo-fhir"&lt;/span&gt;
LOCATION = &lt;span class="hljs-string"&gt;"us-east4"&lt;/span&gt;
DATASET_ID = &lt;span class="hljs-string"&gt;"isc"&lt;/span&gt;
FHIR_STORE_ID = &lt;span class="hljs-string"&gt;"fhir-omop"&lt;/span&gt;
GCS_EXPORT_BUCKET = &lt;span class="hljs-string"&gt;"fhir-export-bucket"&lt;/span&gt;
AWS_BUCKET = &lt;span class="hljs-string"&gt;"intersystems-fhir2omop"&lt;/span&gt;
AWS_REGION = &lt;span class="hljs-string"&gt;"us-east-2"&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Trigger FHIR export&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;trigger_incremental_export&lt;/span&gt;&lt;span&gt;(export_time_iso)&lt;/span&gt;:&lt;/span&gt;
    client = storage.Client()
    bucket = client.bucket(&lt;span class="hljs-string"&gt;"fhir-export-bucket"&lt;/span&gt;)

    blobs = bucket.list_blobs()
    &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; blob &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; blobs:
        print(&lt;span class="hljs-string"&gt;f"Deleting: &lt;span&gt;{blob.name}&lt;/span&gt;"&lt;/span&gt;)
        blob.delete()
    
    credentials, _ = google.auth.default(scopes=[&lt;span class="hljs-string"&gt;"https://www.googleapis.com/auth/cloud-platform"&lt;/span&gt;])
    authed_session = AuthorizedSession(credentials)

    export_uri = &lt;span class="hljs-string"&gt;f"gs://&lt;span&gt;{GCS_EXPORT_BUCKET}&lt;/span&gt;/fhir-export-&lt;span&gt;{int(time.time())}&lt;/span&gt;/"&lt;/span&gt;
    export_uri = &lt;span class="hljs-string"&gt;f"gs://&lt;span&gt;{GCS_EXPORT_BUCKET}&lt;/span&gt;/"&lt;/span&gt;
    url = (
        &lt;span class="hljs-string"&gt;f"https://healthcare.googleapis.com/v1/projects/&lt;span&gt;{PROJECT_ID}&lt;/span&gt;/locations/&lt;span&gt;{LOCATION}&lt;/span&gt;/"&lt;/span&gt;
        &lt;span class="hljs-string"&gt;f"datasets/&lt;span&gt;{DATASET_ID}&lt;/span&gt;/fhirStores/&lt;span&gt;{FHIR_STORE_ID}&lt;/span&gt;:export"&lt;/span&gt;
    )

    body = {
        &lt;span class="hljs-string"&gt;"gcsDestination"&lt;/span&gt;: {&lt;span class="hljs-string"&gt;"uriPrefix"&lt;/span&gt;: export_uri},
        &lt;span class="hljs-string"&gt;"since"&lt;/span&gt;: export_time_iso
    }

    response = authed_session.post(url, json=body)
    print(&lt;span class="hljs-string"&gt;f"Export response: &lt;span&gt;{response.status_code}&lt;/span&gt; - &lt;span&gt;{response.text}&lt;/span&gt;"&lt;/span&gt;)
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; export_uri &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; response.ok &lt;span class="hljs-keyword"&gt;else&lt;/span&gt; &lt;span class="hljs-keyword"&gt;None&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Poll GCS for export results&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;wait_for_ndjson_files&lt;/span&gt;&lt;span&gt;(export_uri_prefix)&lt;/span&gt;:&lt;/span&gt;
    client = storage.Client()
    bucket_name = export_uri_prefix.split(&lt;span class="hljs-string"&gt;"/"&lt;/span&gt;)[&lt;span class="hljs-number"&gt;2&lt;/span&gt;]
    prefix = &lt;span class="hljs-string"&gt;"/"&lt;/span&gt;.join(export_uri_prefix.split(&lt;span class="hljs-string"&gt;"/"&lt;/span&gt;)[&lt;span class="hljs-number"&gt;3&lt;/span&gt;:])
    print(bucket_name)
    print(prefix)

    bucket = client.bucket(bucket_name)
    &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; _ &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; range(&lt;span class="hljs-number"&gt;20&lt;/span&gt;):  &lt;span class="hljs-comment"&gt;# Wait up to ~5 mins&lt;/span&gt;
        blobs = list(bucket.list_blobs(prefix=prefix))
        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; any(blob.name.endswith(&lt;span class="hljs-string"&gt;"Organization"&lt;/span&gt;) &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; blob &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; blobs):
            &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; [blob &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; blob &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; blobs &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; blob.name.endswith(&lt;span class="hljs-string"&gt;"Organization"&lt;/span&gt;)]
        time.sleep(&lt;span class="hljs-number"&gt;5&lt;/span&gt;)
    &lt;span class="hljs-keyword"&gt;raise&lt;/span&gt; TimeoutError(&lt;span class="hljs-string"&gt;"Export files did not appear in GCS within timeout window"&lt;/span&gt;)

&lt;span class="hljs-comment"&gt;# Zip .ndjsons into flat ZIP file&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;create_zip_from_blobs&lt;/span&gt;&lt;span&gt;(blobs, zip_path)&lt;/span&gt;:&lt;/span&gt;
    client = storage.Client()
    &lt;span class="hljs-keyword"&gt;with&lt;/span&gt; zipfile.ZipFile(zip_path, &lt;span class="hljs-string"&gt;'w'&lt;/span&gt;, zipfile.ZIP_DEFLATED) &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; zipf:
        &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; blob &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; blobs:
            data = blob.download_as_bytes()
            fname = os.path.basename(blob.name)
            zipf.writestr(fname + &lt;span class="hljs-string"&gt;".ndjson"&lt;/span&gt;, data)

&lt;span class="hljs-comment"&gt;# Upload ZIP to AWS S3&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;upload_to_s3&lt;/span&gt;&lt;span&gt;(zip_path, s3_key)&lt;/span&gt;:&lt;/span&gt;
    s3 = boto3.client(&lt;span class="hljs-string"&gt;'s3'&lt;/span&gt;, region_name=AWS_REGION)
    s3.upload_file(zip_path, AWS_BUCKET, &lt;span class="hljs-string"&gt;"from_gcp_to_omop"&lt;/span&gt; + s3_key)
    print(&lt;span class="hljs-string"&gt;f"Uploaded &lt;span&gt;{zip_path}&lt;/span&gt; to s3://&lt;span&gt;{AWS_BUCKET}&lt;/span&gt;/from_gcp_to_omop/&lt;span&gt;{s3_key}&lt;/span&gt;"&lt;/span&gt;)


&lt;span class="hljs-comment"&gt;#@functions_framework.cloud_event&lt;/span&gt;
&lt;span class="hljs-comment"&gt;#def mit_grandhack(cloud_event):&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Print out the data from Pub/Sub, to prove that it worked&lt;/span&gt;
&lt;span class="hljs-comment"&gt;#    print(base64.b64decode(cloud_event.data["message"]["data"]))&lt;/span&gt;
&lt;span class="hljs-comment"&gt;#    question = base64.b64decode(cloud_event.data["message"]["data"]).decode()&lt;/span&gt;
&lt;span class="hljs-meta"&gt;@functions_framework.cloud_event&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span&gt;def&lt;/span&gt; &lt;span&gt;receive_pubsub&lt;/span&gt;&lt;span&gt;(cloud_event)&lt;/span&gt;:&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;#envelope = request.get_json()&lt;/span&gt;
    print(cloud_event)
    data = base64.b64decode(cloud_event.data[&lt;span class="hljs-string"&gt;"message"&lt;/span&gt;][&lt;span class="hljs-string"&gt;"data"&lt;/span&gt;]).decode()
    data = cloud_event.data
    print(data)
    print(type(data))
    &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; &lt;span class="hljs-keyword"&gt;not&lt;/span&gt; data:
        &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;"No data"&lt;/span&gt;, &lt;span class="hljs-number"&gt;400&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;#payload = data # json.loads(data)&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;#method = payload.get("protoPayload", {}).get("methodName", "")&lt;/span&gt;
    method = data[&lt;span class="hljs-string"&gt;'message'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'attributes'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'action'&lt;/span&gt;]
    &lt;span class="hljs-comment"&gt;#resource_name = payload.get("protoPayload", {}).get("resourceName", "")&lt;/span&gt;
    resource_name = data[&lt;span class="hljs-string"&gt;'message'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'attributes'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'resourceType'&lt;/span&gt;]
    &lt;span class="hljs-comment"&gt;#timestamp = payload.get("timestamp", "")&lt;/span&gt;
    timestamp = data[&lt;span class="hljs-string"&gt;'message'&lt;/span&gt;][&lt;span class="hljs-string"&gt;'publishTime'&lt;/span&gt;]
    &lt;span class="hljs-comment"&gt;# Input datetime string&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Parse the string to a datetime object&lt;/span&gt;
    dt = datetime.strptime(timestamp, &lt;span class="hljs-string"&gt;"%Y-%m-%dT%H:%M:%S.%fZ"&lt;/span&gt;).replace(tzinfo=timezone.utc)

    &lt;span class="hljs-comment"&gt;# Subtract 5 minutes&lt;/span&gt;
    five_minutes_ago = dt - timedelta(minutes=&lt;span class="hljs-number"&gt;5&lt;/span&gt;)

    &lt;span class="hljs-comment"&gt;# Convert back to ISO 8601 string format with 'Z'&lt;/span&gt;
    timestamp = five_minutes_ago.isoformat().replace(&lt;span class="hljs-string"&gt;'+00:00'&lt;/span&gt;, &lt;span class="hljs-string"&gt;'Z'&lt;/span&gt;)

    print(method)
    print(resource_name)
    print(timestamp)

    &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; &lt;span class="hljs-string"&gt;"CreateResource"&lt;/span&gt; &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; method &lt;span class="hljs-keyword"&gt;and&lt;/span&gt; &lt;span class="hljs-string"&gt;"Organization"&lt;/span&gt; &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; resource_name:
        print(&lt;span class="hljs-string"&gt;f"New Organization detected at &lt;span&gt;{timestamp}&lt;/span&gt;"&lt;/span&gt;)
        export_uri = trigger_incremental_export(timestamp)
        &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; &lt;span class="hljs-keyword"&gt;not&lt;/span&gt; export_uri:
            &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;"Export failed"&lt;/span&gt;, &lt;span class="hljs-number"&gt;500&lt;/span&gt;
        blobs = wait_for_ndjson_files(export_uri)
        zip_file_path = &lt;span class="hljs-string"&gt;"/tmp/fhir_export.zip"&lt;/span&gt;
        create_zip_from_blobs(blobs, zip_file_path)
        s3_key = &lt;span class="hljs-string"&gt;f"/export-&lt;span&gt;{int(time.time())}&lt;/span&gt;.zip"&lt;/span&gt;
        upload_to_s3(zip_file_path, s3_key)
        &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;"Exported and uploaded"&lt;/span&gt;, &lt;span class="hljs-number"&gt;200&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; &lt;span class="hljs-string"&gt;"No relevant event"&lt;/span&gt;, &lt;span class="hljs-number"&gt;204&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

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




&lt;h3 id="step-one-a-w-s-create-bucket"&gt;Step Four: What is Happening right now?&amp;nbsp;🔥&lt;/h3&gt;

&lt;p&gt;To split what is going on, lets inspect the real time processing with some screenshots at each point.&lt;/p&gt;

&lt;h4&gt;FHIR Organization Created&lt;/h4&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%2Feqvuyrl6t8qxjc7odvea.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%2Feqvuyrl6t8qxjc7odvea.png" alt=" " width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Pub/Sub Event is Published&lt;/h4&gt;

&lt;p&gt;&amp;nbsp;&lt;br&gt;
Pub/Sub FHIR Event&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{'attributes': {'specversion': '&lt;span class="hljs-number"&gt;1.0&lt;/span&gt;', 'id': '&lt;span class="hljs-number"&gt;13999883936448345&lt;/span&gt;', 'source': '&lt;span class="hljs-comment"&gt;//pubsub.googleapis.com/projects/pidtoo-fhir/topics/fhir-omop-topic', 'type': 'google.cloud.pubsub.topic.v1.messagePublished', 'datacontenttype': 'application/json', 'time': '2025-05-13T20:13:20.339Z'}, 'data': {'message': {'attributes': {'action': 'CreateResource', 'lastUpdatedTime': 'Tue, 13 May 2025 20:13:20 UTC', 'payloadType': 'FullResource', 'resourceType': 'Organization', 'storeName': 'projects/pidtoo-fhir/locations/us-east4/datasets/isc/fhirStores/fhir-omop', 'versionId': 'MTc0NzE2NzIwMDEwNzczODAwMA'}, 'data': 'ewogICJhZGRyZXNzIjogWwogICAgewogICAgICAiY2l0eSI6IC', 'messageId': '13999883936448345', 'message_id': '13999883936448345', 'publishTime': '2025-05-13T20:13:20.339Z', 'publish_time': '2025-05-13T20:13:20.339Z'}, 'subscription': 'projects/pidtoo-fhir/subscriptions/eventarc-us-east4-fhir2omop-trigger-sub-855'}}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

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




&lt;h4&gt;Cloud Function Receives Resource Event from Subscription&lt;/h4&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%2F3ad7n7bi6o4x01rfzxar.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%2F3ad7n7bi6o4x01rfzxar.png" alt=" " width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Cloud Function Exports the FHIR Store GCS&lt;/h4&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%2Fg9jm6ksz6231azmvg5nv.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%2Fg9jm6ksz6231azmvg5nv.png" alt=" " width="800" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Cloud Function Creates ZIP from GCS and Pushes to AWS&lt;/h4&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%2Fz5sgxh022srca9xg6yoo.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%2Fz5sgxh022srca9xg6yoo.png" alt=" " width="798" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;InterSystems OMOP Transforms FHIR to OMOP&lt;/h4&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%2F5oz0mn2b26tis956uh9u.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%2F5oz0mn2b26tis956uh9u.png" alt=" " width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;Organization Available as Care Site in CDM&lt;/h4&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%2Fh1rdwinbzaqnt9xoe01t.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%2Fh1rdwinbzaqnt9xoe01t.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When did that FHIR Resource get transformed to the CDM ?&lt;br&gt;&lt;br&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fec7w6peuymijyt4c5i.gif" 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%2F1fec7w6peuymijyt4c5i.gif" alt=" " width="400" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;Step Four: Validation Fun&amp;nbsp;✔&lt;/h3&gt;

&lt;p&gt;Fun with OBS and Not so Much fun with Audio&lt;br&gt;&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/-nbYXKQvdQc"&gt;
  &lt;/iframe&gt;
&lt;br&gt;&amp;nbsp;&lt;/p&gt;

&lt;h3&gt;In Conclusion&lt;br&gt;&amp;nbsp;&lt;/h3&gt;

&lt;p&gt;Did something similar last year at MIT Grand Hack, using the same design pattern, but with Questionairre/Response resource and Gemini in the middle of things.&lt;br&gt;&lt;br&gt;&lt;a href="https://www.linkedin.com/pulse/gemini-fhir-agent-ron-sweeney-bk7lc/" rel="noopener noreferrer"&gt;Gemini FHIR Agent MIT Grand Hack&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>analytics</category>
      <category>beginners</category>
      <category>firebase</category>
    </item>
  </channel>
</rss>
