
The first release of Shutter: supervised recovery of failed HL7 deliveries in an InterSystems IRIS Interoperability production, with a board of IRIS AI Hub agents.
When a hospital’s HL7 delivery fails, the usual fix is Resend, and when two systems move a patient at the same moment, that resend writes over the bed manager’s ward with every check passed. Shutter’s IRIS AI Hub agents decide how to put the failure right; a deterministic gate decides whether anything is written.
On an ordinary day a hospital’s integration engine can see hundreds of failed messages. Engineers put right what they can by hand, and the rest can go unactioned. Shutter won’t fix them all, but it helps with most of the failed patient movements it can see: it resends the ones that pass its checks, and for the rest it stops, writes nothing, and names the patient a person needs to review.
Shutter is about delivery, not diagnosis: it resends what the failed message asked for, unchanged, or not at all, and shows why. Recovery needs a receiver with an API; a plain MLLP receiver gets the investigation and a drafted escalation, never a resend.
What can go wrong when you resend: a nurse moves the patient while a resend waits, the receiver refuses it as “duplicate” with nothing filed, the clocks go back, a merge has since been undone by hand. The list says what Shutter does about each and what it doesn’t cover yet.
For judges, in about three minutes, with nothing to install:
Jump to: Take the agents away · Try it · How it works · Where the agents sit · AI Hub · Bonuses · What can go wrong when you resend · Testing · Limits · Run details
Trial mxsalata.github.io/shutter | Online demo shutter.mxsalata.com | A real IRIS, read only Shutter in IRIS | Video [link at submission] | Articles [link at submission] | Open Exchange [link at submission]
Why I built it: my background is interoperability with 28 NHS hospitals in the UK, and real testing of an integration only happens in production, where a receiver behaves in a way nobody foresaw, a message arrives out of order, or someone fixes something by hand. Shutter is a safety net for those cases, made for a clinical setting, where a wrong write has a patient on the other end. Mikolaj Salata (MxSalata)
Instead of a Resend in the Message Viewer, Shutter makes each failed delivery a case: the board, four AI Hub agents (three specialists and a coordinator), reads its sealed evidence and proposes a plan; the gate, deterministic checks, decides whether the plan may write; a person, or the site’s written policy, releases it; one conditional write follows, checked against the receiver’s receipt and history. The AI board proposes; the gate disposes.
| Shutter decides | For example | What is written |
|---|---|---|
| Resend | S2: a transfer that never arrived | One conditional write once released, verified from the receiver’s history |
| Superseded | S1: a later transfer has already moved the patient | Nothing; the closure is checked against the receiver’s history, or against a later message the feed sent |
| Reconcile | S3: the receiver holds the message; only its acknowledgement was lost | Nothing; the receipt is checked |
| Escalate | S8: two moves at one moment | Nothing; a person decides |
| Withheld | S15: a patient merge, for the PAS team | Nothing; no agent runs |
| An incident | S11: an outage, eight failures to one receiver | One chain per encounter, released in event order by the site’s policy or an approval, each item verified, stopping at the first surprise |

Terms: the receiver, the system the failed message was going to (in the demo a bed manager); the catalogue, the failures Shutter knows and each one’s safe outcome; PAS, the patient administration system; Verified, read in the receiver’s receipt and history, never from an acknowledgement (all the terms).
Scenario S8 shows what a plain resend misses. Two systems move one patient at the same moment. The matcher flags it as catalogue entry E7 with no model. Rules only mode has no agents: the message is resent unchanged unless the gate objects. On Shutter’s own scenarios, with no case held out (comparison):

