If the message doesn't qualify for any of the conditions, it won't be delivered anywhere and will be purged at the end of the retention period you've set for the Ens.Util.Tasks.Purge task. Do you need to delete it immediately?

For starters, I think this:

request.GetValueAt("RGSgrp(1).AIGgrp(context.AIGitr).AIG:3)")'=""

Should actually be this:

request.GetValueAt("RGSgrp(1).AIGgrp("_context.AIGitr_").AIG:3)")'=""

Let's see what happens after you fix that.

Jeffrey Drumm · Nov 10, 2025 go to post

If you're using a custom business process to handle the message, you could perform the truncate in an OnRequest() callback.

If you're using a BPL, you would add an action near the beginning of the process to perform the truncate. The request object will be a single batch object containing a collection of records; the next step would be to iterate over the collection and perform the inserts.

You could do essentially the same thing in a DTL; use an SQL rule at the beginning to truncate, then add a foreach to iterate over the batch and populate the DB.

The key is the fact that your BP is getting all of the records in a single object. So when that object arrives, it's time to truncate the table and repopulate it. You will have to skip the first record if it contains headers, of course.

Jeffrey Drumm · Oct 31, 2025 go to post

With IRIS 2025.1.1 on Windows, I cannot reproduce your issue. Both sets of instructions execute normally. 

VS Code Version: 1.105.1 (user setup)
Commit: 7d842fb85a0275a4a8e4d7e040d2625abbf7f084
Date: 2025-10-14T22:33:36.618Z
Electron: 37.6.0
ElectronBuildId: 12502201
Chromium: 138.0.7204.251
Node.js: 22.19.0
V8: 13.8.258.32-electron.0
OS: Windows_NT x64 10.0.19045

ISC ObjectScript Extension 3.2.1-beta.1

ISC Server Manager 3.10.6-beta.3

ISC Language Server 2.8.0

Jeffrey Drumm · Oct 27, 2025 go to post

You are correct in that ReplyCodeAction E* does not evaluate the contents of the ACK message MSA segment; it only looks at the IRIS error code and text.

For your specific example, though, I'm wondering why you're not simply setting a condition to exclude invalid messages in the Routing Rule?

Jeffrey Drumm · Oct 22, 2025 go to post

A foreach (possibly multiple foreaches) is what you need. Getting the iterators right is the issue. You're iterating over the AISgrp in the first RGSgrp of the source message and placing them in target RGSGroups with the same iteration count of the source AISgrp. Here's an example based on the 2.3.1:SIU_S12 schema structure that should do what you want, and also handles repetitions of the AIL, AIP and AIG groups:

Jeffrey Drumm · Oct 15, 2025 go to post

What account is Caché running under? You'll need to review Windows Services to find the Caché service controller (I don't remember the exact name). It's probably running under the Local System account; you'll need to change it to log on with your own credentials if you want it to use your account's environment.

Jeffrey Drumm · Oct 14, 2025 go to post

Instead of ..Contains() as the outer method, use:

..RegexMatch(..ToUpper(source.{PIDgrpgrp(k1).ORCgrp(k2).OBXgrp(2).OBX:ObservationValue()}),"NOT DETECTED|NEGATIVE")
Jeffrey Drumm · Oct 6, 2025 go to post

ORCgrp is a member of PIDgrpgrp and PIDgrpgrp is missing from your "if" statement. The condition should read:

..Contains(..ToUpper(source.{PIDgrpgrp(k1).ORCgrp(k2).OBXgrp(k3).OBX:ObservationValue()}),"DETECTED")

I don't know whether you're using the same doctype for the target, but if you are, the target path will need to be updated as well.

Jeffrey Drumm · Oct 3, 2025 go to post

The rule parser/compiler doesn't handle calls to methods/classmethods unless they extend Ens.Rule.FunctionSet, and it doesn't recognize the ##class() keyword. If your class does extend Ens.Rule.FunctionSet, you should be able to use just the Methodname as shown below:

<assign property="PropertyName" value="SomeMethod(args)" />
Jeffrey Drumm · Oct 3, 2025 go to post

While @David.M's response will provide a query, it will likely not include the HL7 message-derived values. For example, a search that includes the HL7 field values you've listed will return null for their values when executed via the SQL menu or SQL shell.

That said, I created a tool (originally for use with HL7 Spy to extract HL7 messages from Ensemble/Health Connect) that has some features that may help get you what you want. See this thread for an example of its use.

Jeffrey Drumm · Sep 4, 2025 go to post

