HL7Script is used to perform a sequence of instructions based on the contents of one or more inputs (typically HL7 messages) from a file wildcard or database query. The instructions allow you to analyze or transform the input and generate some kind of output. That output can be condensed information about the messages, or a modified set of the input HL7 messages. When used in conjunction with the HL7TransmitterService, it becomes a powerful HL7 integration engine able to modify and direct messages to and from numerous other systems.

Download HL7Tools here


Tip: Print this page to a PDF for a local copy of the reference.

Table of Contents

Script Syntax Conventions

HL7Script script files are plain text files. They contain one or more commands to perform on a series of input HL7 messages.

A few lines of a typical script might look something like this:

SET $NEWID = "X" + PID.3.1 ; Stick an X on the front of the MRN
HL7 SET PID.3.1 = $NEWID ; Replace the MRN in the output HL7 message
HL7 OUTPUT ; Write the output HL7 message to disk

HL7 Data

To reference a value from the input HL7 message, use the following key syntax. Items shown in [square brackets] are optional.

SID[#q].f[~r][.c[.s]]

Where:
SID = 3-letter segment id (e.g. MSH, PID, etc.)
q = segment seQuence
f = Field index
r = Repetition index
c = Component index
s = Subcomponent index

Examples: PID.3.1, PV1.44, OBX#2.5~3.1

A field index is always required, except in a few situations where only a segment reference is needed (see Segment ID Loops, HL7 Commands). All omitted indexes default to 1.

Field index zero references the segment ID itself (e.g. PID.0 = "PID").

If there is more than one of a given type of segment, the segment sequence is used to identify which one you want. Sequence 1 is the segment that appears first. For example, if there were three DG1 segments you would refer to the diagnosis codes as DG1#1.3.1, DG1#2.3.1 and DG1#3.3.1. Without a sequence index, the first such segment is assumed (DG1#1.3.1 is equivalent to DG1.3.1).


Note: The script supports Named Fields in field keys if a Named Fields file is selected in the program preferences or script. Named Fields let you use field keys with "friendly" names rather than (or in addition to) numeric indexes and lets you validate message data against field definitions (see HL7Viewer).

Strings

All data in HL7Script is stored as strings. A value may temporarily be treated as as a number or a date for calculations or comparisons, but at rest it is always a string.

; A string literal
SET $STR = "I am a string"

A string literal value is enclosed in double quotes ("). If the value requires an actual quote character, double it. For example, to specify a lone quote use """".

The plus sign (+) is the string concatenation operator. Whitespace around the plus is allowed.

; Example: Create a formatted name as "Last, First Middle"
SET $FULLNAME = PID.5.1 + ", " + PID.5.2 + " " + PID.5.3

Character indexing starts at 1. Any functions that accept or return character indexes (like POS and SUBSTR) consider the first character to be at position 1.

Comments and Whitespace

Single-line comments are specified with a leading semicolon (;). Semicolon end-of-line comments are also supported.

Block comments are supported using /* and */ pairs, and may appear at the end of a line (only one to a line). Block comments may be nested.

Whitespace is ignored, so feel free to leave blank lines and indent for clarity.

; This whole line is a comment

/*
  Block
  comment
*/

SET __Debug = "1" ; End-of-line comment

LOG "Hello World" /* This is also a valid comment */

Return to Top

Script Sections

A script is divided into sections. Only the "Main" script, the part that applies to each message processed, is required. All of the other sections are optional and are surrounded by tags to separate them from the main script.

<INIT>
  ; Initialization section
</INIT>
<PRE>
  ; Pre-processing section
</PRE>

; Anything not in one of the other sections is the Main script.
; The Main script is run for each message processed and must
; contain at least one line of code.

<POST>
  ; Post-processing section
</POST>
<ERROR>
  ; Error processing section
</ERROR>
<FINAL>
  ; Finalization section
</FINAL>

The placement and order of the optional sections is unimportant, but the lines of the main block should be contiguous.

Initialization

The initialization section is processed only once when the script is first loaded. The section must be surrounded by <INIT> and </INIT> on lines by themselves.

Unlike the other sections, the syntax of the initialization section is limited. Here you may only assign variables (including calling LOADVARS), set up translation tables, and create user-defined procedures.

; Example Initialization Section
<INIT>
  TRANSLATION=tablename
    input=output
    FOO=BAR
  ENDTRANS
  PROCEDURE=procname
    INC _CALLCOUNT
    LOG "You have called this procedure " + _CALLCOUNT + " times."
  ENDPROC
  LOADVARS "D:\HL7\vars.txt"
  %PersistVar="value"
  __Debug="1"
</INIT>

Translation tables are used in message processing to translate one value to another (see the TRANSLATE function). Start the entry with TRANSLATION, an equal sign, and the name of the translation table. You may have as many translation tables defined as you need as long as each table name is unique. The table name, input, and output values need not be quoted unless they contain important whitespace or reserved words. Translation table lookups are case-sensitive unless the ITRANSLATION keyword is used. Each translation table must be finished with ENDTRANS on a line by itself.

User-defined procedures can also be defined in the initialization section. Procedures are an easy way of repeating the same block of lines in more than one place in the script while avoiding duplicate code. The procedure name may or may not be quoted. All lines between the PROCEDURE and ENDPROC lines become the body of the procedure. The procedure can be called at any point in other parts of the script using the "CALL procname" syntax.

You may also make calls to LOADVARS in the initialization section:

LOADVARS "D:\HL7\vars.txt"

Any other lines in the initialization section are considered to be variable assignments with a "name=value" format:

%PersistVar="value"
__Debug="1"

Typically, only persistent or built-in variables would be set in the initialization section.

Use of the SET keyword is not needed when assigning variables in the initialization section, but it is allowed and silently discarded. Its use will display a warning during script validation.

Pre-Processing

The pre-processing section is run at the start of each processing interval. In the interactive HL7Script program, this would be whenever the "Process" button is pressed. In the service application, this would be at the beginning of each interval or input, depending on options. Here you could do things like initialize variables that are interval-specific.

If present, the section must be surrounded by <PRE> and </PRE> on lines by themselves.

There are no syntax restrictions in the pre-processing section like there are for the initialization section. All regular script commands are available, but the input HL7 message is undefined and must not be referenced.

Post-Processing

The post-processing section is performed after all messages from the input have been processed (either per input or per interval, depending on options) and is usually used to finish things up or spit out some collected information from the messages.

If present, the section must be surrounded by <POST> and </POST> on lines by themselves.

<POST>
  LOGVARS
</POST>

There are options to either run or skip post-processing if the script is aborted. Post-processing is always skipped in the event of an error. If cancelled by the user in the interactive HL7Script program, the user will be prompted for the choice.

Like pre-processing, all regular script syntax is available in the post-processing section and the input message is undefined.

Error Processing

If present, the error processing section must be surrounded by <ERROR> and </ERROR> on lines by themselves.

The error processing section is only used when an error is raised during execution of the script. The __ErrorText, __ErrorType, and __ErrorLine built-in variables will be set with information about the error.

At the very least, the section should log the error in a format of your choosing. It could then perhaps trigger some kind of notification so the error can be reviewed by a person. Examples might include inserting a record into a database table that is monitored by a notification job, or executing an external program that sends an email to devops.

If the error processing section is not present, the error will simply be logged in the format of "ERROR ON LINE line: [type] text".

The error processing section is run before any other error handling, such as the options in the service for renaming the input file or executing the error SQL.

Finalization

The finalization section is processed just once, either at service shutdown or at the end of processing in the interactive HL7Script program. The finalization section is always run, regardless of any abort or error conditions.

If present, it must be surrounded by <FINAL> and </FINAL> on lines by themselves. There are no syntax restrictions.

<FINAL>
  SAVEVARS "D:\HL7\vars.txt" "%*"
</FINAL>

$INCLUDE

A line that starts with $INCLUDE and is followed by a script filename will include the lines of the referenced script file into the calling script at that location.

If the script filename does not include a path, the path of the parent script file is assumed. The filename must be a literal value because included scripts are loaded prior to initialization -- you cannot use any variables or script logic to create the filename.

Included scripts may not contain any of the optional sections such as pre-processing. Included scripts can be nested, and circular references are detected/prevented.

$INCLUDE IncludeMe.h7s

If an error message is logged for a line that comes from an included file, the line number will be specially formatted to indicate exactly where the line originated.

Line 0037-0004: Circular include reference: $INCLUDE IncludeMe.h7s

In the above example, it can be seen that on line 37 of the main script, a file was included. On line 4 of the included script, an error was raised.

Return to Top

Variables

Like most script languages, HL7Script has variables. HL7Script doesn't bother with data types; all variables are strings. Any meaning, such as being a date or a number, comes from the context they are used in.

SET

To create or assign a variable, use the SET command followed by the variable name, an equal sign, and the value. The variable name consists of one of the variable prefixes (see below) and an alphanumeric name. The value assigned can be any valid expression; a string literal, an HL7 data value, another variable, or a concatenation of multiple values.

SET $FOO="bar"
SET $NEWID = PID.8 + "." + $FOO
SET $STARTTIME = Z@"NOW"

To clear or undefine a variable, set it to blank:

SET $NEWID=""

To reference a variable's value, simply refer to it by name:

IF $FOO == "bar"
  HL7 SET PID.3 = $NEWID
END

Variable names are alphanumeric and case-insensitive. Referencing a variable name that does not exist simply returns a blank value. The script will not throw an error to alert you of misspellings!

Scope and Lifetime

All variables are global in scope. A variable set anywhere in the script can be referenced anywhere else in the script. However, how long a variable lasts, its lifetime, is based on how it is named. Some variables live only as long as the section they're created in is running, some last for the span of a few sections, and some are effectively permanent.

Section variables start with a dollar sign ($). Once set, they exist only until processing of the current section is completed or the variable is cleared. Often called "automatic" variables. These are useful for short-term usage like loop counters or temporary string manipulation.

Interval variables are prefixed with a leading underscore (_) and are reset each time pre-processing takes place or when deliberately cleared. These allow you to collect information or totals from all of the messages in the current interval. Depending on settings, an interval may be a single input (e.g. a file containing one or more messages), or all input found in the most recent input polling.

Persistent variables are indicated with a leading percent sign (%) and last forever (as long as the service is running) or until cleared. Persistent variables are useful for maintaining long-term state or uptime/lifetime counts.

Service variables are indicated with leading double percent signs (%%) and are shared among all of the connections (threads) of the service. A variable set by one connection can be read by another. Otherwise, they behave like persistent variables. If configured to do so, they are automatically loaded at service startup and saved at service shutdown in a file called "HL7ScriptServiceVars.txt" in the application directory. If you do not make use of service variables, the file will not be created.

Tip: Threads that share a common database connection could use the database instead of service variables to share information.

There are also Built-In variables that start with double underscores (__), which are detailed in their own section of the reference.


Variable Lifetimes
SectionVariables Cleared at Start
Initialization $Section, _Interval, %Persistent
Pre-Processing $Section, _Interval, __Error*
Main Script $Section
Post-Processing $Section
Finalization $Section, _Interval
Error processingRemain as they were at the time of the error.

In the interactive HL7Script.exe program, persistent and service variables are effectively no different than interval variables due to the short lifespan of the script. Service variables are neither loaded nor saved when using HL7Script.exe.

INC and DEC

The INC command increments a variable by adding one to it (by default), assuming it to be numeric. If the variable doesn't yet exist or is not numeric, it is set to the increment value.

DEC works just like INC but decrements the value with subtraction instead of addition. If the value doesn't exist or is non-numeric, it is defaulted to zero and then decremented.

INC $FOO ; Add 1 to $FOO
DEC $BAR ; Subtract 1 from $BAR

An optional value can be provided after an equal sign to change the increment or decrement value to something other than 1. It can be an unquoted literal number or a data value, and may be negative.

DEC $FOO=2 ; Decrement $FOO by 2
INC $FOO=$FOO ; Double the value of $FOO

Dynamic Variable Names

You may create variables with dynamic names that come from data. For example, if you wanted to find out how many messages there were in a file for each given patient id, you could do something like this:

INC "_" + PID.3.1

If the input contained 7 messages for a patient with an id of "12345", the "_12345" interval variable would have a value of "7" at the end of the input.

To reference a dynamically-named variable, use parentheses with the V@ modifier to surround the name expression, or set another variable to the desired name and use V@ with parentheses on the other variable:

; V@ Modifier Parentheses
INC "%" + PID.3.1
LOG "Patient " + PID.3.1 + " has been seen " + V@("%" + PID.3.1) + " times."

; V@() to dereference a variable containing a variable name
SET $MRNVAR = "%" + PID.3.1
LOG "Patient " + PID.3.1 + " has been seen " + V@($MRNVAR) + " times."

SAVEVARS and LOADVARS

Section variables last only until the end of the current section, interval variables get reset at the start of pre-processing, and persistent variables last only as long as the service is running. It may be necessary to save the state of some of these values between runs.

If a database connection is available, values can be read from and written to the database using the Q@ and X@ data modifiers. If a database is not available, the SAVEVARS and LOADVARS commands are available. These commands save and load variables using simple "name=value" text files.

SAVEVARS can be called at any time during the script, but would most likely be used in the finalization section to store persistent variables. Follow the command with a space and the name of the file to store the variables in, then another space and a comma-separated list of variable names to save.

The comma-separated variable list may contain asterisk wildcards. A single asterisk "*" will save all currently defined user variables. A percent sign and asterisk "%*" will save all persistent variables. An underscore and asterisk "_*" will save all interval variables. Any other text followed by an asterisk will save all existing variables with a name that starts with that bit of text, like "%FOO*". The asterisk must always appear last in a wildcard.

If the variable list includes a non-wildcard variable name that is not currently defined, it will be added to the saved file with the special value "$UNDEFINED$". When loaded, that variable will be set to blank and undefined if it exists.

The filename and variable list can be unquoted literals (if they don't contain any spaces), quoted strings, or simple variable references such as $VARFILE and $VARLIST. More complex expressions are not supported.

;Examples
SET $VARFILE="C:\Data\Vars.txt"
SAVEVARS $VARFILE "%KEEP*,%GRANDTOTAL"
SAVEVARS "C:\Data\Persistent.txt" "%*"

LOADVARS can also be called anywhere, but would most likely be used in initialization. It expects just a filename, and will load any variables stored in the file. Just put a space between LOADVARS and the filename.

If the file referenced by LOADVARS does not exist, a log entry will be made but processing will continue normally; it is not considered an error.

;Initialization example
<INIT>
  LOADVARS "C:\Data\Vars.txt"
</INIT>

;Main script example
IF %VARSLOADED<>"1"
  ; Only do this once
  LOADVARS "C:\Data\Vars.txt"
  SET %VARSLOADED="1"
END

A variable file should not be shared among multiple connections; SAVEVARS and LOADVARS are not threadsafe. Service variables or a database connection serve this purpose.

Built-In Variables

There are a number of "built-in" variables available to the script. These built-in variables all start with double underscores (__). These are used to get information about the current script, environment, and settings. Some are read-only, and some may be set to affect script or message behavior.

Variables are strings unless otherwise noted. Boolean values use "0" and "1" for False and True, respectively.

Read-Only Variables

__Aborted
Boolean. Could be used in post-processing to know if the script was aborted or not.
__CurrentPath
The current directory with trailing backslash.
__ErrorLine
For use in the error processing section, it contains the line number of the last run script line just before the error was raised. It has the #-# dashed notation for lines from $INCLUDEd scripts.
__ErrorText
The text of the last error that was raised. Typically used in the error processing section to trigger some sort of notification or additional logging. This and the other __Error variables get reset to blank when pre-processing takes place.
__ErrorType
Like __ErrorText, but contains the class name of the exception that was raised. Examples: EHL7ScriptUserException (raised by the ERROR command), EHL7ScriptException (a generic script error), EHL7Exception (something wrong with the HL7 data or key usage), EConvertError (failing to convert one data type to another), etc.
Tip: If __ErrorType does not start with "EHL7", it is something that was not anticipated and caught. If repeatable, it my be helpful to submit a bug report with replication steps.
__InFile
When using file-based input, the full name of the input file that is being processed. For input from a database, this is the primary key of the current input record as a string. See also: __PK
__InName
Just the filename (without path) of the input file. For database input, this is the same as __InFile.
__InPath
The path (with trailing backslash) of the input file. Blank for database input.
__LastSeg
Numeric. The 0-based index of the last segment in the current HL7 message. This will always be one less than the value of __SegCount.
__LastVar
Numeric. The 0-based index of the last user-defined variable. This will always be one less than the value of __VarCount.
__Null
Returns HL7 null, empty quotes (""). May be easier to read than """""" or N@"".
__PK
When using database input, this is the primary key of the current input record as a string. It is blank when using file-based input. The __PK value is also available from __InFile, but a filename will never appear in __PK.
__ScriptFile
The full name of the current script file.
__ScriptPath
The path of the current script file.
__SegCount
Numeric. The count of segments in the current input HL7 message.
__SegCountOut
Numeric. The count of segments in the output HL7 message.
__VarCount
Numeric. The count of all currently defined user variables ($section, _interval, and %persistent).

Read-Write Variables

Built-in variables may not be INCremented or DECremented.

__Anonymizer
Gets/sets the name of the anonymizer definition file for use in the HL7 ANONYMIZE command. Changing the definition is persistent, even across intervals; it will remain assigned until changed to something else or the service is stopped. Setting it to blank will unassign an anonymizer definition. To reset the anonymizer's persisted data (if not using a DataStore file), re-assign the variable. It can be done in Initialization or within the main script.
; Set the anonymizer up just once in the main script:
IF __Anonymizer==""
  SET __Anonymizer="D:\HL7\Generic.anon.ini"
END
An anonymizer definition that saves increments or uses a data store should not be used by multiple connections at once; the anonymizer is not threadsafe.
__AnonymizerDef
This works just like __Anonymizer, but instead of a filename, it gets or sets the definition itself as a string. This would allow the anonymizer definition to be retrieved from a database, for example.
__AutoADD
Gets/sets the boolean AutoADD message property on the Input and Output HL7 messages. When True, ADD segments are automatically combined when a message is loaded. This works regardless of the Compact and ForceField property settings. The default is False unless set otherwise in HL7Script.exe preferences.
__Caching
Gets/sets the boolean Caching message property on the Input and Output HL7 messages. When True (the default), pre-built FieldStr and SegmentStr values are cached for faster output at the cost of additional memory usage.
__Compact
Gets/sets the boolean Compact message property on the Input and Output HL7 messages. Strips any empty trailing field, component, subcomponent, or repetition separators from the message when True. Empty trailing separators serve no purpose unless dealing with ADD segments. The default is True (or is set by preferences in HL7Script.exe).
__CSVNewLine
When processing a LOOP CSV this value is used to represent a line break in the event a multi-line quoted field is encountered. Default = "\.br\". (See llCSV.ParseCSVLine for details)
__Debug
Boolean, default is False. When True, each script line is logged as it is evaluated in addition to other helpful information about comparisons and flow control.
__DebugData
Boolean, default is False. Logs every expression evaluated as data except quoted literals. This generates LOTS of logging!
__Epsilon
Numeric, default=0. When comparing floating point numbers for equality, Epsilon can be set to a non-zero value (like 0.00001) that allows the two values to differ ever so slightly and still be considered equal.
__FileEncoding
Sets the encoding used for FILE output. Setting the value to blank or "DEFAULT" selects Windows' current default codepage. Setting it to "COPYINPUT" copies the encoding currently being used by the input HL7 message. Any other value (e.g. "UTF-8", "1251", etc.) is used as-is. Providing an invalid encoding name will raise an exception. When reading the value, it always returns the numeric code page for the selected encoding.
__FileEOL
The end-of-line character(s) used for OUTPUT. The default is CRLF. Typically, this would be set by using the H@ data modifier on a hex string value, such as SET __FileEOL=H@"0D0A".
__FilePos
Gets/sets the byte position of the currently open FILE. Assign the quoted literal "EOF" to move the position to the end of the file for appending data. Referencing __FilePos without an open file will raise an error.
__ForceField
Gets/sets the boolean ForceField message property on the Input and Output HL7 messages. When True, each segment always has a trailing field separator. The default is False or is set by preferences.
__InputMsgStr
The input HL7 message's MessageStr property with standard 0x0D (CR) segment terminators. Set the variable to replace the input message. This could be handy when using non-HL7 input to construct a message and then be able to LOOP over the segments, etc.
__InputMsgText
The input HL7 message's MessageText property with 0x0D0A (CRLF) at the end of each segment. When set, it works the same as __InputMsgStr and both will accept either CR or CRLF line endings.
__LowHexOnly
Gets/sets the boolean LowHexOnly message property on the Input and Output HL7 messages. By default (False), 8-bit (0x80 and up) characters are escaped only when the MSH.18 character set value is blank or "ASCII". When True, 8-bit characters will be ignored in hex escaping regardless of the MSH.18 character set value. Note that when __MinimalHex is False and low binary characters (<0x20) are present, the entire subcomponent is still hex escaped.
__MaxLoops
Numeric, default=1000. The maximum number of loop iterations allowed. This prevents an endless loop from hanging the program by throwing an exception when it exceeds this number. There are certain times when this setting is relaxed, such as while processing a LOOP QUERY or LOOP TEXT/CSV where there is a predictable end to the loop.
__MinimalHex
Gets/sets the boolean MinimalHex message property on the Input and Output HL7 messages. When True, as little hex escaping as possible is done, limited to just the characters that require it. When False, the entire subcomponent is hex escaped if any part of it requires encoding. The default is True or is set in preferences.
__MsgStartValues
Gets/sets the comma-delimited list of values that identify the start of a new HL7 message in a multi-message file. The default value is "MSH|,FHS|,BHS|,BTS|,FTS|". Used by the HL7 LOADNEXT command.
__NamedFields
Gets/sets the name of the Named Fields file for use in parsing named field keys and the VALIDATE function. Changing the Named Fields file is persistent, even across intervals; it will remain assigned until changed to something else or the service is stopped. Setting it to blank will unassign a Named Fields file.
__NamedFieldsDef
This works just like __NamedFields, but instead of a filename, it gets or sets the definition itself as a string. This would allow the named fields definition to be retrieved from a database, for example.
__OutputMsgStr
The output HL7 message's MessageStr property with standard 0x0D (CR) segment terminators. Set the variable to replace the output message. Setting it to blank is the same as using HL7 CLEAR.
__OutputMsgText
The output HL7 message's MessageText property with 0x0D0A (CRLF) at the end of each segment. When set, it works the same as __OutputMsgStr and both will accept either CR or CRLF line endings.
__Strict
Gets/sets the boolean Strict message property on the Input and Output HL7 messages. When True, message syntax during parsing is more strictly enforced. Defaults to True or is set in preferences.
__StrictNum
The boolean __StrictNum setting determines what happens when converting a string to a number when the string isn't a valid numeric value (blank, "foo", HL7 null, etc.). When True (the default) an exception is raised, halting execution of the script. If set to False, the converted value will instead be silently defaulted to zero and script processing will continue. See also: The INTDEF and FLOATDEF script functions.
__TZLocal
The local timezone in +/-HHMM format. Defaults to whatever Windows is currently set to.
__TZSender
The current input message's default timezone in +/-HHMM format. The value is reevaluated for each input message. If a message contains a timezone in MSH.7, that value is used. Otherwise, it is set to __TZSenderDefault.
__TZSenderDefault
The default timezone to use for all input messages that don't provide their own default in MSH.7. Defaults to whatever Windows is currently set to.

Return to Top

Functions

There are a number of built-in functions in the HL7Script language. Functions accept one or more arguments and return a value. In HL7Script that value is always a string, just like variables.

To call a function, enclose the function name and comma-separated arguments inside square brackets.

SET $VAR = [LEFT, 3, "foobar"] ; Sets $VAR to "foo"

Function names are case-insensitive. Whitespace between arguments is ignored, so use a space after commas if preferred.

The function name itself must be the first value, and does not need to be quoted (but may be). In fact, many values within the square brackets may be left unquoted as long as they are unambiguous constants, like numbers or a string with no spaces or punctuation.

Variables, HL7 keys, and other expressions will be evaluated before being passed to the function. If a function expects an HL7 key or variable name, you must quote the value so it remains unchanged.

; As expected, this returns the number of components in MSH.9 (probably 2)
SET $VAR = [COMCOUNT, "MSH.9"] ; OK

; This fails because the value of MSH.9.1 is probably "ADT" or "ORU", etc.
SET $VAR = [COMCOUNT, MSH.9] ; Invalid HL7 key: "ADT"

In addition to setting variables directly, functions can be used as part of complex expressions that include concatenation or other nested function calls.

IF [LEN, PID.13.1] #> "9"
    SET $PHONE=[FORMATDIGITS, "(099)999-9999", [DIGITS, PID.13.1]]
END

Function Reference

The functions that take date and/or time arguments expect them to be in HL7 format or the string constant "NOW" for the current system date/time. Blank or null input is interpreted as a "zero" date (1899-12-30 00:00:00). Supplying an invalid date will typically raise an error.

Arguments shown in [square brackets] are optional. A default value for a missing argument is shown with an equal sign and value after the name. Mutually exclusive options are separated with a vertical pipe character (This|That|Other).

Notice that many of the numeric functions include an optional "modifier" argument that is added to the result. This is a convenience to save you a separate call to MATH or INC/DEC. The modifier may be negative.

AGE,dob[,ondate=NOW]
Returns a person's age in years between the date of birth and a second date which defaults to "NOW" for the current date.
CLEAN,string[,allowedchars=@@##]
Returns the string stripped of any characters not in the allowed character list. If allowedchars is not provided, it defaults to case-insensitive alphanumeric (A-Z and 0-9). See llStrings.CharsOnly for more information.
COALESCE,string1,string2[,string3...]
Returns the first non-null argument. If all of the arguments are null, the function returns HL7 null (""). If you want to return the first non-blank argument, prefix the arguments with the N@ Null-if-Blank modifier.
COMCOUNT[OUT],fieldkey[,modifier=0]
Returns the count of components in the specified field/repetition from the current input HL7 message. The optional modifier is added to the result. If the "OUT" variation is used, it works on the output message instead.
CSVFIELD,index
When inside a CSV loop, it returns the value of the csv field at the given index. The first field is index zero. The $CSVCOUNT section variable holds the count of fields currently available. Calling CSVFIELD with an out-of-range index will simply return a blank. Attempting to call CSVFIELD outside of a CSV loop will raise an exception. Equivalent to the I@ data modifier.
DATEDEF,default,string
If the string is a valid HL7 date or datetime value, it is returned as-is. If not, the default value is returned. A valid date can range anywhere from just the year (yyyy) up to milliseconds, and may include a time zone.
DATEDIFF,precision,startdatetime,enddatetime
Calculates the difference between two date/time values as an integer in the desired unit of precision (YMDHNSZ). Any fractional part of a unit is truncated. DATEDIFF ignores time zones, so convert before calling if needed.
SET $YEARS=[DATEDIFF, Y, 20190101, 20200101] ; 1 year
SET $DAYS=[DATEDIFF, D, 20190101, 202001011500] ; 365 days, ignores the time difference
DATEMATH,precision,datetime,part1,value1[,part2,value2...]
Performs date math on the given value and formats the result as an HL7 date/time string using the given precision (YMDHNSZ). If you want a time only, add a "T" to the precision (e.g. "ST"). The same characters used for the precision are used for the parts. Ignores time zones. See llDates.DateMath for more information.
; Add two months and three days to a date
SET $VAR=[DATEMATH, D, 20150125, M, 2, D, 3] ; Returns "20150328"
; Add three and a half hours and return just the time
SET $VAR=[DATEMATH, ST, 20161229081445, H, 3, N, 30] ; Returns "114445"
DIGITS,string
Returns only the numeric digits from the string. Handy for stripping any punctuation from phone numbers and SSNs. The 9@ data modifier does the same thing.
EXECUTE,HIDE|SHOW|MAXIMIZE|MINIMIZE,WAIT|NOWAIT|ms,command
Executes an external command or program. The first argument determines how the process appears using the options HIDE, SHOW normally, MAXIMIZE, or MINIMIZE. The second argument controls whether the script will WAIT indefinitely for it to finish, launch it and immediately continue (NOWAIT), or wait a specific number of milliseconds. The third argument is the command itself.
The function returns the exit code of the process or the error code returned by CreateProcess should it fail to launch. A return value of "0" typically indicates success. If the timeout period expires, the return value will be the string "WAIT_TIMEOUT".
; Use fc to compare two files for equality.
; Five seconds should be plenty of time for two small text files.
SET $FC = [EXECUTE, HIDE, 5000, "fc /L foo.txt bar.txt > nul"]
IF $FC == "0"
  ; FC sets an exit code of zero when the files match
  LOG "The files match"
ELSEIF $FC == "1"
  ; It sets an exit code of 1 when they do not
  LOG "The files differ"
ELSE
  ERROR "Error launching FC: " + $FC
END
EXISTS,filename
Returns "1" if the specified file exists, "0" if it does not. You may also check against wildcards (e.g. *.txt). The J@ data modifier is a shortcut for this function.
FIELDCOUNT[OUT],segmentkey[,modifier=0]
Returns the count of fields in the specified segment from the current input HL7 message, plus the optional modifier. The count excludes the segment ID itself, which internally is stored as field zero. If the "OUT" variation is used, it works on the output message instead.
FILEPART,part,filename
Returns part of a filename. The available part values are as follows, and include example output in parentheses given a filename of "D:\Path\File.ext":
D=Directory (D:\Path)
P=Path (D:\Path\)
F=Filename (File.ext)
N=Name only (File)
E=Extension with dot (.ext)
X=eXtension without dot (ext)
S=Strip extension (D:\Path\File)
V=Volume/Drive (D:)
FLOATDEF,default,string[,modifier=0]
If the string is a valid numeric value, it is returned. If not, the default value is returned. The optional modifier is added to whichever value is returned. The return value will be formatted with a number of decimal places equal to the most precise of all the arguments.
FORMATDATE,formatstring,datetime
Formats a date/time value using the llDates.FormatDateTimeEx function (which is Delphi's FormatDateTime with a few of my own extensions).
SET $ANSIDATE=[FORMATDATE, "yyyy-mm-dd", D@"NOW"] ; Returns "2017-02-21"
FORMATDIGITS,formatstring,digits[,escapechar]
Right-justifies/overlays a string of characters (usually digits) into a format string. Especially handy for phone number/SSN formatting, but it could conceivably be used on any type of input. See llStrings.FormatDigits for more information.
SET $TEST=[FORMATDIGITS, "(099)999-9999", "6025551212"] ; Returns "(602)555-1212"
SET $TEST=[FORMATDIGITS, "(099)999-9999", "5551212"]    ; Returns "555-1212"
SET $TEST=[FORMATDIGITS, "999.999.9999", "6025551212"]  ; Returns 602.555.1212
SET $TEST=[FORMATDIGITS, "bar", "foo"]                  ; Returns "foobar"
FPMATH,decimals,operator,num1,num2
Performs floating-point math (+-*/^) on the two numbers. The result is rounded to and formatted with the specified number of decimal places. This can also be used to round a value to a desired number of decimal places by just adding zero to it. Specifying zero decimals will truncate the result to an integer and return it without a decimal portion. The second number must be an integer when using exponentiation (^).
INTDEF,default,string[,modifier=0]
If the string is a valid integer, it is returned. If not, the default value is returned. The optional modifier is added to whichever value is returned.
KEYEXISTS[OUT],fieldkey
Returns "1" if the location specified by the given key exists in the input message, "0" if it does not. If the "OUT" variation is used, it works on the output message instead.
LEFT,count,string
Returns the leftmost count of characters from the string. If the count is longer than the string, the entire string is returned. If count is negative, the leftmost length+count characters are returned.
SET $VAR=[LEFT, 3, "foobar"]  ; Returns "foo"
SET $VAR=[LEFT, -1, "foobar"] ; Returns "fooba"
LEN,string[,modifier=0]
Returns the length of the string plus the optional modifier.
MATH,operator,num1,num2
Performs integer math (+-*/%^) on the two numbers. Remember that integer division discards any remainder, so 5 / 2 = 2. To get the remainder you can use the modulo (%) operator: 5 % 2 = 1.
PADC,count,string[,padchar=" "] (also PADL, PADR)
Pads the string evenly on both sides to the requested length. The PADL and PADR variations pad only the left or right side of the string, respectively. The pad character defaults to a space. If the count is smaller than the size of the string, the string is shortened. If the count argument is negative, only strings shorter than the desired length will be padded; longer strings will be unchanged.
PATHJOIN,part1,part2[,part3...]
Combines parts of a path or filename, making sure each part is properly separated with a backslash. End with a blank argument if you want the final result to end with a backslash.
SET $VAR=[PATHJOIN, "D:", "Path", "File.ext"] ; Returns "D:\Path\File.ext"
SET $VAR=[PATHJOIN, "C:\DIR", ]               ; Returns "C:\DIR\"
POS,substring,string[,modifier=0]
Returns the position of the first occurrence of the substring within the string or zero if not found. The optional modifier is added to the result. The first character in a string is position one (1). POS is case-sensitive.
POSEX,substring,string[,offset=1[,modifier=0]]
Works just like POS but accepts an offset value for where to start searching. Returns the position of the next occurrence of the substring on or after the offset position. It returns zero if not found. The optional modifier is added to the result. POSEX is case-sensitive.
QUERYFIELD,fieldname|index
When inside a LOOP QUERY, use QUERYFIELD to reference the fields from the current query row. The single argument may be a field name or a 0-based numeric field index. If the requested field does not exist or the index is out of range, an exception will be raised. Equivalent to the Y@ data modifier.
RANDOM,min,max[,modifier=0]
Generates a random integer in the range of min..max inclusive. The optional modifier is added to the result.
REPCOUNT[OUT],fieldkey[,modifier=0]
Returns the count of repetitions in the specified field from the current input HL7 message plus the optional modifier. If the "OUT" variation is used, it works on the output message instead.
REPLACE,string,old,new (also IREPLACE)
Replaces all instances of the old value with the new in a string. The replace is done case-sensitively unless the IREPLACE variation of the function is used.
RIGHT,count,string
Returns the rightmost count of characters from the string. If the count is longer than the string, the entire string is returned. If count is negative, the rightmost length+count characters are returned.
SEGINDEX[OUT],segmentkey|fieldkey
Given a segment or field key, returns the 0-based segment index within the current input message, or blank if not present. Handy for LOOP SEGMENTS ranges. If the "OUT" variation is used, it works on the output message instead.
SEGKEY[OUT],segmentindex
Given a 0-based segment index for the input message, it returns the segment key (SID#q). The segment sequence is always included, even if #1. If the index is blank or out of range, an empty string is returned. If the "OUT" variation is used, it works on the output message instead.
SUBCOUNT[OUT],fieldkey[,modifier=0]
Returns the count of subcomponents in the specified field/component from the current input HL7 message plus the optional modifier. Returns zero if the field is blank or does not exist. If the "OUT" variation is used, it works on the output message instead.
SUBSTR,string,start[,count=MaxInt]
Returns a substring of the string starting at the specified character. If the count of characters to return is not specified, the remainder of the string is returned. Returns blank if the start index is out of range.
TIMEDEF,default,string
If the string is a valid HL7 time value (data type TM), it is returned. If not, the default value is returned. A valid time can range anywhere from just an hour up to milliseconds, and may include a time zone.
TRANSLATE,table,value[,default=""]
Looks up a value in a translation table and returns its translation. The table's definition determines its case sensitivity. If the value is not found, the default value is returned. An error is raised if the requested translation table has not been defined.
TRIM,string (also LTRIM, RTRIM)
Trims whitespace from both sides of a string. The LTRIM and RTRIM variations trim only the left or right side of the string, respectively. The P@ data modifier does the same thing as TRIM.
TZCONVERT,datetime[,fromTZ=__TZSender​[,toTZ=__TZLocal​[,includeOffset=1]]]
Converts a date/time value from one timezone to another, defaulting to converting from the sender's timezone to the local timezone. If the datetime value includes its own timezone, that is used instead of the fromTZ value. The format/precision of the output will match that of the input. The boolean includeOffset argument determines whether the output value includes the final timezone.
UNIQUEFILENAME,filename
Used to get a filename that is known not to exist. If the given filename does not exist, the name is returned unchanged. If it does exist, a two-digit (or more) number is appended to the end and incremented until it finds a name that does not exist and returns that.
VALIDATE,INPUT|OUTPUT[,REQUIRED][,REPEAT][,LENGTH][,DATATYPE][,TABLE]
If a Named Fields file is assigned (see the __NamedFields built-in variable), you can validate a message against the field definitions. The first argument determines which message is validated, INPUT or OUTPUT (or just I or O). The other optional arguments are strings indicating what to validate (only the first three characters are needed). If no options are provided, all are validated. The function returns blank if the message passes validation, or a comma-separated list of errors if it fails. If no Named Fields file is assigned, VALIDATE always returns blank. Note that only numeric and date/time data types can truly be validated without context. Everything else is essentially just a string.
; Validate only required fields in the input message:
SET $ERR=[VALIDATE, INPUT, REQ]
IF $ERR <> ""
  LOG "Input message is missing required fields: " + $ERR
END
VARINDEX,varname
Returns the 0-based variable index of the variable with the given name. If the variable name is blank or not defined, it returns blank.
VARNAME,varindex
Returns the name of the variable defined at the given index or blank if the index is blank or out of range.
VARVALUE,varindex
Returns the value of the variable defined at the given index or blank if the index is blank or out of range.

Return to Top

Data Modifiers

Modifiers are placed in front of other expressions (literals or data) and serve to change it in some way, like a little inline function with a single argument. Modifiers are a single character followed by an at-sign (@). More than one modifier may be used in a row. If more than one is attached to a value, the modifiers are applied right-to-left, as in the following example:

; If PID.5.1 is null change it to blank (B@),
; trim any spaces (P@), then upper-case the value (U@).
SET $X=U@P@B@PID.5.1

A modifier applies only to the value it is directly attached to. In other words, a modifier does not cross a concatenation boundary (+). In the following example, only "foo" would be made upper-case, not "bar", resulting in the variable $X being set to "FOObar":

SET $X=U@"foo" + "bar" ; Result is "FOObar"

Use parentheses to extend what a modifier applies to. Place an open paren directly after the at-sign (no spaces!) and the closing paren after the last value you wish to affect. In the following example, the entire value would be made upper-case, resulting in $X being set to "FOOBAR":

SET $X=U@("foo" + "bar") ; Result is "FOOBAR"

All of the modifiers that work on date/time values (D@, M@, S@, T@, Z@) expect the value to be an HL7 date/time value (HL7 data types DT, DTM, TM, or TS) or the quoted literal "NOW" for the current system time. Blank or null input is unchanged by the date/time modifiers.

Modifier Reference

A@ - ASCII
The Delphi-style ASCII notation (e.g. "#13#10") that follows is converted into a string.
B@ - Blank-if-Null
If the value is HL7 null (""), change it to a blank string.
C@ - Call function
An alternative way to call a script function. The arguments are pre-evaluated and put into a single string in the format of a delimited list like so:

FUNCTIONNAME,argument[,argument...]

The first non-alphanumeric character in the string becomes the list delimiter, which is important if any of the arguments may contain commas. The first value is the function name, and any subsequent values are the arguments to that function. Examples:
SET $DIFF=C@("MATH,-," + ZA1.3 + "," + ZA1.4) ; Subtract ZA1.4 from ZA1.3
SET $NEWID=C@("PADL,10," + PV1.3 + ",0") ; Pad the MRN with leading zeros
SET $DIAGTEXT=C@("LEFT|100|" + DG1.3.2) ; First 100 characters of data that may contain commas
See the Function Reference for a complete list of HL7Script functions.
D@ - Date
Formats a date/time value as an HL7 date without time. Example: D@"20150409153321" becomes "20150409"
E@ - Enquote
Enquotes the value in single quotes, doubling any internal quotes. Particularly useful for SQL queries.
SET $PK=Q@("SELECT id FROM patients WHERE mrn = " + E@$MRN)
F@ - FieldStr
Returns the FieldStr property for the requested field key. The FieldStr is the entire field as it appears in the message, with all separators and escaping still present. For example, F@MSH.9 would get you something like "ADT^A08".
G@ - SegmentStr
Returns the entire SegmentStr for the requested segment key, with all separators and escaping intact.
H@ - Hex
The hex value that follows is converted into a string. Case does not matter.
SET __FileEOL=H@"0D0A"
I@ - Index CSVFIELD
A shortcut for the CSVFIELD function. Put I@ in front of a 0-based numeric index to retrieve the specified column value while inside a CSV loop.
J@ - Exists
A shortcut for the EXISTS script function. Put J@ in front of a filename or wildcard, and it returns "1" if it exists, "0" if it does not.
K@ - Key
The string expression following K@ is evaluated as field key, retrieving the specified value from the current input HL7 message.
SET $PATIENTTYPE=K@("PV1#"+$COUNTER+".18.1")
L@ - Lower-case
Change the data to lower-case.
M@ - Minutes
Converts the time portion of a date/time value into an integer number of minutes since midnight. Could be useful for appointment durations in SCH or AIS segments.
N@ - Null-if-Blank
If the value is blank, change it to an HL7 null ("").
O@ - Output key
Works like K@ where it expects a field key, but gets the data from the output HL7 message rather than the input message.
P@ - Prune
"Prune" the value by trimming leading and trailing spaces. A shortcut for the TRIM script function.
Q@ - Query
Followed by a SQL expression, it runs a simple lookup query that is expected to return a single value. The first field from the first row of the results is returned as a string. If the query returns no results, a blank is returned. It also sets the $ROWSAFFECTED section variable with the number of rows actually returned by the query. Requires a database connection.
R@ - RepetitionStr
Returns the entire RepetitionStr for the requested field key. For example, R@PID.3~1 would get you the first patient identifier repetition, such as "12345^^MRN".
S@ - Seconds
Formats a date/time value as an HL7 timestamp with a precision of seconds. Example: S@"NOW" -> "20150409153345"
T@ - Time
Formats a date/time value as an HL7 time-only value with a precision of seconds. Example: T@"20150416081422.3250" -> "081422"
U@ - Upper-case
Change the data to upper-case.
V@ - Variable value
Precedes an expression or unquoted literal variable name to retrieve said variable's value. Examples: V@("%" + PID.3.1), V@$FOO.
SET $FOO = "I am foo"
SET $BAR = "$FOO"
LOG "$FOO = " + $FOO ; "I am foo"
LOG "$BAR = " + $BAR ; "$FOO"
LOG "V@$BAR without parens: " + V@$BAR   ; "$FOO"  Same as without V@
LOG "V@($BAR) with parens:  " + V@($BAR) ; "I am foo"  Oooh, tricky!
LOG "V@V@$BAR double-dereference: " + V@V@$BAR ; "I am foo"  Same as V@()
W@ - Double Quote
Enquotes the value in double quotes, doubling any internal quotes.
X@ - EXEC SQL
Followed by a SQL expression, it executes SQL that expects no results, such as an INSERT, UPDATE, or EXEC of a stored procedure. A blank is returned on success, otherwise an error message. It also sets the $ROWSAFFECTED section variable with the number of rows affected by the SQL. Requires a database connection.
Y@ - querY field
A shortcut for the QUERYFIELD script function. When inside a LOOP QUERY, use the Y@ modifier to reference the fields from the current query row. The value after Y@ may be a field name or a 0-based numeric field index. If the requested field name does not exist or the index is out of range, an exception will be raised.
Z@ - Milliseconds
Formats a date/time value as an HL7 timestamp with millisecond precision. Example: Z@"NOW" -> "20150409153345.5210"
9@ - Digits
A shortcut for the DIGITS script function. Removes all characters except numeric digits 0-9.
0@ - Noop
Zero is a "noop" (no operation) modifier and does nothing to the data. Left over from testing, it might come in handy someday...

Return to Top

Script Flow Control

The main script is processed from top to bottom for each input message. Within the script, you can branch and loop based on what is contained in the message. All such flow control is done using IF and LOOP blocks.

The IF or LOOP keyword starts a block. The keyword is followed by the condition(s) of the block (see below).

ELSEIF and ELSE are optional parts of an IF block. They are not valid in a LOOP block.

The END keyword ends the block started by the nearest IF or LOOP line. Blocks can be nested with only a few restrictions.

All LOOP/IF/ELSEIF/ELSE/END statements must appear on a line by themselves.

IF Blocks

IF blocks work like they do in most programming languages. The IF keyword is followed by a boolean expression that is evaluated for a true or false answer. If true, the statements following the IF line are processed until an ELSEIF, ELSE, or END is encountered. If false, control passes to any ELSEIF or ELSE sections. If there are none, the script continues with the statement following the END.

The ELSEIF keyword requires a boolean expression just like IF. If present, ELSEIF must appear after the IF, and before ELSE and/or END. You may have as many ELSEIF sections as needed. When the main IF expression is false, the first ELSEIF expression to evaluate to true is run. An ELSEIF section ends when another ELSEIF, ELSE, or END is reached.

The ELSE section must always be the last thing before END when present, and only one ELSE is allowed. The ELSE statements are run only when none of the IF or ELSEIF sections are found to be true.

IF blocks may be nested without restrictions.

; Set the discharge date.
; If the message is not an ADT^A03 messages, make it blank. Otherwise,
; use PV1.45 if present, MSH.7 if present, or finally the current time.
IF MSH.9.1=="ADT" AND MSH.9.2=="A03"
  IF B@PV1.45.1<>""
    SET $DCDATE=PV1.45.1
  ELSEIF B@MSH.7<>""
    SET $DCDATE=MSH.7
  ELSE
    SET $DCDATE=S@"NOW"
  END
ELSE
  SET $DCDATE=""
END

Boolean expressions are joined logically by AND, OR, and XOR. You may also prefix expressions with NOT to flip the true/false value. Parentheses are fully supported.

In the absence of parentheses, the unary NOT operator has precedence and is applied to the expression on its right first. Then, logical operators are evaluated from left to right. The expression "1 OR NOT 0 AND 1" evaluates as if it were written as "(1 OR (NOT 0)) AND 1". Tip: Use parentheses to avoid ambiguity.

The expressions in IF and ELSEIF statements require some kind of comparison to take place to generate a boolean result. You may specify any of a number of operators (all of which are two characters in length for ease of parsing) when comparing values:


Comparison Operators
==equal
<>not equal
>>greater than
<<less than
>=greater than or equal
<=less than or equal
$$contains substring
*=appears in list
~=starts with
=~ends with
~~LIKE pattern matching
#=numerically equal
#>numerically greater than
#<numerically less than

Whitespace around the operator is supported. The following three examples are all logically identical:

IF PID.3<>""
IF PID.3 <> ""
IF NOT PID.3 == ""

All of the string comparisons are case-sensitive. If you want to do a case-insensitive comparison, use the U@ or L@ data modifiers on both sides to make sure the case of the strings are equal case.

Nulls ("") are not considered equal to blanks in these comparisons. If you want to test blanks against nulls, prefix one or both data elements with one of the B@ Blank-if-Null or N@ Null-if-Blank data modifiers.

The numeric comparisons convert the left and right values to numbers for the comparison. If either value contains a decimal point, the numbers are compared as floating point numbers. When comparing floating point numbers for equality with the #= operator, the __Epsilon built-in variable can be set to allow slight differences to still be considered equal. If a value is not a valid number, an exception is raised (by default, see __StrictNum). The INTDEF and FLOATDEF script functions can be used to give a valid default value to possibly invalid numeric data.

The $$ "contains" operator can also be thought of as meaning "has $ub$tring". If the value on the left contains the value on the right, the expression evaluates to true.

The *= "appears in list" operator compares the value on the left to a string on the right that contains a delimited list of values. It returns true if the left side value is one of the values in the list. The first character in the list string indicates the delimiter value (it must be non-alphanumeric) so you can use something besides a comma if needed.

;Do something if PV1.10 is "FOO", "BAR", "AS IF" or "MEH"
IF PV1.10 *= ",FOO,BAR,AS IF,MEH"
  ;...
END

The ~~ LIKE pattern matching operator compares the value on the left to the pattern on the right and returns true if they match. The pattern uses the default "%" for any-character matching, "_" for single-character matching, and "\" as the escape character. A blank pattern will always return a false result. See the llLike.TLikePattern class for more info.

"AnyKey" Matching

HL7 keys on the left side of any comparison expression may have one of the indexes replaced by an asterisk wildcard (*) in order to compare against values in any matching message element.

The following example will evaluate to True if any DG1 segment has the letter "A" in field 6:

IF DG1#*.6 == "A"
  LOG "There is an admitting diagnosis in this message!"
END

The asterisk can be in any index position, but only one asterisk may appear in a key at a time. The most common uses would be in the segment sequence or repetition index positions. It is worth noting again that only keys on the left side of the comparison operator are checked for asterisks. It must be a lone HL7 key and must not include any quoted literals or concatenation. Modifiers are allowed, like U@ or L@.

This feature was developed primarily for use by HL7Viewer's filtering and find features, but it could save you a line or two of code in a script.

An AnyKey wildcard will keep incrementing the number in the asterisk index and doing comparisons until the comparison becomes true or the key is found not to exist three consecutive times. Only then does it assume there is no more data to check and stops trying.

Each comparison in an expression is checked separately. Trying to do something like the following with two AnyKey matches in the same IF statement does not work like you might think:

IF DG1#*.6 == "A" AND DG1#*.15 == "1"
  LOG "There is a primary admitting diagnosis in this message!" ; Not necessarily true!
END

It could not be assumed that the DG1.6 "A" and DG1.15 "1" were actually found in the same segment. To do that properly, you would want to use a LOOP...

LOOP Blocks

LOOP blocks repeat a series of commands as long as the loop condition remains true. That condition varies based on what kind of loop is desired.

Single question marks (?) within the loop block are replaced by the 1-based numeric index on each loop. Double question marks (??) are replaced with the 0-based index. For example, the first time through the loop, "??" would be replaced by "0", and "?" would become "1". On the next loop, "??" would be "1" and "?" would be "2", etc.

Many loop types may be nested. In nested loops, only the original condition from the outer loop (e.g. IN1#? or PID#1.3~?) is replaced inside the nested loops. Other inner loop question marks receive their value from their own loop counter.

; Nested loop example
LOOP OBX
  LOG "?" ; This gets its value from the outer loop
  LOOP OBX#?.5 ; OBX#? gets replaced by the outer loop
    LOG " ? " + OBX#?.5~?.1 ; OBX#? from outer, other ?s from inner
  END
END

To prevent runaway scripts, there is a maximum loop count property. If a loop exceeds this number of iterations, an exception will be raised. The default value is 1000. This value can be read/written in the script using the __MaxLoops built-in variable.

BREAK and CONTINUE

BREAK is used to break out of a loop early. It can be followed by a number to indicate how many levels of LOOP nesting should be broken out of. If no value is specified, one (1) is assumed. Trivia: BREAK 0 is effectively just a jump to the nearest END statement.

CONTINUE is used to skip the rest of the current loop iteration and move on to the next without breaking the loop completely.

BREAK and CONTINUE must appear on lines by themselves.

Expression Loops

An expression loop will repeat so long as the expression following LOOP evaluates to True. The expression is the same as one that would be used in an IF statement and is identified by the presence of one or more of the comparison operators. This could be considered the equivalent of a WHILE loop in many programming languages.

SET $FOO = "0"
LOOP $FOO #< "3"
  LOG "$FOO=" + $FOO
  INC $FOO
END

Segment ID Loops

To iterate over a certain type of segment in the input HL7 message, use a 3-character segment ID after LOOP. For example, LOOP DG1. The loop will be run once for each segment matching the given segment ID. If no such segment exists, the loop will not be entered.

Inside the loop use the segment ID with a question mark segment sequence to represent the current segment, e.g. DG1#?.

The following example is the correct way to find out if there is a primary admitting diagnosis in a message, as compared to the AnyKey anti-example that would not work as desired:

LOOP DG1
  IF DG1#?.6 == "A" AND DG1#?.15 == "1"
    LOG "There is a primary admitting diagnosis in DG1#?" ; This is true!
  END
END

Segment Index Loops

To loop through the segments of the input message based on position rather than type, use LOOP SEGMENTS.

By default, all segments of the message are included. If you want only a subset of the segments, you may specify a range of segments to loop over in LOOP SEGMENTS [start[-end]] format. You may use numeric segment indexes or segment keys.

The first segment is segment zero (usually MSH). If no end segment index is given, the loop will process to the last segment. The segment range can come from data, for example:

LOOP SEGMENTS $FIRSTSEG + "-" + $LASTSEG

Use ### as the replacement value for the current segment within a SEGMENTS loop. It will be replaced with the segment ID and sequence, e.g. "MSH#1" or "IN1#3". The regular ? and ?? replacements are also available, and ?? matches the current zero-based segment index.

; Copy every segment in the message except "Z" segments
HL7 CLEAR
LOOP SEGMENTS
  IF [LEFT, 1, ###.0] <> "Z"
    HL7 COPYSEG ###
  END
END

SEGMENTS loops may be nested with other loops including other SEGMENTS loops. The ### and question mark replacements always come from their innermost parent loop.

Field Repetition Loops

To iterate over a repeating field, use a field key as the loop condition: LOOP PID#1.3. The loop is run once for each repetition of the given field, or skipped if the field is not present. A field that is present but blank has one (blank) repetition.

Within the loop, use a question mark after the repetition separator to represent the current repetition, e.g. PID#1.3~?. If a field repetition loop will be nested (outer or inner) you must include the segment sequence index (e.g. #1) in the field key.

; This example outputs each id of the repeating patient identifier list:
LOOP PID#1.3
  OUTPUT PID#1.3~?.1
END

Text and CSV Loops

Use LOOP TEXT or LOOP CSV to read a text file and loop over each line of the file. Follow TEXT or CSV with a space and the name of the file to be read. This could be especially handy when using non-HL7 files as input.

If the filename is blank or otherwise not found, it is not considered an error. It will be logged and the loop will simply be skipped.

The section variable $LOOPTEXT is set to the value of the current line at the start of each loop. When using LOOP CSV, the line is also parsed as csv data. The section variable $CSVCOUNT contains the number of fields parsed from the current line, and you can call the CSVFIELD script function (or use the I@ data modifier) to retrieve the values of those fields. If the line is blank, $CSVCOUNT will be zero. If the line fails to parse, $CSVCOUNT will be set to "-1".

CSV loops handle multi-line quoted fields. If a line ends with an unterminated quoted field, the next line will be read and continue to be parsed into fields and appended to $LOOPTEXT. The line break itself is replaced with the value of the __CSVNewLine built-in variable.

; Example with a non-HL7 text or csv file as input
LOOP CSV __InFile
  LOG "Line ?: " + $LOOPTEXT
  ; The IF allows testing LOOP TEXT or LOOP CSV
  IF $CSVCOUNT <> ""
    LOG "Field count: " + $CSVCOUNT
    SET $X="0"
    LOOP $X #< $CSVCOUNT
      SET $FIELDVALUE=[CSVFIELD, $X] ; Could also use I@$X
      LOG "Field " + $X + ": " + $FIELDVALUE
      INC $X
    END
    LOG ""
  END
END

Since the number of lines in a text file may be considerably higher than the normal setting for __MaxLoops, that limit is not enforced when using one of these loops.

TEXT/CSV loops may not be nested inside other TEXT/CSV loops. $LOOPTEXT and $CSVCOUNT are cleared when the loop ends.

Query Loops

Use LOOP QUERY to perform a database query and loop over the rows it returns. Follow LOOP QUERY with an expression containing the query SQL. A database connection must be defined to use a query loop.

Use the LOOP BIGQUERY variation when the query is expected to return a very large result set. It fetches the set in chunks rather than all at once. The default of all at once is generally faster and more efficient, but could use a lot of memory on a very large set.

Within the loop, use the Y@ data modifier or the QUERYFIELD script function to retrieve the fields from the current row. They can access a field by name or by position within the select list, with the first field being index zero.

LOOP QUERY "SELECT id, value FROM sometable WHERE foo = "+E@$FOO
  ; The Y@ modifier or QUERYFIELD function get your query fields, by name or index.
  LOG "Row ?: ID=" + Y@"id" + " Value=" + [QUERYFIELD, "value"]
END

The $ROWSAFFECTED section variable is set to the number of rows returned by the query, and the $FIELDCOUNT variable is set to the number of fields in each row.

Like LOOP TEXT/CSV, query loops also do not enforce the __MaxLoops limit since a query has a finite but sometimes large number of rows returned.

Query loops may not be nested inside other query loops. Attempting to call QUERYFIELD or use the Y@ modifier outside of a query loop will raise an error.

QUIT, ABORT, and ERROR

QUIT can be specified at any point in the script to stop processing the script for the current message and move on to the next.

IF B@PV1.19 == ""
  QUIT
END

ABORT is similar, but stops processing ALL messages from the current input file, wildcard, or interval. It will also skip post-processing unless you choose to allow it.

ERROR works similarly to ABORT but actually raises an exception. The text of the error comes from the remainder of the line; ERROR must be followed by a space and an expression. An ERROR will always cause post-processing to be skipped, but will run the lines of the error processing section if present.

IF MSH.11 == "P"
  ERROR "Production messages in the Test system!"
END

CALL

You may call a user-defined procedure at any point in the script using the CALL command followed by the name of the procedure as defined in the initialization section. The procedure name can be an unquoted literal value or data. The lines of the procedure are run as if they were inserted into the script at that point and then the script continues where it left off.

Calling a procedure inside a loop does not perform any question mark or segment substitution on the lines of the procedure.

User-defined procedures accept no arguments and return no values, but they do have full read and write access to all variables when they are called. Variables that are set or modified during the procedure continue to exist after it finishes and are available to the rest of the script.

Here is an example of using variables within a procedure to mimic a function that accepts arguments and returns a value:

<INIT>
  PROCEDURE=TZEXTRACT
    /* Takes a date/time value and extracts the timezone if present.
     * Set $TZX_DATETIME as the input date/time value. This variable will be
     * updated by having the timezone removed from it.
     * The $TZX_TZ variable will contain the extracted timezone.
     */
    SET $TZX=[POS, "-", $TZX_DATETIME] ; Is there a minus?
    IF $TZX == "0" ; If not, how about a plus?
      SET $TZX=[POS, "+", $TZX_DATETIME]
    END
    IF $TZX == "0"
      SET $TZX_TZ="" ; The input does not contain a timezone
    ELSE
      SET $TZX_TZ=[SUBSTR, $TZX_DATETIME, $TZX]
      DEC $TZX
      SET $TZX_DATETIME=[SUBSTR, $TZX_DATETIME, 1, $TZX]
    END
  ENDPROC
</INIT>

SET $TZX_DATETIME=PV1.45 ; "20150427080000-0700"
CALL TZEXTRACT
LOG $TZX_DATETIME        ; "20150427080000"
LOG $TZX_TZ              ; "-0700"

SLEEP

Follow the SLEEP keyword with a number of milliseconds, and execution of the script will pause for approximately that length of time.

A good design should generally be able to avoid the addition of artificial delays, but SLEEP has come in handy for testing.

Do not SLEEP for long periods of time, as this can prevent the service from responding to stop/shutdown requests in a timely manner.

Return to Top

LOG Commands

The LOG command is used to write information to the log file (or screen when using the HL7Script program). The expression following LOG is evaluated and written to the log with a timestamp. The format of the timestamp is determined by program settings.

If you log an empty string (i.e. LOG "") a blank line will be added to the log without a timestamp for formatting purposes.

LOGWHEN is a way to do some conditional or trace logging without everything that goes along with the __Debug setting. The LOGWHEN command works just like LOG, but the second word on the line is an unquoted variable name. The log entry will only be made if that variable is currently defined (not blank). You can also use a very simple expression instead of a variable name, but it cannot contain any spaces or concatenation, and must resolve to the name of a variable in order to work.

LOG "This will always make it to the log."
SET $EXTRA=""
LOGWHEN $EXTRA "This will not get logged."
SET $EXTRA="1"
LOGWHEN $EXTRA "Extra logging is turned on!"

The following commands are also available to write formatted information to the log file:

LOGVARS [wildcard [NOPREFIX]]
Sorts the variables by name and then writes them all to the log in "Name=Value" format, one per line. If any wildcards are specified (see SAVEVARS for wildcard syntax), only the variables with names matching the wildcards are logged. Specifying NOPREFIX will strip the wildcard from the start of the variable names before logging them. LOGVARS only logs user variables, not built-in variables.
LOGSCRIPT
Dumps the entire script to the log with line numbers. Blank lines, comments, and whitespace will have been removed from the script during initialization.
LOGBLOCK
Like LOGSCRIPT, but only shows the lines from the current IF/LOOP block being processed. The LOOP, IF, ELSEIF, ELSE, and END lines themselves are not included.
LOGTRANSLATIONS
Dumps any translation tables to the log.

When using HL7ScriptService, all logging done in the script is considered to be at the default "Info" logging level.

Return to Top

Output and File Commands

OUTPUT works much like the LOG command. The expression following OUTPUT is evaluated and written to the current output file as a line of text. If no output file is available, output goes to the log instead.

; Write the value of $FOO to the output file
OUTPUT "FOO="+$FOO

The end-of-line characters used by OUTPUT are controlled by the __FileEOL built-in variable, and default to a carriage return and line feed (CRLF, 0x0D0A). If you are writing an HL7 output file, you would want to set it to a carriage return only.

The OUTPUTX command is the same as OUTPUT, but does not include a line terminator in case you need finer control over how your output file is constructed. If writing to the log, OUTPUTX works no differently than OUTPUT because the log controls the line endings.

The file's encoding is controlled by the __FileEncoding built-in variable, defaulting to the system's default codepage. Other values like "UTF-8" or "1251" can also be selected.

The file's current byte position can be read or changed using the __FilePos built-in variable.

; Change file output to use HL7 line terminators and UTF-8 encoding
SET __FileEOL=H@"0D"
SET __FileEncoding="UTF-8"

To prepare or manipulate the file used by OUTPUT, use one of the FILE commands:

FILE APPEND filename
Closes any currently open file and then opens the specified output file. If the file already exists it will be appended to, setting the file position to the end of the file.
FILE REWRITE filename
Closes any currently open file and then opens the specified output file. If the file already exists it will be truncated/overwritten.
FILE OPEN filename
Closes any currently open file and then opens the specified file, leaving the file position at the beginning of the file (after any byte order marker). Meant more for reading than writing, such as reading a multi-message HL7 file using the HL7 LOADNEXT command. The file must exist.
FILE CLOSE
Closes the currently open output file.
FILE DELETE [filename]
Deletes the specified file. If no filename is supplied, the currently open output file is closed and then deleted.
FILE COPY|MOVE|RENAME source=target
Copies/Moves/Renames the source file to the target filename. If the source file is currently open, it will be closed. COPY and MOVE will overwrite an existing target file. RENAME will fail if the target filename already exists.

If no explicit path is provided in the filenames, the current directory is assumed. This is the current directory of the application, not necessarily the input or script file. The __CurrentPath, __InPath, __OutPath, and __ScriptPath built-in variables may be of use in constructing filenames.

Only one output file may be open at a time. The file remains open until it is closed or the end of the current section.

If a FILE command fails, an exception will be raised.

Return to Top

Base64 Commands

The Base64 commands are used to load and save binary files, encoding or decoding the content in Base64 format. This can be especially handy if you are using non-HL7 input such as pdf files.

BASE64LOAD filename varname
Reads the specified file from disk, encodes the contents as Base64, and stores that encoded data in a variable.
; Load the input pdf file and store the data in an OBX segment:
BASE64LOAD __InFile $B64DATA
HL7 SET OBX.5=$B64DATA
SET $B64DATA="" ; Could be big - tidy up!
BASE64SAVE filename base64data
Decodes Base64 data and writes it to the specified file.
; Retrieve pdf data from an OBX segment and save it to disk:
SET $PDFFILE=__InPath + OBR.2 + ".pdf" ; Use the order number as the filename
BASE64SAVE $PDFFILE OBX.5

Return to Top

HL7 Commands

Often, a script will be used to output some or all of a set of input messages in a slightly different format. The script has an input HL7 message and an output HL7 message. To work with the output HL7 message, use HL7 commands.

Syntax: HL7 COMMAND [OPTIONS]

Commands available:

HL7 ADDSEG segmentString
Adds a segment to the output message. The data that follows the ADDSEG command is treated as a SegmentStr. It can be as small as just the segment ID or as detailed as needed. Example: HL7 ADDSEG G@PID
HL7 ANONYMIZE
Anonymizes the output message using the currently loaded anonymizer definition, controlled by the __Anonymizer or __AnonymizerDef built-in variables. If there is no definition loaded, an exception will be raised.
HL7 APPEND[TEXT] filename
Adds the output message to a file using the message's AppendToFile method. If the file does not yet exist, it will be created. If APPENDTEXT is used, the message will be saved with CRLF line endings. See also: HL7 SAVE[TEXT]
HL7 CLEAR
Clears the output HL7 message. The output message is not automatically cleared between messages in case you need to combine multiple messages into one. If you have unexpected data in your output message, you probably forgot to CLEAR it.
HL7 CLEARSEG index|segmentKey
Clears the segment specified by the 0-based numeric index or segment key in the output message, leaving only the segment ID (and encoding characters if a header segment). Example: HL7 CLEARSEG AL1#2
HL7 COMBINEADD
Combines any ADD segments in the message. The Compact and ForceField message properties must be False for this to work reliably because those options modify trailing separators.
HL7 COPYINPUT
Clears the output message and replaces it with a copy of the input message.
HL7 COPYSEG index|segmentKey
Copies the requested segment from the input message and adds it to the output message. If the specified segment does not exist in the input message, a blank segment of that type is added.
HL7 DELREP fieldKey
Deletes the specified field repetition from the output message. Example: HL7 DELREP PID.3~2
HL7 DELSEG index|segmentKey
Deletes the specified segment from the output message. If a segment key with an AnyKey sequence is given (e.g. HL7 DELSEG NK1#*), all segments of the specified type will be deleted.
HL7 ENCODING DEFAULT|COPYINPUT|encoding
Sets the encoding on the output message for use by APPEND and SAVE. DEFAULT selects Windows' default ANSI codepage. COPYINPUT copies the encoding currently being used by the input HL7 message. Any other value (e.g. UTF-8, 1251, etc.) is used as-is. Specifying an invalid encoding name will raise an exception.
HL7 FORCECOMPACT
Compacts the data (removes extra trailing separators) in the output HL7 message even when the Compact property (__Compact) is turned off.
HL7 INSERTSEG index segmentString
Like HL7 ADDSEG, but you specify the 0-based index where you want the new segment inserted into the output message.
HL7 LOAD filename
Loads the output HL7 message from a single-message file using the message's LoadFromFile method. Handy if you are building a message out of multiple non-sequential input messages.
HL7 LOADNEXT
Loads the next message from the currently open multi-message HL7 FILE into the output HL7 message and advances the file position to the start of the next message. If the end of the file has been reached, the output message will simply be CLEARed. Using HL7 LOADNEXT without an open file will raise an error. See also: the __FilePos built-in variable.
FILE OPEN $multimsgfile
HL7 LOADNEXT
LOOP __SegCountOut #> "0" ; Message is cleared if no more messages
  ; Do something with this message...
  HL7 LOADNEXT
END
FILE CLOSE
HL7 OUTPUT
Writes the output HL7 message to the active output file, one segment per line. The line terminator of the output file is controlled by the __FileEOL built-in variable.
HL7 [I]REPLACE [WHOLE] level old=new
Replaces values in the output message with new values. The replacement is case sensitive unless IREPLACE is specified to make it case-insensitive. You must specify at what level you wish to replace the data using the following options (which may be shortened to three characters if desired):
WHOLE
If the WHOLE keyword option is given, the entire value must match the old value to be replaced. Most useful on the smaller values like fields or subcomponents. For example, if WHOLE SUB "RE"="OP" was used, a subcomponent with a value of "FRED" would not be changed. Ignored at the KEY level.
MESSAGE
Replaces old with new at the MessageStr level.
SEGMENT
Replaces old with new at the SegmentStr level.
FIELD
Replaces old with new at the FieldStr level.
SUBCOMPONENT
Replaces old with new at the subcomponent level. This level (or KEY) should be used for most replacements, as all data is fully un-escaped.
KEY
The old value is given as an HL7 key, and case does not matter as the entire key value gets replaced (since there is no old value to look for). If a specific segment sequence and/or field repetition are specified, only those specific instances will be replaced. Otherwise, all matching segments and repetitions are replaced. Only existing values are changed; to add values, use HL7 SET.

Examples:
HL7 REPLACE KEY PID.3="12345" - Replaces any PID.3, like PID#?.3~?.1.1.
HL7 REPLACE KEY PID#1.3="12345" - Replaces any PID.3 repetition in the first PID segment only.
HL7 REPLACE KEY PID.3~1="12345" - Replaces only PID.3~1 in any PID segment.
HL7 REPLACE KEY PID#1.3~1="12345" - Replaces only PID#1.3~1.1.1 (use SET!)
HL7 REPLACESEG index|segmentKey segmentString
Replaces the segment in the output message specified by the 0-based numeric index or segment key with the provided segment string. An exception will be raised if the numeric index is out of range or the segment key resolves to a segment that does not exist.
HL7 SAVE[TEXT] filename
Writes the output message to a file using the message's SaveToFile method. If the file exists, it will be overwritten/replaced. If SAVETEXT is used, the file will be saved with CRLF line endings. See also: HL7 APPEND[TEXT]
HL7 SET fieldKey=data
Assigns the data to the specified field key in the output HL7. If you use asterisks in place of the segment sequence and/or field repetition index, all such instances will be set. Sort of the opposite of HL7 REPLACE, which replaces all instances by default.

Examples:
HL7 SET PV1.2=PID.18.1 - Assigns PID.18.1 from input to PV1#1.2~1.1.1 in output.
HL7 SET IN1#*.3="SELF" - Sets IN1.3.1.1 in any IN1 segment to "SELF".
HL7 SET PID.3~*="12345" - Sets all repetitions of PID#1.3.1.1 to "12345".

If the field does not exist, a single repetition is added/set to the value. If you only want to change existing values, use HL7 REPLACE KEY.
HL7 SPLITADD FIELDS|LENGTH|SEPARATOR=Max
Splits long segments into ADD segments. The FIELDS option splits by the Max number of fields. The LENGTH option splits by Max length. SEPARATOR splits by Max length at the nearest separator <= Max. Only the first character of the split type is required. The Compact and ForceField message properties must be False for this to work reliably.

Return to Top

XML Commands

A common non-HL7 input format is XML. These commands allow you to read and write XML documents.

The commands use standard XPath notation to identify elements and attributes. You can use absolute paths that start at the document root, or relative paths based on the currently selected node. Square brackets surround the index of repeating elements. An at-sign (@) indicates an attribute. XML is case-sensitive.

After each XML command, you can check the value of the boolean section variable $XMLOK. If set to "1", the command was successful. If "0", the last XML command has failed. The most common failure would be attempting to select or read an element or attribute that does not exist.

The following commands are used when reading an XML file:

XML OPEN filename
Opens the specified file. Only one XML document may be open at a time. A document remains open until closed, another document is opened, or the script section ends.
XML CLOSE
Closes any open XML document and clears the $XMLOK variable.
XML SELECT xpath
Selects the specified node as the "active" node. Subsequent XML commands can use relative XPath values to access data. If the requested node does not exist, the active node will not change and $XMLOK will be set to "0".
XML GET variable=xpath
Reads the value of an element or attribute and stores it in the specified variable. If the value does not exist, the variable will be cleared and $XMLOK will be set to "0".

The commands for writing XML are limited, but functional. The commands listed above are also available when working with a new XML document.

XML NEW rootnode
Create a new UTF-8 encoded XML document with a root node of the given name. The root node is selected as the active node.
XML SAVE filename
Saves the document to the given filename. The document remains open.
XML ADDCHILD nodename=text
Adds a child node with the given name and text to the active node. The text may be blank, but must be present. The new child node is selected as the active node.
XML DELATTR attributename
Deletes the active node's attribute of the given name. $XMLOK will be set to "0" if the attribute is not found.
XML DELCHILD nodename
Deletes the matching child of the active node. The node name may include an index, such as "child[2]", to delete the Nth node matching the name. $XMLOK will be set to "0" if a match is not found.
XML SETATTR attributename=value
Creates or updates the attribute of the given name on the active node.
XML SETTEXT value
Sets the text of the active node to the new value.

Using commands other then OPEN, CLOSE, or NEW without an active XML document will raise an exception.

The following example uses an rss.xml file as input, since that is a commonly understood XML standard and allows the concepts to be easily demonstrated.

XML OPEN __InFile
IF $XMLOK == "0"
  ERROR "Failed to open XML file: " + __InFile
END
SET $ITEM="1"
; This selects a node with an absolute (from the root) path:
XML SELECT "/rss/channel/item[" + $ITEM + "]"
; Loop until the requested node doesn't exist:
LOOP $XMLOK == "1"
  ; Get data using relative paths (. = active node):
  XML GET $TITLE="./title"
  XML GET $GUID="./guid"
  XML GET $IPL="./guid/@isPermaLink" ; An attribute of the guid
  LOG "Item ?: title=" + $TITLE + ", guid=" + $GUID + ", isPermaLink=" + $IPL
  INC $ITEM
  ; SELECT can also use relative paths (.. = active node's parent)
  XML SELECT "../item[" + $ITEM + "]"
END
XML CLOSE

Return to Top

Sample Scripts

Here are some scripts I have actually used as examples of how to write your own. I will add more interesting examples as I encounter them.

This simple script is the sort of thing I do frequently - get a count of the different varieties of something. This one analyzed a batch of order messages (ORM) that failed due to invalid/undefined order frequencies by gathering a count of the frequencies and the facilities they were sent from.

;Increment the count for this facility-frequency combo:
INC "%" + MSH.4.1 + "-" + OBR.27.2
;What is the grand total for each frequency?
INC "%TOTAL-" + OBR.27.2

<POST>
  LOGVARS %* NOPREFIX
</POST>

/* Sample output
2015-02-24 08:17:01.748 - LOGVARS %* NOPREFIX

A0-IN AM=31
A0-ONCE NOW=11
A0-THREE TIMES DAILY PRN=4
A0-WEEKLY=1
D0-THREE TIMES DAILY PRN=7
F0-THREE TIMES DAILY PRN=1
N0-IN AM=15
TOTAL-IN AM=46
TOTAL-ONCE NOW=11
TOTAL-THREE TIMES DAILY PRN=12
TOTAL-WEEKLY=1
*/

There was an interruption of the inbound feed at a client. After restoring the feed, they sent a file full of messages that they should have sent us during that time. I used this one-liner script to put the message control IDs into SQL statements for our inbound message store so I could make sure that we had received them all and had processed them successfully. (We did!)

OUTPUT "SELECT CONTROLID, STATE FROM INBOUND WHERE CONTROLID = " + E@MSH.10

Here is a variation on the above that looks those messages up directly using a database connection:

<INIT>
  %OK = "0"
  %FAILED = "0"
  %NOTFOUND = "0"
  %TOTAL = "0"
</INIT>

INC %TOTAL
SET $STATE = Q@("SELECT STATE FROM INBOUND WHERE CONTROLID = " + E@MSH.10)
IF U@$STATE == "SUCCESS"
  INC %OK
ELSEIF $STATE == ""
  INC %NOTFOUND
  LOG MSH.10 + " not found"
ELSE
  INC %FAILED
  LOG MSH.10 + " failed"
END

<FINAL>
  LOG ""
  LOG %OK + " OK"
  LOG %FAILED + " failed"
  LOG %NOTFOUND + " not found"
  LOG %TOTAL + " total"
</FINAL>

This script (with minor modifications) is currently in production use with HL7ScriptService to take an inbound HL7 feed and direct the messages to their appropriate destinations based on content. The HL7TransmitterService takes the messages from the output folders and sends them on to their destinations.

<INIT>
  PROCEDURE=SaveMsg
    ; $SUBDIR must be set by the caller
    ; The base file path is where the script is.
    LOG "Sent to " + $SUBDIR
    SET $SAVEFILE=__ScriptPath + $SUBDIR + "\" + __InName
    SET $SAVEFILE=[UNIQUEFILENAME, $SAVEFILE]
    HL7 SAVE $SAVEFILE
    INC $SENDCOUNT
  ENDPROC
</INIT>

LOG "Processing " + __InName + " - " + PV1.39 + " " + PV1.18 + " " + F@MSH.9

; Copy input to output
HL7 COPYINPUT
SET $SENDCOUNT="0"

; Output all ADT to the outpatient system
IF MSH.9.1=="ADT"
  SET $SUBDIR="OutOP"
  CALL SaveMsg
END

; Does this message need to go to inpatient (ACME facility)?
; All ORU/ORM messages go to IRF only.
; For ADT, IN is the current inpatient type and REF is a pre-admit inpatient.
IF MSH.9.1=="ORU" OR MSH.9.1=="ORM" OR (PV1.39=="ACME" AND (PV1.18=="IN" OR PV1.18=="REF"))
  SET $SUBDIR="OutIRF"
  CALL SaveMsg
END

; Make sure nothing has slipped through the cracks
IF $SENDCOUNT=="0"
  ERROR "Message not forwarded: " + __InName
END

This script took production lab ORUs as input. It anonymized them by removing any NK1 segments and replacing the PID & PV1 segments with pre-created replacements so all the labs could be found on a single test patient. It also kept only the first sample source (OBR) from each message, omitting any others. All the output was appended into a single file which was then processed in the test system.

HL7 CLEAR
LOOP SEGMENTS
    IF "###"=="OBR#2"
        LOG "Skipping OBR#2 on " + MSH.10
        BREAK
    ELSEIF ###.0=="PID"
        HL7 ADDSEG "PID|1||LABM001||LABS^TEST||19700101|M|||123 MAIN ST^^GREAT FALLS^MT^59404|||||M||LABA001|111-22-3333"
    ELSEIF ###.0=="PV1"
        HL7 ADDSEG "PV1|1|I|WEST^101^A|EL|||DRWHO^WHO^DOCTOR||DRWHO^WHO^DOCTOR|REH||||PR|||DRWHO^WHO^DOCTOR|IN||MC||||||||||||||||HOM|||MDE||ADM|||201908121300"
    ELSEIF ###.0<>"NK1"
        HL7 COPYSEG ??
    END
END
HL7 APPEND "C:\Temp\AnonLabs.hl7"

HL7Tools.zip includes CombineFragments.h7s, a script that will re-combine fragmented messages. That script demonstrates numerous techniques including section and persistent variable usage, IF and LOOP statements, and saving/loading HL7 messages to files.

Return to Top

Script Validation

In the interactive HL7Script program, there is a button labeled "Validate Script". This will syntax check the currently selected script and display any errors or warnings in the logging pane.

No logging, output, or other file- or database-related activity (HL7 SAVE/LOAD, etc.) is actually done during validation. Any queries or non-HL7 input like XML or LOOP TEXT/CSV have simulated input when validating. SQL connections and queries are not checked.

Each LOOP will be entered once, and all IF statements have all IF, ELSEIF, and ELSE sections run. The only code that will remain untouched are PROCEDUREs that are never CALLed.

Keep in mind that not all validation failures actually represent errors in your script. Because every line of the script is being run once while ignoring the normal logic and branching, parts of your script will be run with unexpected data. A script that fails validation may always run perfectly in normal usage. Validation is a way to test the entire script for syntax errors and highlight possible logic errors you might not have otherwise noticed.

Since the logic can be so dependent upon data, the input used for validation can be chosen. It may be an HL7 file (the first message will be read), a default HL7 message in the form of an ADT^A08 message that contains only an MSH segment, or non-HL7 input which supplies simulated input. With HL7 input, it may be helpful to test with the mostly blank message to highlight any assumptions that may have been made about the data.

For example, a script increments a dynamically named variable based on some values in the input message:

INC "_" + PID.3 + "_" + PID.18

If PID.3 is blank, an error will be raised about attempting to increment a built-in variable because the variable name starts with double underscores. In normal usage, there is probably a condition around that line to prevent reaching it if PID.3 is blank, but validation will process that line anyway.

ValidationInput

If a special comment is written in the script that contains the text "ValidationInput=", the input prompt can be bypassed, automatically selecting the specified input.

The value after the equal sign may be an HL7 filename, the word Default to select the default HL7 message, Non-HL7 to leave the input message blank, or the word HL7 to use an HL7 message embedded directly in the following lines of comments.

/*
ValidationStrict=0
ValidationInput=HL7
MSH|^~\&||ACME||REHAB|20200726093700.1234-0500||ADT^A08|150233321|D|2.3|||AL|NE
EVN|A08|202007260937|||WHO101^WHO^DOCTOR|202007260937
PID|1||MRN001||TEST^TOM^T||19770707|M||CA|123 MAIN ST^^GILBERT^AZ^85290||4805551212|||S|NO|ACCT001|999-99-9999
PV1|1|O|||||SPO101^SPOCK^DOCTOR^A^^^M.D.|||||||PR||||RCR||MC|||||||||||||||||||MDE||PRE|||202007260900
*/

An embedded HL7 message must be terminated by the close of a comment block (*/) or the word ENDHL7 on a line by itself.

If the value is not one of these options or the file does not exist, the prompt will be shown instead. A filename without a path is assumed to be in the script directory.

ValidationStrict

The special comment ValidationStrict=0 can be used to turn off the Strict message property during validation to relax message construction rules. This may avoid some false-positive HL7 errors like "Segment IDs must be 3 characters" and "First message segment must be a header/trailer segment". The value is boolean, and can be set on ("1") or off ("0") as needed. When not present, the default preference for Strict is used.

Return to Top

Command-line Usage

HL7Script.exe accepts command-line arguments to pre-set the various options. These are helpful for creating shortcuts to commonly used configurations. As an example, the following command is the shortcut used to load HL7Script's testing script, ready to run:

HL7Script.exe /Connection=HL7 /File /HL7 /Input=.\QA\QA.hl7 /Log="" /Script=.\QA\QA.h7s

Each option starts with a slash and the name of the option, followed by an equal sign and option value if one is required. Option names are case-insensitive and only the first letter is required. Enquote option values that contain spaces. Empty quotes can be used to clear an option value.

/Connection=name
Specify the name of a pre-configured database connection.
/Database
Switch to Database input.
/File
Switch to File input.
/HL7
Switch to HL7 input.
/Input=inputfile
Specify the input file or wildcard when using File input.
/Log=logfile
The log filename. Clear it for on-screen logging only.
/NonHL7
Switch to non-HL7 input.
/PollingSQL=sql
The polling SQL to use for Database input.
/Script=scriptfile
The script file to load.

Omitted options and those not available on the command-line assume their last-used values as would normally occur at startup.

Return to Top

HL7ScriptService

HL7ScriptService.exe is a Windows service that will periodically poll a directory or database for input and process that input with a script. The input may contain HL7 messages or other kinds of data. Multiple connections can be configured, each set to poll for different input and use different scripts and settings.

Use the HL7ScriptServiceConfig.exe program to configure the service settings. The settings are stored in HL7ScriptService.ini in the application directory. Each connection that you configure is stored in a separate [Section] in the ini file.

To add a connection, fill out the Connection Name and other properties and press Add. To edit an existing connection, select it in the list, modify the settings and press Save. To duplicate a connection, select one from the list, change the Connection Name and press Add. Use the Delete button to remove the selected connection.

The configuration program can also install and control the service when Run as Administrator (which it does by default). Alternately, you can use "HL7ScriptService /install" (or /uninstall) in a command prompt that was started using "Run as Administrator". The service appears in the Service Control Manager as "HL7ScriptService".

When upgrading, it is always a good idea to run the config program before restarting the service. It will alert the user if there are important changes and allow the settings to be reviewed and saved. If the version stored in the ini differs enough from the current service version, the service will refuse to start until the ini file is updated.

When the service is running, the "Start" button changes to "Connection Status". Press the button to open a dialog showing the current status, last activity time, and uptime message count for each active connection. The screen auto-refreshes to act as a live dashboard. To query the connection status info from an external process, see the Service Monitoring topic.


HL7ScriptServiceConfig

HL7 Script Service Connection Settings
Setting NameTypeNotes
Connection Name string The name of the connection and ini [Section]. Log entries are prefixed with this name.
Disabled booleanDisable the connection without needing to delete it. Appears as strikethrough.
Schedule specialDetermine when processing takes place. See below.
Connection Log string Use a connection-specific log instead of the global log.
Connection Log Level string Logging level for the connection-specific log. (N=Use global level)
Polling Interval number How frequently, in seconds, to poll for input.
Non-HL7 booleanThe input is not HL7 data. The input message supplied to the script will be blank.
HL7 Caching booleanTurn on the input and output messages' caching of SegmentStr and FieldStr values.
Post-Process at Interval endbooleanPerform post-processing at the end of each interval, or after each input.
Post-Process on Abort booleanPerform post-processing after an ABORT.
Script File string The script filename used to process the input files.
Named Fields File string Optional filename of a Named Fields file for field keys and message validation.
Database Connection string The name of a pre-configured Database Connection.
File-based
Input Wildcard string Path and wildcard for the input files to process.
Max Files per Interval number Limits the number of files processed per interval to keep the service responsive.
Sort Files By string Sorting helps make sure files are processed in the order received.
Recurse Subdirectories booleanLook for files in subdirectories of the Input File Mask.
One Message Per File booleanOne message per file when checked, one or more messages per file when unchecked.
Archive Directory string Path to move input files to after successful processing. (blank=off)
Days to Keep Archived Files number Number of days to keep archived files. (0=forever)
Delete Files booleanIf not archiving, delete input files after successful processing.
Error Extension string Rename files that raise exceptions so regular processing can continue. (blank=off)
Add ERR booleanAdds an ERR segment to a file renamed with the Error Extension.
Message Start Values string Comma-separated values that identify the start of a message in a multi-message file.
Database
Polling SQL string A query to poll the database for the next batch of input to be processed.
Error SQL string A SQL statement to update the database with the results after an unexpected error.

HL7 Script Service Global Settings
Setting NameTypeNotes
Service ID string See Multiple Service Instances.
Log Filename string The filename may contain date substitution surrounded by percent signs (%).
Logging Level string Determines how much logging is done, ranging from None to Trace.
Show Levels booleanShows the logging level of each entry into the log with a single letter like [I] for Info.
Timestamp Format string The date/time format for each log entry. Default=yyyy-mm-dd hh:nn:ss
Days to Keep Logs number Number of days to keep dated log files. (0=forever)
Persist Service VariablesbooleanControls the saving/loading of service variables upon service stop/start.

Tip: If you never process billing files, you can optimize the loading of multi-message files a little bit by changing the Message Start Values setting to just "MSH|". Every line read from a file has to be checked against this list to see when the next message starts. This setting is not used when reading One Message Per File.

If the service fails to start, check the log file. If the service is unable to use the log file specified in the ini file, it will try to write to HL7ScriptService.log in the executable directory.

If you modify the ini or named fields files after the service has started, you must restart the service to load the new values. The configuration program will prompt you to do this. Changes to script files will be automatically detected at the start of each interval and will be reloaded as needed. If the script needs to be reloaded, the current finalization section will be run prior to doing so.

Any logging done in the script with LOG commands is considered to be at the default "Info" logging level by the service.

The log filename can contain a date replacement format string surrounded by percent signs (e.g. "D:\Logs\HL7ScriptService%yyyymmdd%.log"). Any FormatDateTimeEx-compatible format string will do. When using a dated log file, logs older than the Log Days setting will be cleaned up automatically at startup and each time the date changes.

The log date replacement is assumed to change no more frequently than daily, but may be longer (weekly, etc.). When using a daily log file, the suggested Timestamp Format is "hh:nn:ss" since the date is implicit for all entries in the same file.

Connection-specific logs can be used to put a connection's log entries into its own log instead of the global log. These can use the same date replacement syntax in the filename as the global log. All settings besides the filename and logging level come from the global log settings (timestamp format, days, etc.). A connection-specific log will not prefix each entry with the connection name since it will always be the same.

Schedule

A connection can be scheduled. There are three options: Always on (the default), a single start and stop time, or on a cycle. A cyclical schedule means it runs for X minutes on, then X minutes off. When a schedule is set, the Schedule button text will appear in bold. If a cyclical schedule is set, it will also be italicized. The button's hint will describe the current schedule. Schedule starts and stops will appear in the log at the Verbose or higher levels.

Each connection will be configured for File-based input or Database input. Each interval, the directory or database is polled to see if there is new input available for processing.

An ABORT from the script will halt processing of all remaining input for the current interval. The PostOnAbort setting will determine whether post-processing is performed. The ABORT flag will be reset on the next interval.

If an exception is raised during processing (either unexpected or because of an ERROR command), processing will halt and post-processing will be skipped. The ERROR processing section will be run, or an entry will be made in the log noting the file name/primary key and error message if the ERROR section was not provided.

When a connection is using non-HL7 input, the input message supplied to the script is empty. The script must know how to handle the input based only on its name or primary key. The filename or primary key is available in the __InFile built-in variable. Options like LOOP TEXT, LOOP CSV, BASE64LOAD and BASE64SAVE, and XML commands are available for processing alternate input.

File-based Input

If you are not using the ArchivePath or AutoDelete settings, you will want to have the script delete/move/rename the files out of the way after processing so you don't keep processing them repeatedly. Archive cleanup (ArchiveDays > 0 or LogDays > 0) is performed at service startup and whenever the date changes.

When post-processing is done after each file, the input file is closed before post-processing so it can be deleted if needed. Single-message files (OneMessagePerFile=1) are never left open and can be deleted at any time.

An ABORT or exception from the script will skip the automatic archive or deletion of the current input file, but the script can still do so on an ABORT.

In the event of an exception and the Error Extension setting has a value, the file will be renamed using that extension so that it doesn't keep getting picked up, preventing other files from being processed. If the error extension contains an asterisk, it will include the file's original extension rather than just replacing it. If the Add ERR setting is turned on and this is a single-message HL7 file, an ERR segment with the error details will be added to the message when it is renamed. This allows you to see at-a-glance what caused the file to fail without having to search for the filename in the log.

A Database Connection is optional when using file-based input. When supplied, the LOOP QUERY, Q@ (Query), and X@ (eXec) features are available for use.

Database Input

A Database Connection is required when polling a database for input.

The Polling SQL is used to check for input on each interval. The first field returned must be the primary key of the input. This value is provided to the script as if it were the input filename, in the __InFile built-in variable.

If the input is an HL7 message, the second field returned must be the HL7 message as a string. For non-HL7 input, the second field may be omitted. Any additional fields are ignored, so limit the query to the required values if possible. There are no parameters available to the Polling SQL.

Two additional considerations for the Polling SQL are the order in which messages are selected for processing, and limiting the number of messages returned per interval to keep the service responsive.

The Error SQL is used when an unexpected error occurs during processing. It is provided the primary key (:PK) as a string, the error message (:ErrorText), and error type (:ErrorType) as parameters. When a message is successfully processed, it is assumed that any required updates to the input will be performed within the script itself. If the script has an error processing section, it gets run prior to the Error SQL.

The SQL editing buttons open a larger editing dialog that includes detailed help including the parameters that are available to each query. Note that line breaks in your SQL appear as \.br\ in the single-line edit and when stored in the ini. These are replaced with actual CRLF line breaks in the editor dialog and when sent to the database engine.

Multiple Service Instances

HL7ScriptService and HL7TransmitterService can be configured to run multiple instances on a single server. A common use for this would be running both a Test and Production instance on the same server, possibly of different versions. The instructions are the same for either service.

Set up two (or more) separate application directories, each with its own copies of the executables. Run the configuration program in each directory to create the ini file. After configuring the regular settings, press the Service ID button. Enter a value that will uniquely identify the service running from each directory. Keep the value short and alphanumeric, like "Test" or "Prod".

The Service ID button will make sure your chosen ID is not already in use, and will automatically uninstall/reinstall the service as needed to change an existing ID. Any changes you may have made to the service startup type or logon account will be preserved for you. The Service ID is saved to the ini file.

When a Service ID has been assigned, the configuration program will show the ID in square brackets in the Service Control area's status text:

HL7 Service ID

The service will appear in the Services management console with the Service ID value appended to the base service name. If you had set up "Test" and "Prod" instances of HL7ScriptService, you would see the entries named HL7ScriptServiceTest and HL7ScriptServiceProd.

Each service can then be configured, started, stopped, or uninstalled independently of any others.

Each instance must have unique log filenames. Attempting to share log files will cause conflicts when writing to them.

Return to Top

Notepad++ Language Definition

I have included HL7Script_NPPLang.xml with HL7Tools. This is my customized language export file for Notepad++. You can import this into your copy of Notepad++ by going to the Language menu, "Define your language...", and then using the Import button.

The language definition uses the "h7s" extension to identify HL7Script files. Files without this extension (like .txt files) will require the language to be selected manually. On my system, I have associated .h7s files with Notepad++ so they open automatically when double-clicked or when the edit button is pressed in the HL7Tools programs.

Return to Top