What do the agents add? Not the flag: the matcher had already marked S8, and in Rules only the gate doesn’t stop a resend that names no entry. The board checks the entry against this case’s evidence, says in words why it applies or doesn’t, and keeps any dissent (in S8, the conformance specialist’s resend, and why it was overruled). Each agent is given the catalogue’s entry and preferred outcome, and the coordinator took it in 24 of 25 recorded plans: agents applying written policy, not answering unaided (what that does and does not show). Site policy mode settles 8 of those 11 from the catalogue with no agent and sends S4 and S8 to the board; S7 has no such mode.
| Way to try it | You need | Time |
|---|---|---|
| The trial: recorded runs; nothing leaves the browser | Nothing to install | The quick tour (open it): 9 steps, about 4 minutes. The engineers’ tour (open it): 6 steps, about 3 minutes |
| The online demo: a live hospital, a new failure every 7 minutes; any resend waits for you | The sign-ins below | The steps below |
The replay edition, ./install.sh --replay: your own instance, the board replaying its recorded live runs |
Docker; no key, no Early Access file | Healthy in 2 to 8 min once the 6 GB image is pulled |
The AI Hub edition, ./install.sh --image-file ...: live agents, or their recordings without a key |
Docker and the build 109 file from the Early Access portal; ANTHROPIC_API_KEY for live agents |
Healthy in 3 to 15 min on build 109 (receipt) |
In a hospital’s own IRIS for Health: Shutter installs without the demo, beside or inside the productions it watches (docs/INSTALL.md; several namespaces). Failures reach it through a forwarder, or through the production’s alerts with no change to the hospital’s own code. A receiver with no API still gets a case, the board’s diagnosis and a drafted email, never a resend, and a production review needs nothing configured (what each kind of receiver gets). Every proof so far is against the demo receiver or a stub, so treat recovery as a demonstration until a site has tested its own receiver.
The online demo is Mikolaj’s Fictional Hospital, with made-up patients, on AI Hub build 2026.3.0AI.154.0. The board runs live for the judges’ account, whose sign-in the organisers pass to the judges within a model budget of 15 USD a day. Its sign-ins:
demo / shutter.demo-trace / Shutter-Demo-IRIS-Portal: open https://shutter.mxsalata.com/csp/healthshare/shutter/Shutter.Portal.Guide.cls (or “Shutter in IRIS” in the web app); it first asks you to sign in with GitHub or an emailed one-time code, then signs you in as that user.On your machine: install.sh checks Docker, Compose, memory, disk, the ports and the image, and changes nothing until every check passes (Windows or Apple silicon).
git clone https://github.com/MxSalata/shutter.git && cd shutterThe AI Hub edition (live agents), with the image file from InterSystems' Early Access portal:
./install.sh --image-file irishealth-community-2027.1.0AI.109.0-docker.tar.gz
Or the replay edition: the public IRIS for Health image, no Early Access file, no key:
./install.sh --replay
Shutter is ready after 3 min 21 s:
the web app: http://localhost:52773/shutter/index.html (approver / shutter, or operator / shutter)
first steps: Scenarios, run S2 in Board mode, then approve its case (the README's "On your machine or in the trial, first")
the portal: http://localhost:52773/csp/sys/UtilHome.csp (_SYSTEM / SYS, namespace SHUTTER)
2027.1.0AI.109.0, the one the Early Access portal offers since 2 November 2026. Shutter is also verified on 2026.3.0AI.154.0, the contest’s first (receipt).ANTHROPIC_API_KEY in .env (Anthropic’s API only, so with another provider’s key it replays too); without a key the board replays, and the case page says so. The replay edition is the same without live agents or knowledge search. Allow Docker at least 5 GB of memory, the minimum, and about 25 GB of free disk.approver / shutter (approves) or operator / shutter; the Management Portal as _SYSTEM / SYS, namespace SHUTTER, or as approver / shutter for the Workflow inbox.The gate is the same in every mode; Single investigator runs live only (the modes).

Try to break it, with S2’s case waiting (the first works in the trial too; the rest need your machine): as operator, Approve plan is disabled and the API answers 403; confirm the dialog in two tabs: one write, the second answering 409; approve the Workflow task with PlanDigest empty or wrong: the case blocks. Every click: first-steps.md.
On the online demo, first: with the judges’ account, Convene the board on a blocked, escalated or failed case: it investigates again, live. Break the feed on the banner: the bed manager goes down for a minute, eight movements fail, and in the demo’s rehearsal on 3 November 2026 Shutter ended with one incident waiting for one approval, with no model call; on Incidents, Approve release sends them in order.

One web app for a PC and a phone, 57 screens and states tested at a phone’s width. All 26 scenarios: scenarios.md.


FailureTimeout 0), so nothing is stuck behind it.MRN-T1; free text is matched exactly, and only that), demographics are dropped and note text becomes a flag.Sign-in stays inside IRIS: Shutter keeps no users or passwords of its own; IRIS roles say what a person may do, and the approver of record is that user. The workflows are ObjectScript rather than BPL, by choice; every step is a production message, so the Visual Trace shows it.