The Matches() method in the DTL and Routing Rule function list does not use regex syntax; it uses COS "?" pattern matching. The pattern you'd need for your specific example is "1A2N" for 1 alpha character followed by 2 digits.

There's also a ..RegexMatch() method for the DTL and Routing Rule editors in most modern versions of IRIS. That accepts most pcre regex patterns.

Jeffrey Drumm · Aug 21, 2025 go to post

It's been my experience that the contents of the ACK message are not evaluated by any of the ReplyCodeActions. If you need to specifically suspend messages that are NAK'ed with this value in MSA:3, you'll need to create a custom operation and associated adapter (likely extending EnsLib.HL7.Operation.TCPOperation and EnsLib.HL7.Adapter.TCPOutboundAdapter respectively) that implements the NAK/Suspend logic.

Jeffrey Drumm · Aug 18, 2025 go to post

Include the setting in the SETTINGS string, but prefix it with a dash (-) character:

Parameter SETTINGS = "-Whatever,DSN:Basic:selector?context={Ens.ContextSearch/DSNs},..."

EDIT: Was apparently typing that while you were reading the documentation 😁

And for competeness' sake, the actual documentation:

Adding and Removing Settings

Jeffrey Drumm · Jul 9, 2025 go to post

And those message schema categories are configured in the services and operations that use that SearchTable? And, just as importantly, have always been there? It's not going to index messages that don't have those assigned in the message object's properties.

Jeffrey Drumm · Jun 25, 2025 go to post

I'm assuming you mean one target field, since a segment in HL7 starts with a segment identifier like PID or PV1 and contains a string of fields separated by field delimiters (normally the "|" character).

If, for example, you want to copy the  Patient First Name and Last Name separated by a space character into a single field, you could do this:

The underscore character is the concatenation operator in IRIS, so the example is taking:

source.{PIDgrpgrp(1).PIDgrp.PID:PatientName(1).GivenName}, Concatenating it with a space character followed by source.{PIDgrpgrp(1).PIDgrp.PID:PatientName(1).FamilyName}, then storing it in target.{PIDgrpgrp(1).PIDgrp.PID:PatientName(1)}. This effectively replaces the entire field with just the patient's first and last name.

My example uses the HL7 2.5.1 schema; your field paths may be different based on the version of HL7 you're using.

Jeffrey Drumm · Jun 20, 2025 go to post

OK, I'm pretty sure I found the problem.

Studio's default compiler flags are "cbk" and VS Code's were set to "cuk." I changed VS Code to match and it appears the issue is resolved.

EDIT: Weirdly, the tooltip text is not updated ...

