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.
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
To reference a value from the input HL7 message, use the following key syntax. Items shown in [square brackets] are optional.
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).
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.
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 */
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.
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.
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.
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.
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.
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>
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.
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.
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!
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.
There are also Built-In variables that start with double underscores (__), which are detailed in their own section of the reference.
Variable Lifetimes | |
---|---|
Section | Variables Cleared at Start |
Initialization | $Section, _Interval, %Persistent |
Pre-Processing | $Section, _Interval, __Error* |
Main Script | $Section |
Post-Processing | $Section |
Finalization | $Section, _Interval |
Error processing | Remain 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.
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
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."
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.
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.
Built-in variables may not be INCremented or DECremented.
; 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.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
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.
SET $YEARS=[DATEDIFF, Y, 20190101, 20200101] ; 1 year
SET $DAYS=[DATEDIFF, D, 20190101, 202001011500] ; 365 days, ignores the time difference
; 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"
; 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
SET $ANSIDATE=[FORMATDATE, "yyyy-mm-dd", D@"NOW"] ; Returns "2017-02-21"
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"
SET $VAR=[LEFT, 3, "foobar"] ; Returns "foo"
SET $VAR=[LEFT, -1, "foobar"] ; Returns "fooba"
SET $VAR=[PATHJOIN, "D:", "Path", "File.ext"] ; Returns "D:\Path\File.ext"
SET $VAR=[PATHJOIN, "C:\DIR", ] ; Returns "C:\DIR\"
; Validate only required fields in the input message:
SET $ERR=[VALIDATE, INPUT, REQ]
IF $ERR <> ""
LOG "Input message is missing required fields: " + $ERR
END
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.
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.
SET $PK=Q@("SELECT id FROM patients WHERE mrn = " + E@$MRN)
SET __FileEOL=H@"0D0A"
SET $PATIENTTYPE=K@("PV1#"+$COUNTER+".18.1")
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@()
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 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.
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 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 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.
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
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
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.
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
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.
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 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
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"
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.
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:
When using HL7ScriptService, all logging done in the script is considered to be at the default "Info" logging level.
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:
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.
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.
; 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!
; 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
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.
Commands available:
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
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:
The commands for writing XML are limited, but functional. The commands listed above are also available when working with a new XML document.
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
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.
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.
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.
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.
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.
Omitted options and those not available on the command-line assume their last-used values as would normally occur at startup.
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.
HL7 Script Service Connection Settings | ||
---|---|---|
Setting Name | Type | Notes |
Connection Name | string | The name of the connection and ini [Section]. Log entries are prefixed with this name. |
Disabled | boolean | Disable the connection without needing to delete it. Appears as |
Schedule | special | Determine 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 | boolean | The input is not HL7 data. The input message supplied to the script will be blank. |
HL7 Caching | boolean | Turn on the input and output messages' caching of SegmentStr and FieldStr values. |
Post-Process at Interval end | boolean | Perform post-processing at the end of each interval, or after each input. |
Post-Process on Abort | boolean | Perform 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 | boolean | Look for files in subdirectories of the Input File Mask. |
One Message Per File | boolean | One 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 | boolean | If 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 | boolean | Adds 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 Name | Type | Notes |
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 | boolean | Shows 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 Variables | boolean | Controls 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.
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.
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.
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.
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:
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.
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.