The three specialists (conformance, encounter and impact) and the coordinator are business operations in the production: the specialists run in parallel, then the coordinator writes the plan. A pass takes 24 to 47 seconds and costs 0.34 to 0.50 USD across the 25 recorded passes on claude-opus-5-5. No agent has a write tool: only the gate’s checked, released plan is ever written. The contest’s task, and the code to read first: ai-hub.md. What one S8 agent was sent and did; in the trial, Show the recording on each member’s card.

| AI Hub feature | Where |
|---|---|
%AI.Agent: the board, a single investigator, the production review agent, the Explain agent and the board’s question mode |
Agent |
%AI.Tool with typed arguments; the answer tools refuse a setting not among the case’s facts |
Tools, Schema |
%AI.Policy.Authorization holds each run to its own case; %AI.Policy.Audit logs each tool call in the trace; the tool catalogue is pinned |
Policy, Catalogue.cls |
The provider key in the IRIS wallet; a live run only for holders of Shutter_LiveAgents, within the day’s budget; a failure from the feed runs as system, under that budget alone |
Setup.cls, Dispatch.cls, Budget.cls |
Findings must cite evidence the run was given (evidence_refs), or the model repairs them; precedents go to the person, never to an agent |
Findings.cls, CONTRACTS.md |
| Cases, evidence and plans as read-only MCP tools through IRIS’s own MCP gateway, each call audited under the caller’s IRIS user | CONTRACTS.md |
What worked, what was hard, measured on build 2026.3.0AI.154.0: typed tools, the two policies, FastEmbed knowledge bases and prompt caching worked well; agent.Run() takes three arguments (the guide shows four) and calls only OnRunStart and OnRunComplete, so a token budget is judged after the run; a fresh Config Store needs a registry rebuild; the image has no IPM; a production sends its alerts to the one item named Ens.Alert. On 2027.1.0AI.109.0 the three arguments and the missing IPM still hold; the rest wasn’t re-run. The list, and what I would like to see: building-on-ai-hub.md; more in article 1 [link at submission].
| Bonus | Where to see it |
|---|---|
| Workflow Engine | Each approval is an EnsLib.Workflow task for the role “Shutter Approvers”, bound to the plan’s digest; S2’s waits in the Workflow inbox (CaseWorkflow.cls, IncidentWorkflow.cls). For a hospital: approvers work in the inbox IRIS already has, and an approval can never be reused for another plan |
| IRIS Vector Search | The agents’ knowledge bases are IRIS vector tables (%AI.RAG.VectorStore.IRIS, FastEmbed AllMiniLML6V2, 384 dimensions), Shutter_Knowledge.contracts (26 chunks) to Shutter_Knowledge.explain (83), searched by the agents’ search_* tools (Library.cls; a VECTOR_COSINE query on build 109). For a hospital: the agents look up the site’s own interface contract and encounter rules, kept inside IRIS, instead of guessing |
| Embedded Python | Any IANA feed time zone, through Python’s zoneinfo (Time.cls, PyOffset). In SHUTTER: SELECT Shutter_Util.Time_Probe('20261015092000', 'Europe/Paris') answers "iso":"2026-10-15T07:20:00Z". For a hospital: a feed in any IANA time zone other than Europe/London (whose rules Shutter keeps in ObjectScript) is put in true event order, summer time included (except the repeated hour at a clock change when a time carries no offset, which reads with the zone’s first offset), and the site profile accepts a feed time zone only when zoneinfo knows it (SiteProfile.cls) |
| Custom Adapter | The HL7 index walk is Shutter’s own inbound adapter, handing batches of message headers to the HL7 Index item (Index/Adapter.cls). For a hospital: Shutter knows each encounter’s other messages, so it can tell an old resend from the latest state |
| PEX (Java) | The Second Parser, a Java business operation (EnsLib.PEX.BusinessOperation with HAPI 2.6.0): when IRIS can’t read a failed message, it says whether the message is sound or malformed; see S25 (pex/). For a hospital: whether the sender or the site’s schema has to be fixed. SELECT Shutter.Setup_Doctor() (the Doctor, Shutter’s one-query readiness report) reports second_parser |
| Docker | install.sh, or docker compose up (docker-compose.yml, Dockerfile). For a hospital: a full trial on a laptop, away from the live production |
| IPM | One package for the AI Hub builds, iris-shutter, requiring IRIS 2026.3 or later with Interoperability, as the organisers ask: zpm "install iris-shutter" once it is published, or zpm "load" from a clone, as the image build does on 109 and 154 (module.xml); the replay edition’s core alone is core/module.xml. For a hospital: one install on its own IRIS for Health with AI Hub. Create and use a namespace called SHUTTER first (the install guide, section 3.1): a load in USER runs, but the web app’s “Shutter in IRIS” link answers 404 there and this page’s addresses name SHUTTER. |
| Community Opportunity idea DPI-I-689 | Interoperability as the traceability tool for AI agents: below. For a hospital: an integration team audits an agent with the Visual Trace and SQL it already knows |
| AI Hub bug reports | #11: %AI.Agent.Run() takes three arguments, not four; #12: Run() never calls OnIterationStart or OnIterationComplete |
| Embedded Python bug reports | #26: signal 11 after catching an SQL error in a Python SQL function under dynamic SQL; #27: defaults pasted into the Python source |
| Online demo | The live hospital, shutter.mxsalata.com: a new failure every 7 minutes, decided under the site’s written policy; any resend waits for you; the sign-ins are under Try it. The trial on GitHub Pages is its offline companion, recorded runs only |
| YouTube video | [link at submission] |
| Developer Community articles | [link at submission] |
Tracing AI agents with Interoperability (DPI-I-689). Every agent run is a message, so the production’s own trace does a tracing tool’s job, with no other service to run: run S8 in Board mode (the replay edition is enough), and In IRIS: Message trace opens its session in the Visual Trace, one lane per item, each AgentResponse holding the model, mode, tokens and tool calls. In the SQL page (namespace SHUTTER), that session’s tool calls:
-- One session's tool calls in order, with the board member that made each (? is the session number)
SELECT t.Seq, %EXACT(t.Name) AS Tool, t.DurationMs, t.Ok, t.Denied, %EXACT(h.TargetConfigName) AS Member
FROM Shutter_Bridge.ToolCall t JOIN Ens.MessageHeader h ON h.ID = t.RunKey
WHERE h.SessionId = ?
ORDER BY h.ID, t.Seq