EDIT 2: If the first line of the comment in the source contains HTML tags, the tooltip reverts to some earlier iteration of the comment (no idea where it's finding that). However, the popup is correct. Removing the tag(s) from the first line causes the tooltip to display the specified comment.

So ... if you want your tooltip to match the first comment line, make sure it has no HTML tags!

Jeffrey Drumm · Jun 1, 2025 go to post

@Robert Cemper pointed you in the right direction. However the USER namespace is not normally enabled for Interoperability. You can create your own database(s)/namespace(s) in the Management Console and they would default to being interoperability-enabled, or you can enable USER for Interoperability with the following command:

Do ##class(%EnsembleMgr).EnableNamespace("USER",1)
Jeffrey Drumm · May 29, 2025 go to post

The credentials entered at the Web Gateway login page are not related to any credentials stored in IRIS. They're controlled solely by the Username and Password entries in the [SYSTEM] section of CSP.ini. If those entries are deleted from CSP.ini, you should bypass the login page completely.

Note that if you're using a standalone web server on an IRIS installation that's been upgraded from an earlier version, there may be multiple CSP.ini files. For example, I work with an IRIS installation that has been upgraded many times and is now using a standalone web server; the original CSP.ini for the previously-included "private" web server is in the <install-dir>/sys/csp/bin/ folder, but the active CSP.ini is in /opt/webgateway/conf/.

Jeffrey Drumm · May 28, 2025 go to post

^Ens.AppData is the target of the $$$EnsStaticAppData macro, which is referenced in these classes:

Ens.Adapter.cls
Ens.Util.File.cls
EnsLib.EDI.EDIFACT.Operation.BatchStandard.cls
EnsLib.EDI.X12.Document.cls
EnsLib.EDI.X12.Operation.BatchStandard.cls
EnsLib.EDI.X12.Operation.FileOperation.cls
EnsLib.File.InboundAdapter.cls
EnsLib.FTP.InboundAdapter.cls
EnsLib.HL7.Operation.BatchStandard.cls
EnsLib.RecordMap.Operation.FileOperation.cls
EnsLib.SQL.InboundAdapter.cls
EnsLib.SQL.InboundProcAdapter.cls
EnsLib.SQL.Operation.GenericOperation.cls
EnsLib.SQL.Snapshot.cls

Jeffrey Drumm · May 28, 2025 go to post

You can't specify a DocType Name in a routing rule, at least directly. By specifying the docName, you're both selecting the message by Message Type/Trigger Event and identifying the structure (DocType Name) that will be used to parse the message in the rule. If you look at the HL7 v2.3 DocType Category via the Management Console in Interoperability | Interoperate | HL7 v2.x | HL7 v2.x Message Structures | 2.3, then select the ADT_A04 Message Type, you'll see this:

This means that an A04 event will be evaluated/parsed using the structure defined for an A01; the DocType Name (Message Structure) is ADT_A01.

Jeffrey Drumm · May 5, 2025 go to post

Are you dealing with multiple DocTypes and Categories of messages going to Epic, or are all messages the same schema?

I know you're not crazy about a custom operation, but if you take a look at the source for EnsLib.HL7.Operation.TCPOperation, you'll see that it would be dead simple to copy/extend it, add a check for the population of the Ordering Provider field, and the logic to populate it with a default if it's empty.

Jeffrey Drumm · May 5, 2025 go to post

From within your task, you can obtain the task's classname with $CLASSNAME() and query the %SYS.Task table using the TaskClass of your task to fetch the OutputFilename column.

If you want to use that file under your direct control, you can set "Open output file when task is running" to "yes," enter the full path of the filename, then set the previous setting back to "no." The filename will remain in the table.

If you're calling the same class under multiple Task schedules or with different configurations, the schedule values and settings are also available to help you refine your selection. Settings are stored in $LIST() format.

EDIT: You can also define a Filename property in the task class, assuming it's a custom class. It will show up as a configurable field in the task editor. That way you don't have to deal with the OutputFilename settings.

Jeffrey Drumm · May 4, 2025 go to post

Usually, OBX segments are either defined as repeating segments or members of repeating segment groups. The syntax you'll use will vary depending on the HL7 Document Category and Structure you're using in your DTL. In HL7 2.5.1, the OBX segment itself is non-repeating, but is part of a repeating group (OBXgrp) inside another repeating group (ORCgrp) inside yet another repeating group (PIDgrpgrp).

You first need to get the count of the total number of OBX segments, which you can do by supplying the "*" character in the iteration argument to the source path's OBXgrp(). Add 1 to that, and you have your iteration for the new OBX segment.

Use that value as the iteration for the new OBX segment and populate the fields as needed, as in the example below:

The above assumes that the OBX segments are the last segments in the message. If they're not and the message requires another OBX at the very end, it's a bit more complicated ... you'd create a new segment object, populate it, then use the AppendSegment method to slap it on the end of the target:

Jeffrey Drumm · Apr 27, 2025 go to post

Until such time as InterSystems provides synchronization for security components across mirror members, you can save a bit of effort by exporting them on the primary and importing them on the alternate server via the ^SECURITY routine in the %SYS namespace. At least you won't need to create them manually.

You can do the same for users, roles, resources and a few other things as well. All of these have ObjectScript methods for accomplishing the same in the Security package.

Jeffrey Drumm · Apr 17, 2025 go to post

There really is no list of "Standard" settings. You can use whatever section name you desire and it will appear in the list of settings for the business host in the production.

Most adapters provide the settings Basic, Connection, and Additional. However if the setting you're creating doesn't fit any of those categories, you can create your own with the "MySetting:MyCategory" format.

The documentation for creating business host settings can be found here.

EDIT: After reviewing the documentation, I discovered that there are a set of predefined categories and they're listed here.

Jeffrey Drumm · Apr 5, 2025 go to post

Are you using VS Code? With the Intersystems plugins installed, IRIS Terminal Lite is available on version 2025.1.

Unless the Linux system admin has set session timeouts for ssh, you shouldn't be timing out. The default settings (for Redhat and Ubuntu, at least) do not time you out.

Jeffrey Drumm · Mar 27, 2025 go to post

Glad you got it working.

I feel I should mention that I specifically told you to just that 2 days ago ... quoted here:

There are a few issues with this code, but the biggest one is that you've created a clone and should be modifying the clone (pOutput) exclusively. However, you're still calling SetValueAt() against a segment pulled from pRequest.

The above notwithstanding, you're still sending the original unchanged message object (pRequest) to the target operation with the SendRequestAsync() call.

Fundamentally, you should:

  1. Create a clone of the request object pRequest (i.e. pOutput)
  2. Modify the clone, which is a mutable copy of the original message
  3. Send the modified clone (pOutput) to the target operation
Jeffrey Drumm · Mar 26, 2025 go to post

You're cloning the source message inside the While loop that is iterating over segments within the message. That's not doing anything useful.

Clone the message, then iterate over its OBX segments, making whatever changes are necessary. Before sending it with SendRequestAsync(), do a pOutput.%Save().
This snippet below is completely untested and there are other enhancements I would make to it, but I'm not going to be available for a while, so here's something to play with. It's a cleaned up version of the code snippet you provided above. Note that there is not a single call to GetSegmentAt() ... it's an unnecessary extra step.

    // Message Subtype
    Set tMessageSubType = pRequest.GetValueAt("ORCgrp(1).ORC:1")
    //
    // Check if OBR:19 contains "Implant Usage (PSAS)" OR "Issued in Clinic (PSAS)"
    Set tOrderType = pRequest.GetValueAt("ORCgrp(1).OBRuniongrp.OBRunion.OBR:19(1).1")
    If ((tOrderType["Implant Usage (PSAS)") || (tOrderType["Issued in Clinic (PSAS)")) {
        // First loop: Count occurrences of "Item Number:"
        Set i = 1
        Set tItemNumberCount = 0
        While (pRequest.GetValueAt("ORCgrp(1).OBRuniongrp.OBXgrp("_i_").OBX")'="") {
            Set tOBXText = pRequest.GetValueAt("ORCgrp(1).OBRuniongrp.OBXgrp("_i_").OBX.5", .tStatus)
            //
            If (tOBXText [ "Item Number:") {
                Set tItemNumberCount = tItemNumberCount + 1
            }
            //
            If (tOBXText[ "Total Cost:") {
                Set tTotalCostSegment = i
                //$$$LOGINFO(tTotalCostSegment_" ------ "tTotalCostSegment check Total Cost")
            }
            Set i = i + 1
        }
        // Second loop: Modify OBX segments if necessary
        Set i = 1
        Set tItemNumberProcessed = 0
        Set pOutput = pRequest.%ConstructClone(1) // moved here because %ConstructClone() need only be done at the message object level.
        While (pOutput.GetValueAt("ORCgrp(1).OBRuniongrp.OBXgrp("_i_").OBX")'="") {
            Set tOBXText = pOutput.GetValueAt("ORCgrp(1).OBRuniongrp.OBXgrp("_i_").OBX.5", .tStatus) // using pOutput instead of pRequest
            If (tOBXText [ "Item Number:") {
                Set tItemNumberProcessed = tItemNumberProcessed + 1
            }
            If (tItemNumberCount > 1) && (tOBXText [ "REASON FOR REQUEST:") {
                set tOBXKey = i
                Set tOBXText=$p(tOBXText,":",2,5) /// OBX 5 text parsed to replace in OBX 5 later
                // Start replacement process
                Do pOutput.SetValueAt(tOBXText,"ORCgrp(1).OBRuniongrp.OBXgrp("_i_").OBX.5")
                $$$LOGINFO(tOBXText_" ---- OBX 5 parsed to be saved in new OBX 5") /// works
                Set tOBXTextChanged = pOutput.GetValueAt("ORCgrp(1).OBRuniongrp.OBXgrp("_i_").OBX.5")
                $$$LOGINFO(i_tOBXTextChanged_" --- i --- tOBXTextChanged, Is OBX 5 changed???")
            }
            Set i = i + 1
        }
    }
    Do pOutput.%Save()
Jeffrey Drumm · Mar 26, 2025 go to post

%ConstructClone() need only be done at the message object level.

And I'm not sure why you're extracting segments to update them; you can use SetValueAt() against the message object instead a segment object ... 

For example this code:

Set tOBXSegmentpOutput = pOutput.GetSegmentAt("ORCgrp(1).OBRuniongrp.OBXgrp("_i_").OBX", .tStatus)
Do tOBXSegmentpOutput.SetValueAt(tOBXText, 5)

Is functionally identical to:

Do pOutput.SetValueAt(tOBXText,"ORCgrp(1).OBRuniongrp.OBXgrp("_i_").OBX.5")

That should help simplify the code.

You shouldn't need to do a "deep clone" for this, so the "1" argument to %ConstructClone() is optional.