Blog › ICP guides
MUMPS developer on retainer: naked reference, global variables, Caché ObjectScript, and MUMPS/M on monthly retainer
October 3, 2026 · ~14 min read
A MUMPS developer was maintaining an Epic healthcare clinical data module that managed patient diagnosis history. The module read patient records from the global ^PATDAT, a hierarchical sparse array keyed by patient ID and record type. A typical access pattern was ^PATDAT("12345","DX","HIST") — the patient ID "12345", record type "DX" (diagnosis), sub-record "HIST" (history). The developer was adding a new sub-record type to the diagnosis history: alongside "HIST" they needed to also write a "CURR" sub-record representing the current active diagnoses. The developer wrote two consecutive lines in the new routine: first, SET ^PATDAT("12345","DX","HIST")=historyData, then immediately after: SET ^("DX","CURR")=currentData.
In MUMPS, ^ without a global name is a “naked reference” — it continues from the most recently set global reference, replacing the subscript at the level where the naked reference’s first subscript begins. The previous full reference was ^PATDAT("12345","DX","HIST"): three subscripts. A naked reference of ^("DX","CURR") starts at subscript level 1 and provides two subscripts, which replaces the previous reference at subscript levels 1 and 2, producing ^PATDAT("DX","CURR"). The developer intended ^PATDAT("12345","DX","CURR") — a three-subscript path under patient "12345". The naked reference produced ^PATDAT("DX","CURR") — a two-subscript path under a key named "DX" in the root of ^PATDAT, which is the wrong structural location entirely.
Three patient records where both a HIST write and a CURR write occurred in the same routine execution had their CURR data written to the wrong global location: ^PATDAT("DX","CURR") instead of ^PATDAT("12345","DX","CURR"). The correct per-patient CURR sub-record remained unset. Downstream read code that checked ^PATDAT(patientID,"DX","CURR") found no data and defaulted to empty current diagnoses. Wrong diagnosis records read: 3 → 0 after replacing the naked reference with the fully qualified SET ^PATDAT("12345","DX","CURR")=currentData.
The reason naked reference bugs are systematically invisible: naked references are a documented MUMPS feature, used intentionally in legacy code for brevity — avoiding the repetition of a full global path when making multiple accesses to the same global subtree. The bug is invisible at the code site because ^("DX","CURR") looks like a shorter form of the prior access — and indeed it is intended as one, but the subscript level resolution is context-dependent on the most recent global reference’s subscript count. If the prior global reference had been ^PATDAT("12345") (one subscript), the naked reference ^("DX","CURR") would still produce ^PATDAT("DX","CURR") — wrong. If the prior reference was ^PATDAT("12345","DX") (two subscripts), the naked reference would need to be ^("CURR") to produce ^PATDAT("12345","DX","CURR") — correct. The subscript count of the prior reference determines whether a naked reference resolves correctly, and maintenance developers editing existing routines may unknowingly change the prior reference’s subscript count, invalidating all subsequent naked references in the routine. The MUMPS interpreter produces no warning either way.
MUMPS: the M language, global variables, and healthcare computing
MUMPS — Massachusetts General Hospital Utility Multi-Programming System — was developed between 1966 and 1967 at Massachusetts General Hospital by Neil Pappalardo, Curt Marble, and Robert Greenes on a PDP-7. It was designed explicitly for interactive database applications in clinical settings: fast access to sparse, hierarchical patient data, with multiple concurrent users and direct disk persistence without a separate database server. MUMPS was standardized as ANSI/ISO M in 1977; subsequent revisions appeared in 1984 and 1995 (ANSI X11.1-1995). The language is also called M, and both names refer to the same language. Current production implementations include Caché ObjectScript (InterSystems, commercial, extends MUMPS with object-oriented classes, SQL, and web APIs), GT.M (now maintained as YottaDB, open source, the engine underlying VistA), and InterSystems IRIS (the successor to Caché, released in 2018, combining the MUMPS runtime with a data platform and interoperability layer). Epic Systems — the dominant US electronic health records vendor — uses MUMPS as the foundational language of Epic EHR software; virtually every major US hospital system running Epic is running MUMPS code. VistA (Veterans Information Systems and Technology Architecture), the EHR built by the US Department of Veterans Affairs, is also built on MUMPS and has been open source since the 1980s; it runs in VA hospitals and has been adopted by health systems internationally.
MUMPS’s design philosophy is radical minimalism: everything is a string. There are no data types. Numbers are strings that happen to be interpretable as numeric; the expression "3" + "4" evaluates to 7; "3ABC" + "4" evaluates to 7 because MUMPS takes the leading numeric portion of a string for arithmetic. The $NUMBER() function extracts the numeric value of a string explicitly. A variable holds whatever string was stored in it; the same variable can hold a patient ID string, a numeric count, or a formatted date, and arithmetic on it will silently coerce whatever value is present to its leading numeric component. This makes MUMPS highly permissive at the cost of making type-related bugs entirely silent.
The global variable model is MUMPS’s most distinctive feature. A global is a persistent hierarchical sparse array stored on disk: the name ^PATDAT denotes the root of the array, and subscripted forms like ^PATDAT("12345","DX","HIST") address individual nodes by their subscript path. Subscripts are strings; the hierarchy is implicit in the subscript structure. Globals survive process termination — they are the database. Process-local variables use no ^ prefix and exist only for the lifetime of the process. Key global operations: $DATA(^G(s)) returns 0 (undefined), 1 (has a value at this node), 10 (has descendant nodes but no value here), or 11 (both value and descendants); $ORDER(^G(s)) returns the next defined subscript at the same level as s, or "" at the end; $QUERY(^G(s)) returns the next defined node at any level in depth-first order; KILL ^G(s) deletes the node and all its descendants. The sparse array model means that only nodes that have been SET exist; iterating with $ORDER skips over the vast range of undefined subscripts between defined ones, which is efficient but requires careful termination logic.
Naked references, $ORDER traversal, and MUMPS pattern matching
Naked references are a performance feature from MUMPS’s 1960s origins: disk access to a global node requires resolving the full subscript path, and if consecutive accesses are to nodes in the same global subtree, repeating the full path is wasteful both to type and to execute. A naked reference ^(sub1,sub2,...) resolves by taking the global name and initial subscripts from the most recently referenced global and replacing subscripts starting from level 1 with the provided subscripts. “Most recently referenced” means the last SET, GET, KILL, or expression that accessed a global — including globals accessed inside routines called between the full reference and the naked reference. Naked reference resolution is not lexically scoped; it depends on runtime execution order. A DO call to a subroutine that internally accesses any global will update the naked reference context, invalidating any naked reference that follows the DO call in the calling routine. The intrinsic variable $REFERENCE holds the current naked reference context as a string; it can be saved before a DO call and restored via indirection afterward: SET savedRef = $REFERENCE … SET @savedRef = value, where the @ operator treats the string in savedRef as a global reference to be dereferenced. Defensive practice for maintenance code: always use fully qualified global names and avoid naked references in any routine that will be modified by developers unfamiliar with the existing subscript context.
MUMPS indirection via @ is the language’s most powerful and most dangerous feature. SET @varname = value treats the string value of varname as a global reference name and writes value to that global. This allows computed global names (building a global name from a string variable) and the $REFERENCE save/restore pattern. It also makes static analysis of global access patterns impossible: a MUMPS program that uses indirection extensively cannot be reliably audited by examining the source text alone, because the actual global accessed at runtime is determined by the contents of the variable being dereferenced.
$ORDER is the primary mechanism for iterating over sparse global subscripts. The canonical pattern for iterating all patients in ^PATDAT is: SET patID = "" FOR SET patID = $ORDER(^PATDAT(patID)) QUIT:patID="" DO processPatient(patID). $ORDER returns the next defined subscript after its argument at the same level, or "" when there are no more subscripts. The termination condition must be QUIT:patID="" — a string comparison against the empty string. Using QUIT:patID=0 instead introduces a subtle bug: in MUMPS, "" = 0 evaluates to true in a numeric context (the empty string coerces to 0), so QUIT:patID=0 terminates the loop correctly at the end of the global — but also terminates it prematurely if the first defined subscript happens to be the string "0". Any patient or record with a key equal to "0" will cause the loop to exit before processing that record.
MUMPS pattern matching uses the ? operator: "12345" ? 5N matches five numeric characters; "A123" ? 1A3N matches one alpha character followed by three numeric characters. Pattern codes include N (numeric), A (alpha), E (any character), and P (punctuation). The $FIND(string,target) function returns the position after a target substring in a string; $EXTRACT(string,from,to) extracts a substring by position; $PIECE(string,delimiter,position) extracts a delimited field (analogous to split in modern languages). These string operations are heavily used in MUMPS healthcare code for parsing HL7 messages, extracting structured fields from delimited records, and validating patient ID formats before global access.
Typical MUMPS retainer work and what it looks like in a work log
Naked reference bugs are the largest category of MUMPS retainer work that produces no visible artifact. The pattern has a consistent shape: a routine makes a full global reference, then uses a naked reference for a subsequent access in the same global subtree; the routine works correctly when both references have the same subscript structure; a maintenance edit changes the subscript count of the full reference, or inserts a routine call that changes the naked context, and the downstream naked reference now resolves to a different global path. The bug is invisible at the code site because the naked reference still looks syntactically valid. Work log entry: “DIAGHIST routine: SET ^("DX","CURR")=curr naked reference after SET ^PATDAT("12345","DX","HIST")=data; naked context = ^PATDAT("12345","DX","HIST") (3 subscripts); naked ^("DX","CURR") replaces subscripts 1–2 → ^PATDAT("DX","CURR") not ^PATDAT("12345","DX","CURR"); 3 patient CURR records written to wrong global node; fix: replaced with SET ^PATDAT(patID,"DX","CURR")=curr; wrong records: 3 → 0; 2.5h.”
$ORDER traversal termination failures are the second category. A developer writes a FOR loop over a global, intends to terminate when $ORDER returns the empty string signaling end-of-subscripts, but writes the QUIT condition as QUIT:nextKey=0 rather than QUIT:nextKey="". In MUMPS numeric coercion, the empty string and the string "0" are both equal to zero, so the loop exits both at end-of-global (correct) and at the first subscript whose value is "0" (incorrect). Any inventory item, patient, or record whose key is exactly "0" is silently skipped. Work log entry: “INVLOOP routine: QUIT:nextKey=0 exits loop when nextKey="0" (numeric coercion: "0"=0 is true); inventory items with key "0" skipped; 5 inventory items missed; fix: replaced with QUIT:nextKey=""; missed items: 5 → 0; 2h.”
Caché ObjectScript class/global interop failures are the third category, and they are common in legacy codebases that were originally written in standard MUMPS and then partially migrated to Caché’s object layer. A developer defines a Caché class with properties, but also maintains a parallel code path that accesses the underlying global storage directly using the ^ClassName.PropertyName global notation, bypassing ObjectScript method dispatch. When a schema change moves a property to a new global storage location — a common occurrence during Caché class refactoring — the direct global access reads from the old mapping node, which now holds stale or absent data, while the class method path reads from the new location correctly. Caché ObjectScript stores class instance data in ^ClassName.PropertyD globals with an internal subscript structure managed by the class compiler; accessing via ##class(ClassName).%Open(id) uses the correct property-to-global mapping regardless of schema changes, while direct ^ClassName.DiagData(id) access is tightly coupled to the storage layout at one point in time. Work log entry: “PatientRecord class: SET ^PatientRecord.DiagData(id)=value direct global access bypassed Caché class property storage mapping; property DiagData moved to ^PatientRecord.DiagDataD in new schema; %Save() did not see direct global write; values lost on next %Open(); fix: replaced direct global SET with oref.DiagData = value; DO oref.%Save(); lost values: 3 → 0; 2.5h.”
Track MUMPS developer retainer hours without the status emails
When a 2.5-hour MUMPS debug session traces three wrong Epic patient diagnosis records to a naked reference that resolved to ^PATDAT("DX","CURR") instead of ^PATDAT("12345","DX","CURR") — invisible because the naked reference looks like a shorter form of the prior access — the work log must name the naked reference context, the prior global reference, the subscript count mismatch, and the patient record count before and after. HourTab gives your MUMPS retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the global path and the resolution mechanism. No client login. No status emails. CSV in, URL out.
How HourTab tracks MUMPS developer retainer hours
MUMPS retainer work is invisible by the same mechanism that makes naked references dangerous: the MUMPS interpreter produces no warning when a naked reference resolves to an unintended global node. The SET completes with no error — the data is written, just to the wrong location. The code compiles and runs correctly in the sense that MUMPS produces no diagnostic. The incorrect behavior appears only in the records where the prior global reference had the wrong subscript count at runtime, which may depend on a code path that occurs infrequently. An Epic module that processes thousands of patient records per day and executes the bug path in only three of them produces three quietly wrong records with no log entry, no exception trace, and no indication that anything has gone wrong at the MUMPS level.
The work log needs to name the mechanism: which routine, which naked reference, which prior global reference set the context, why the subscript count produced the wrong path, and what the patient or record count change was before and after the fix. A log entry that says “fixed global access bug in patient history module, 2.5h” is not auditable. A log entry that says “DIAGHIST routine: SET ^("DX","CURR")=curr after SET ^PATDAT("12345","DX","HIST")=data; naked context 3 subscripts; ^("DX","CURR") replaces levels 1–2 → ^PATDAT("DX","CURR"); 3 patient CURR records at wrong global node; replaced with fully qualified path; wrong records: 3 → 0; 2.5h” is auditable.
HourTab gives MUMPS developers a public retainer-hours URL they send to clients — hospital systems and Epic implementation partners maintaining clinical data modules, VA medical centers running VistA customizations, InterSystems IRIS upgrade projects, and healthcare ISVs building on GT.M/YottaDB. For MUMPS retainers, each work log entry should name the mechanism: which global, which naked reference or traversal, what subscript path was intended and what was resolved, and which records demonstrated the incorrect behavior. Comparative context: MUMPS retainer work has structural overlap with other legacy healthcare and mainframe languages — COBOL retainers cover implicit data coercion and REDEFINES clause aliasing bugs, where the data record is written to a correct storage location but read through an incompatible field layout, producing wrong values with no error message — structurally the same failure mode as a naked reference writing to a structurally incorrect but syntactically valid global node; and Common Lisp retainers cover defmacro variable capture, where a bare symbol in a macro expansion resolves to the caller’s lexical binding rather than the macro’s intended internal variable — a context-dependency problem that parallels the MUMPS naked reference exactly: in both cases, the resolution of a reference depends on context that is not visible at the code site, and maintenance edits that change that context silently invalidate the dependent reference.
FAQ: MUMPS developer retainers
What does a MUMPS developer on retainer typically do?
A MUMPS developer on monthly retainer covers naked reference global access diagnosis (identifying where a bare ^ followed by subscripts resolves to an unintended global node because the prior global reference’s subscript count differed from the developer’s expectation; the fix is replacing the naked reference with a fully qualified global name); $ORDER traversal termination debugging (identifying where a QUIT condition uses numeric comparison against 0 rather than string comparison against "", causing the loop to exit prematurely when a subscript key is the string "0"); Caché ObjectScript class/global interop diagnosis (identifying where direct global notation bypasses property-to-global storage mapping, causing values to be written to the wrong global node and lost on the next %Open()); GT.M and InterSystems IRIS performance tuning; and Epic/VistA module maintenance including routine debugging, global structure inspection, and upgrade coordination.
What MUMPS work is most commonly underlogged?
Naked reference bugs are the most systematically underlogged MUMPS retainer work. A naked reference — ^ followed by subscripts but no global name — resolves by taking the most recently accessed global and replacing subscripts from level 1. The naked reference’s subscript count determines which level is overwritten: a prior reference with three subscripts and a naked reference providing two subscripts produces a two-subscript path, not a three-subscript path. The bug is invisible at the code site because the naked reference looks like a shorter form of the prior access. There is no MUMPS interpreter warning, no compile error, and no diagnostic — the SET completes successfully, writing data to the wrong global node. The incorrect behavior appears only in records where the prior global reference had the wrong subscript count at runtime. Maintenance edits that change a prior reference’s subscript count, or that insert a routine call between a full reference and its dependent naked references, invalidate all downstream naked references in the routine. Two to five hours invisible per occurrence.
What are typical MUMPS developer retainer rates?
Entry-level MUMPS developers with experience in MUMPS globals, $ORDER traversal, and basic Caché ObjectScript typically bill at $75 to $135 per hour. Mid-level MUMPS programmers with experience in naked reference debugging, GT.M/InterSystems IRIS, and Epic/VistA module maintenance typically bill at $110 to $195 per hour. Senior MUMPS developers with deep knowledge of Caché class/global interop, InterSystems IRIS upgrade paths, VistA customization, and Epic certification typically bill at $155 to $280 per hour. Monthly retainer ranges: $1,800 to $3,400 per month for advisory engagements covering naked reference audits and global structure reviews (12 to 20 hours per month); $2,800 to $6,500 per month for active maintenance including routine debugging, $ORDER traversal review, Caché ObjectScript class work, and IRIS upgrade support.
What should a MUMPS developer retainer agreement include?
A MUMPS developer retainer agreement should specify: implementation scope (GT.M/YottaDB, Caché, or InterSystems IRIS — these differ in extension APIs, class system support, and journal/recovery mechanisms); Epic/VistA scope (whether the retainer covers Epic EHR module maintenance, VistA custom routines, CPRS integration, or HL7 interface work); Caché ObjectScript class scope (whether method development, class compilation, and %Save()/%Open() lifecycle work are included); naked reference audit scope (whether the retainer covers converting legacy naked references to fully qualified global names throughout a codebase, which is a significant effort in large legacy routines); $ORDER traversal review scope (whether the retainer includes auditing loop termination conditions for the ="" vs =0 class of bugs); and IRIS upgrade scope (whether the retainer covers InterSystems IRIS migration from Caché, namespace migration, class mapping changes, and compatibility testing).
How should MUMPS developer retainer hours be logged?
Log each MUMPS retainer session with the following detail. For a naked reference bug: routine name, the naked reference used, the prior global reference that set the naked context (including its subscript count), the expected global path, the actual global path that was resolved, and the patient or record count before and after the fix (e.g., DIAGHIST routine: SET ^("DX","CURR")=curr naked reference after SET ^PATDAT("12345","DX","HIST")=data; naked context = ^PATDAT("12345","DX","HIST") (3 subscripts); naked ^("DX","CURR") replaces subscripts 1–2 → ^PATDAT("DX","CURR") not ^PATDAT("12345","DX","CURR"); 3 patient CURR records written to wrong global node; fix: replaced with SET ^PATDAT(patID,"DX","CURR")=curr; wrong records: 3 → 0; 2.5h). For a $ORDER traversal bug: the routine name, the loop termination expression, the QUIT condition that was wrong (e.g., QUIT:nextKey=0 vs QUIT:nextKey=""), and the subscript value that triggered the premature exit. For a Caché ObjectScript class/global interop bug: the class name, the property name, the direct global notation used, the correct %Save() method call, and the values lost then recovered.