The full walk-through: tracing.md; tokens and cost by board member in SQL: running.md.
Each row is played against the demo receiver or a stub listener. Seen in practice marks the ones I’ve met at hospitals myself. Handled: a test shows the outcome. Stops and flags: nothing is written and the case says why, so a person decides, often the right answer. Says so: Shutter tells you and takes no action. “In CI”: IRIS for Health Community 2026.2, the agents replaying recorded live runs; the gates, rules and receiver run for real. What each row doesn’t cover.
| What goes wrong | What Shutter does | Proof |
|---|---|---|
| A transfer never reached the receiver | Handled. One conditional write once a person or the site’s written policy releases it, checked against the receiver’s receipt and history | S2 in CI |
| The receiver took the message, but its answer was lost | Handled. The case reads the receipt, finds it, and resends nothing | S3 in CI |
| Seen in practice. A later transfer, or a nurse’s move by hand, reaches the receiver while a resend waits for approval | Handled. The approval is bound to the receiver’s version: when it moves, that approval is void, nothing is written on it, and the case is planned afresh from what the receiver now holds. A later transfer stands | S7, S20 in CI; boundary check |
| Seen in practice. A later move overtook the failed one, but the failed move is still owed to a system that keeps history | Handled. Per receiver: a current-state system closes with nothing sent; a history-of-record system goes to a person with the correction steps, never a plain replay | S22 in CI |
| A transfer timed in the hour the clocks go back, on a Europe/London feed with no offset | Stops and flags. A time that could be either hour isn’t ordered by guesswork; a person decides | S21 in CI |
| Seen in practice. The receiver refuses a message as “duplicate”, but nothing was filed | Handled. That refusal closes nothing: the case reads the receipt and history, finds neither, and a person approves the resend, verified only from the receipt and history | S4 in CI; Verify tests |
| A newer message for the same visit is on its way as Shutter is about to resend | Stops and flags. The gate and the connector hold the resend and name the message; if it lands, the case closes with nothing written | S24 in CI |
| A failed patient merge, perhaps since undone by hand or read the wrong way round | Stops and flags. Never resent automatically. By default no agent runs, the PAS team gets the case, and no reverse merge is invented | S15 in CI |
| A plain MLLP receiver answers AE, the engine suspends the message and the queue moves on. Seen in practice: a listener that allows one connection, so a second can knock out the live feed | Stops and flags. A case opens through the alert intake with the evidence and the reason; nothing is resent, and Shutter opens no MLLP connection of its own | Plain receiver check in CI |
| Two patients in one message, such as an A17 bed swap | Stops and flags. Not mapped, nothing sent, a person decides, no split invented | Trigger test, gate test |
| Any result or order, where a resend could put a preliminary over a final | Stops and flags. No result or order is ever resent: up to 2 MB it gets a case and a drafted email | Gate test; onboarding check, run by hand |
| “Completed” in the Message Viewer means less than it reads: reply checking off, or any positive code accepted | Says so. Onboarding states, per TCP operation, what Completed proves there; Verified is never read from an acknowledgement | Onboarding check, run by hand on stock IRIS; Verify tests in CI |
| Two approvers, one working from an old tab, or one approval clicked twice | Handled. In the web app a changed plan or an older case version is refused, one approval makes one write, and a closed case takes no more; in the Workflow inbox a wrong digest blocks the case with nothing written | Boundary check and inbox check in CI |
CI reports what follows on its run at the release commit: every job green, all 771 browser tests passed.
main that changes code: the full install on IRIS for Health Community 2026.2, 37 receiver contract tests, 290 unit tests (8 more on an AI Hub build only) and 26 scenarios end to end through the REST API, one through the demo hospital’s production.For a clinical safety officer: the hazards, data flow, threats and what a site still owes.
Every limit in this list, in full, is on one page, which links the rest.
AlertOnError on opens a case for a person.dry_run and writes anyway has taken the write: an alert, no undo.failed for a person. To be fixed in v1.1.0 (the measurements).SHUTTER_DAILY_BUDGET_USD stops new live runs at the day’s limit, then cases take the rules path; runs in flight can finish past it, so keep a hard limit at the provider (more). A storm of one kind convenes one board; the rest follow it, each through its own gate and approval.FailureTimeout -1 at a receiver with an API can deliver the same message twice; one instance, with failover, mirrors and restores untested, no switch yet to stop all writes, and Case Workflow’s pool kept at one; no local-model profile, on purpose.What the organisers ask each README to give; install.sh above does these steps for you.
docker image load -i irishealth-community-2027.1.0AI.109.0-docker.tar.gz
docker tag docker.iscinternal.com/docker-intersystems/intersystems/irishealth-community:2027.1.0AI.109.0 irishealth-community:2027.1.0AI.109.0
git clone https://github.com/MxSalata/shutter.git && cd shutter
cp .env.example .env # SHUTTER_IMAGE names the build; ANTHROPIC_API_KEY for live agents (model claude-opus-5-5)
docker compose up -d
docker compose ps # again until it says healthy
curl http://localhost:52773/api/shutter/health
irishealth-community:2026.3.0AI.154.0, and set SHUTTER_IMAGE to it. If docker image load prints another name, tag that one."ok":true once the production runs..env, set SHUTTER_IMAGE=containers.intersystems.com/intersystems/irishealth-community:2026.2, then docker compose up -d --build.docker compose down -v first (it removes the instance’s data), then remove .env or change SHUTTER_IMAGE and SHUTTER_WITH_AIHUB in it: install.sh refuses an .env set up for the other edition.Open it: the web app at http://localhost:52773/shutter/index.html and the Management Portal at http://localhost:52773/csp/sys/UtilHome.csp, with the sign-ins under Try it (production Shutter.Production; the demo hospital’s is Hospital.Production in HOSPITAL). More in docs/readme/running.md and, for a hospital’s own IRIS for Health, docs/INSTALL.md.
Troubleshooting. install.sh prints [fix] at each check it stops at, and gives up after 25 minutes with the log’s last 30 lines; SELECT Shutter.Setup_Doctor() in the SQL page (namespace SHUTTER, as _SYSTEM) is the readiness report. “License limit exceeded”: docker compose restart. The rest: troubleshooting.md.
Shutter is an entry to the InterSystems Programming Contest “AI Agents in Interoperability Workflows” (Open Exchange contest 49).