Written by

BPLus Tecnologia
Article André Dienes Friedrich · Jun 3 13m read

CardioIris and AI-assisted development on InterSystems IRIS

An engineering walkthrough of the IRIS-CardioFlow project architecture with real code for its AI, FHIR, and connectivity layers and the role of iris-agentic-dev in a modern ObjectScript workflow.


Introduction

Monitoring cardiovascular surgical flow in real time is a classic healthcare integration problem: data arrives from heterogeneous sources, must be persisted with clinical semantics, exposed through an API, and presented in a way the care team can act on. The CardioIris repository (internally named IRIS-CardioFlow) is a lean demonstration of that scenario, built on InterSystems IRIS 2026.

What makes it interesting from an engineering standpoint is not the demo itself but its anatomy: a small yet complete project that exercises nearly every layer of a typical IRIS application a persistence model, business logic in ObjectScript, a REST API, an embedded FHIR resource builder, an AI agent wired in through Embedded Python, containerization, and a presentation layer. Precisely because it has that shape, it is the kind of project that benefits most from agentic development tooling such as iris-agentic-dev, which connects AI assistants directly to a live IRIS instance.

This article describes how CardioIris works, shows real code for its AI, FHIR, and connection layers, and explains why a tool like iris-agentic-dev matters when building and maintaining projects with this structure.


Part 1 — How CardioIris works

Overview

CardioIris presents itself as a cardiovascular surgical-flow monitoring demo. The repository was deliberately trimmed to contain only what is needed to run the product:

  • a Docker runtime for IRIS;
  • the IRIS classes that power the demo;
  • the simulated-data seeding logic;
  • a browser dashboard;
  • a local proxy that removes direct browser authentication against IRIS;
  • startup scripts.

The language split mirrors that anatomy: roughly 44% HTML (the dashboard), 32% ObjectScript (the IRIS core), 12% Python (the proxy and the AI agent), 10% PowerShell (the bootstrap), and a small fraction of Dockerfile.

Runtime architecture

The execution flow is organized into four cleanly separated responsibilities. Docker runs the IRIS container; PowerShell bootstrap scripts compile the IRIS classes, publish the REST API, create the technical API user, and seed the simulation dataset; a local Python gateway serves the product UI and proxies API requests to IRIS; and the browser talks only to the local product URL.

That design carries one notable architectural decision: the browser never talks to IRIS directly. All traffic goes through the Python proxy listening on http://127.0.0.1:8787. IRIS remains exposed at http://localhost:52773/api/cardio/..., but the front-end only ever sees http://127.0.0.1:8787/api/cardio/....

Layer 1 — Connections

The connectivity story has two halves: how the browser reaches IRIS safely, and how IRIS itself exposes the API.

On the front side, bin/cardioflow_proxy.py is a small threaded HTTP server. Its entire reason for existing is to keep IRIS credentials out of the browser: it injects HTTP Basic authentication server-side and forwards everything under /api/cardio/ to IRIS.

IRIS_BASE = os.getenv("CARDIOFLOW_IRIS_BASE", "http://localhost:52773")
IRIS_USER = os.getenv("CARDIOFLOW_IRIS_USER", "cardioapi")
IRIS_PASSWORD = os.getenv("CARDIOFLOW_IRIS_PASSWORD", "Cardio123!")

def build_auth_header() -> str:
    token = base64.b64encode(f"{IRIS_USER}:{IRIS_PASSWORD}".encode("utf-8")).decode("ascii")
    return f"Basic {token}"

def proxy_to_iris(self, head_only: bool = False):
    target = f"{IRIS_BASE}{self.path}"
    request = Request(target, method="GET")
    request.add_header("Authorization", build_auth_header())
    with urlopen(request, timeout=30) as response:
        body = response.read()
        # ... relay status, filtered headers and body back to the browser

A detail worth highlighting for anyone working behind a corporate network: the proxy relies on Python's standard-library urllib rather than a third-party HTTP client, which keeps the dependency surface minimal and tends to behave better through restrictive proxies and antivirus inspection.

On the IRIS side, the API is a %CSP.REST dispatcher. The UrlMap is the single source of truth for the routing surface, and it already includes a health probe and an AI analysis route alongside the dashboard endpoints:

Class CardioFlow.API.DashboardDispatch Extends %CSP.REST
{
Parameter CONTENTTYPE = "application/json";
Parameter HandleCorsRequest = 1;

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<Routes>
  <Route Url="/dashboards/overview" Method="GET" Call="GetOverview" Cors="true" />
  <Route Url="/dashboards/summary"  Method="GET" Call="GetSummary"  Cors="true" />
  <Route Url="/patient/:id/status"  Method="GET" Call="GetPatient"  Cors="true" />
  <Route Url="/health"              Method="GET" Call="Health"      Cors="true" />
  <Route Url="/ai/analyze/:patientId" Method="GET" Call="AnalyzePatient" Cors="true" />
</Routes>
}
}

The data connection inside IRIS is plain InterSystems SQL over the persistence table, using %SQL.Statement with bind variables, the right pattern for a parameterized lookup like a single patient:

ClassMethod GetPatient(id As %String) As %Status
{
    set stmt = ##class(%SQL.Statement).%New()
    set status = stmt.%Prepare("SELECT TOP 1 PatientId, PatientName, Status, EventTime, HospitalCode, ScenarioTag "
        _ "FROM CardioFlow_Analytics.SurgeryStatus WHERE PatientId = ? ORDER BY EventTime DESC")
    set rs = stmt.%Execute(id)
    if rs.%Next() {
        set obj = ##class(%DynamicObject).%New()
        do obj.%Set("patientId", rs.%Get("PatientId"))
        do obj.%Set("patientName", rs.%Get("PatientName"))
        do obj.%Set("hospitalCode", rs.%Get("HospitalCode"))
        do obj.%Set("scenarioTag", rs.%Get("ScenarioTag"))
        write obj.%ToJSON()
    } else {
        do ..ReportHttpStatusCode(404, "Patient not found")
    }
    quit $$$OK
}

Layer 2 — FHIR

This is where the project shows real healthcare ambition rather than a toy dashboard. When the AI route is invoked, the dispatcher pulls a record from SQL and hands it to a dedicated CardioFlow.FHIR.ResourceBuilder, which turns the flat surgery-status row into three standards-based FHIR resources, Patient, Procedure, and Observation — and assembles them into a FHIR Bundle:

Set record = ##class(CardioFlow.Analytics.SurgeryStatus).%New()
Set record.PatientId   = rs.%Get("PatientId")
Set record.PatientName = rs.%Get("PatientName")
Set record.Status      = rs.%Get("Status")
Set record.HospitalCode = rs.%Get("HospitalCode")
Set record.ScenarioTag = rs.%Get("ScenarioTag")

// Flat row -> standards-based FHIR resources
Set patient   = ##class(CardioFlow.FHIR.ResourceBuilder).BuildPatient(record)
Set procedure = ##class(CardioFlow.FHIR.ResourceBuilder).BuildProcedure(record)
Set obs       = ##class(CardioFlow.FHIR.ResourceBuilder).BuildObservation(record)

// Assemble a FHIR Bundle (type = collection)
Set entries = ##class(%DynamicArray).%New()
Do entries.%Push({"resource": (patient)})
Do entries.%Push({"resource": (procedure)})
Do entries.%Push({"resource": (obs)})

Set bundle = ##class(%DynamicObject).%New()
Do bundle.%Set("resourceType", "Bundle")
Do bundle.%Set("type", "collection")
Do bundle.%Set("entry", entries)

Two things are worth noting. First, the patient identifiers in the seed data follow a FHIR-style convention (for example FHIR-TEST-001), which is consistent with treating these records as FHIR resources downstream. Second, building a Bundle of type: collection is exactly the FHIR-idiomatic way to package several related resources for a single subject here, so the AI agent receives a complete, standards-shaped clinical context rather than a proprietary JSON blob.

Layer 3 — AI

The FHIR bundle exists to feed an AI agent. The /ai/analyze/:patientId route resolves the patient, builds the bundle (above), reads an Anthropic API key from a global or the environment, and delegates the actual model call to Embedded Python:

Set apiKey = $Get(^CardioFlow.Config("ANTHROPIC_API_KEY"), "")
If apiKey = "" { Set apiKey = ##class(%SYSTEM.Util).GetEnviron("ANTHROPIC_API_KEY") }

Set resultJson = ..CallAIAgent(bundle.%ToJSON(), apiKey)
Write resultJson

The bridge into Python is a single method with [ Language = python ], which is the heart of the IRIS Embedded Python story, ObjectScript and Python sharing the same process and the same data, with no network hop between them:

ClassMethod CallAIAgent(bundleJson As %String, apiKey As %String) As %String [ Language = python ]
{
    import sys, os, json
    sys.path.insert(0, '/home/irisowner/src/CardioFlow/AI')
    try:
        from Agent import analyze_patient_bundle
        bundle = json.loads(bundleJson)
        result = analyze_patient_bundle(bundle, apiKey)
        return json.dumps(result)
    except Exception as e:
        return json.dumps({
            "error": str(e),
            "riskLevel": "UNKNOWN",
            "recommendation": "AI agent unavailable",
            "reasoning": str(e),
            "suggestedNextStatus": "",
            "escalate": False
        })
}

The shape of that fallback object tells you what the agent is expected to return: a structured clinical assessment with a riskLevel, a recommendation, a reasoning trace, a suggestedNextStatus, and an escalate flag. In other words, the AI is not generating free text for a human to read, it is producing a structured decision payload the application can act on, with a graceful degradation path when the model is unavailable. That is a sound pattern: the system stays functional even when the AI layer is offline.

Putting it together

End to end, a single AI analysis request travels through every layer described above:

Browser ──/api/cardio/ai/analyze/{id}──▶ Python proxy (Basic auth)
        ──▶ IRIS %CSP.REST dispatcher
        ──▶ SQL lookup (%SQL.Statement) on SurgeryStatus
        ──▶ FHIR ResourceBuilder → Patient + Procedure + Observation → Bundle
        ──▶ Embedded Python (Language = python) → Anthropic model
        ──▶ structured {riskLevel, recommendation, escalate, ...} JSON
        ──▶ back through the proxy to the dashboard

That is a remarkably complete slice of a real IRIS for Health application compressed into a demo: connectivity with credential isolation, a relational persistence model, a clean REST surface, FHIR-standard resource shaping, and an AI agent reached through Embedded Python.

Data model and scenarios

The demo is seeded with three scenario packs, Emergency escalation, Delayed surgery, and Recovery escalation, each contributing one case at every stage of the surgical flow (one awaiting, one in surgery, one post-op). That produces predictable, easy-to-validate totals: AWAITING = 3, IN_SURGERY = 3, and POST_OP = 3, across nine records spread over three hospitals (BOSTON-HEART, CHICAGO-CARDIAC, SEATTLE-MED). The aggregation endpoint reflects this directly:

ClassMethod GetSummary() As %Status
{
    set rs = ##class(%SQL.Statement).%ExecDirect(,
        "SELECT Status, COUNT(*) AS Total FROM CardioFlow_Analytics.SurgeryStatus GROUP BY Status")
    set obj = ##class(%DynamicObject).%New()
    set summary = ##class(%DynamicArray).%New()
    do obj.%Set("summary", summary)
    while rs.%Next() {
        set row = ##class(%DynamicObject).%New()
        do row.%Set("status", rs.%Get("Status"))
        do row.%Set("total", rs.%Get("Total"))
        do summary.%Push(row)
    }
    write obj.%ToJSON()
    quit 1
}

Summary of attributes

CardioIris is small but architecturally representative. It demonstrates an IRIS persistence model, ObjectScript business logic, a clean REST API, a FHIR resource builder, an AI agent via Embedded Python, Docker containerization, deterministic data seeding, and a sensible security boundary between the browser and the database. It is precisely this profile dense in ObjectScript, dependent on compilation and a live IRIS instance, mixing ObjectScript with Embedded Python, that makes it worth discussing how AI-assisted development tooling fits into the loop.


Part 2 — iris-agentic-dev and why it matters

What it is

iris-agentic-dev is an InterSystems-community tool that connects GitHub Copilot, Claude Code, or any MCP-compatible AI assistant directly to a live IRIS instance. The pitch, in one line: the AI can compile, test, search, read, write, and debug ObjectScript without leaving the chat.

Technically, it runs as a local MCP (Model Context Protocol) server, shipped as a single binary written predominantly in Rust (about 90% of the codebase). The AI assistant calls the tools the server exposes, iris_compile, iris_doc, iris_execute, and others and iris-agentic-dev executes them against the real IRIS instance over the Atelier REST API. The AI sees compile errors, class definitions, and execution output inline, the same way it would with a local filesystem.

Wiring it into Claude Code is a small block of configuration:

{
  "mcpServers": {
    "iris-agentic-dev": {
      "command": "iris-agentic-dev",
      "args": ["mcp"],
      "env": {
        "IRIS_HOST": "localhost",
        "IRIS_WEB_PORT": "52773",
        "IRIS_USERNAME": "_SYSTEM",
        "IRIS_PASSWORD": "SYS",
        "IRIS_NAMESPACE": "MYAPP"
      }
    }
  }
}

It resolves the connection by a clear precedence order CLI flags, then a .iris-agentic-dev.toml in the project, then environment variables, then VS Code server definitions, then Docker containers, and finally a localhost port scan. Most of its 23 tools work purely over Atelier REST against any IRIS instance, local or remote; a subset (production control, container management, interoperability queue inspection) requires Docker.

Why this matters for a project like CardioIris

The relevance becomes obvious when you overlay the tool set onto the CardioIris anatomy. Almost everything a developer does by hand in the CardioIris loop has a direct counterpart in the tool:

Compiling and iterating the core classes. CardioIris lives on compiling SurgeryStatus.cls, Runner.cls, DashboardDispatch.cls, and the FHIR.ResourceBuilder. With iris_compile, the assistant compiles those classes and gets errors with line numbers without the developer switching windows. The "edit → compile → read error → fix" cycle collapses into one continuous conversation.

Validating data and the API. CardioIris's acceptance criteria are essentially queries: count records by status, confirm nine records across three hospitals, verify a patient's fields. With iris_query, the AI runs exactly the SQL that validates AWAITING = 3, IN_SURGERY = 3, POST_OP = 3 directly against the table, and with iris_execute it can exercise the logic in Runner.cls before the proxy is even started.

Debugging the FHIR and AI bridge. The most error-prone parts of this project are the FHIR mapping and the ObjectScript-to-Python boundary, where exceptions surface as opaque INT-level errors. iris_debug maps those back to the source line in ObjectScript, and iris_execute lets the assistant invoke CallAIAgent with a sample bundle to confirm the Embedded Python path resolves and returns the expected structured payload.

Automated tests. iris_generate_test scaffolds %UnitTest classes for SurgeryStatus, and iris_test runs the suite and returns structured pass/fail — a natural path to turn the README's manual validation into repeatable tests.

Container and production orchestration. Because CardioIris is Docker-centric, iris_containers lets the AI select and start the right container, and should the project grow into Interoperability, common in FHIR-based healthcare integrations, iris_production and iris_interop_query give visibility into the production and its message queues without leaving the flow.

The deeper point is architectural. In IRIS, knowledge lives inside the instance: compiled classes, the class dictionary, generated routines, running productions. Generic AI assistants that only see files on disk are blind to that state. iris-agentic-dev closes exactly that gap by giving the AI read/write access to the live instance state through the Atelier API. For a project like CardioIris, dense in ObjectScript and dependent on compilation against a real instance, that is the difference between an assistant that merely suggests text and one that participates in the development cycle end to end.

 


Conclusion

CardioIris is a compact yet architecturally complete demonstration of an IRIS clinical-monitoring application: persistence, ObjectScript business logic, a clean REST API, FHIR resource shaping, an AI agent reached through Embedded Python, containerization, and a proxy layer that protects database credentials. In miniature, it captures what real IRIS for Health systems look like.

It is exactly that structure that makes tooling like iris-agentic-dev so relevant. By exposing the live IRIS state to an AI assistant over MCP compilation, SQL queries, debugging, tests, and container orchestration the tool turns ObjectScript development from a manual, fragmented cycle into a continuous, conversational one. For anyone building on InterSystems IRIS, that kind of integration stops being a convenience and becomes a concrete advantage in both productivity and quality.

Declaration of AI usage  For the code development, using Claude Code with the InterSystems MCP iris-agentic-dev, using Claude Opus 4.8 to structure and transform and complement the article.


References: the CardioIris repository (github.com/Guspex/cardioIris) and iris-agentic-dev (github.com/intersystems-community/iris-agentic-dev). Code excerpts are drawn from DashboardDispatch.cls and cardioflow_proxy.py.