Skip to content

Ack Scripts

Every destination that waits for a response from the endpoint (HL7 socket (MLLP), ASTM socket, DICOM, channel queue, web service (REST or SOAP)) runs an ack script after the destination receives that response. The script's job is to inspect the response and decide whether the message completed successfully or should go to the error queue.

The script is configured on the destination node under Additional Parameters -> Acknowledgement Script (Ack Script), alongside Wait for Ack, Stop on Error, Ack Timeout, and Max Resend. See Destination Nodes -> Additional Parameters for those surrounding fields.

The response binding

The ack script runs in a Rhino JavaScript context with a response variable containing whatever the destination received back:

Destination What response holds
Socket (HL7 MLLP) The HL7 ACK as a string
Socket (ASTM) The ASTM ACK as a string
DICOM The responses array (DICOM destinations use responses, not response; see below)
Web Service (REST) The HTTP response body as a string
Web Service (SOAP) The SOAP response body (including envelope) as a string
Channel Queue The response posted by the downstream channel

For string responses, response may be null or empty if the endpoint returned nothing. Test for that explicitly before calling string methods. The destination has already decoded the response bytes into a Java String before the script runs. There is no need to re-decode response with a different charset inside the script. In particular, passing an encoding argument to qie.parseHL7String does not "convert" the ACK between UTF-8 and UTF-16.

For DICOM, the binding is responses (plural), a JavaScript array of one or more response objects, since a single DICOM association can carry multiple sub-operations. See the default DICOM ack script (below) for the loop pattern.

Other bindings available

The ack script has access to the same in-flight bindings as any mapping or condition script:

Binding Refers to
message The outbound message that this destination sent (as processed through the channel up to this destination)
source The original unmodified message received by the channel's source node
channelCache The channel-level cache
messageCache The message-level cache
sharedCache The system-wide shared cache
qie The QIE script API

Comparing the ACK against the original inbound message, for example source.getNode('MSH-10'), is a common pattern when the destination has rewritten MSH-10 before sending.

Return-value contract

What the script returns (or which helper it calls) determines what happens next:

Outcome What happens
return true (or any non-falsy value other than false / 0) Message marked completed. The response is stored as the response received.
return false (or return 0) Message sent to the error queue with error message "ACK error:" + the response text. The response is stored as the response received.
message.error(msg) Same as return false but with a custom error message instead of the "ACK error:" + response default.
qie.throwAckError(msg) Aborts the script and treats it as if no ACK was received. After Ack Timeout the message is resent, up to Max Resend times, then sent to the error queue with "message not acknowledged after being sent XXX times" (or msg if qie.throwAckError(msg) was used).
qie.throwAckError(msg, false) Aborts the script and sends the message directly to the error queue with msg as the error, with no retry. Use when the negative ACK is unambiguous and resending does not help (e.g. an HL7 MSA-1 = AE Application Error).
qie.simulateSendError(msg) Aborts the script and treats it as if the send itself never happened. The message is resent. After the destination's "Error msg after XXX consecutive send errors" threshold, the message is sent to the error queue with msg as the error. After "Stop after XXX consecutive send errors", the channel is also stopped.
Bare JavaScript throw Logs an error but otherwise behaves like the ACK was not received (same retry semantics as qie.throwAckError).

Choosing the right exit

Use this table when deciding what the script should do for a particular response:

Response says… Use… Why
Endpoint accepted the message return true Normal success path.
Endpoint explicitly rejected (e.g. HL7 MSA-1 = AR/AE, SOAP <RegistryError>, REST 400) return false or message.error(msg) or qie.throwAckError(msg, false) Permanent failure; the message should go to the error queue without retry. Pick message.error or the 2-arg throwAckError when you want a custom error message.
No response at all (empty / null) within the timeout qie.throwAckError(msg) (1-arg) or qie.simulateSendError(msg) Both retry. Use throwAckError for ack-timeout-style retries (governed by Max Resend); use simulateSendError for send-error-style retries (governed by Error Management thresholds).
Transient failure (network blip, HTTP 503, soft error) qie.simulateSendError(msg) Resends as if the send never happened; the destination's send-error counters apply.
Endpoint accepted the message but you want to log a warning return true after qie.warn(msg) or qie.info(msg) Successful completion with a log entry.

Default scripts

QIE installs a default script on every new ack-aware destination. These are good starting points and can be edited freely.

HL7 MLLP / ASTM (the default for socket destinations)

if (response === null || response.length === 0) {
   return false;
}
var ackMessage = qie.parseHL7String(response);
var msgId = message.getNode('MSH-10');
var ackId = ackMessage.getNode('MSA-2');
if ((msgId.equals('') && ackId.equals('')) || !msgId.equals(ackId)) {
   qie.throwAckError("Ack id (" + ackId + ") not equal to message id (" + msgId + ")");
}
if (ackMessage.getNode('MSA-1').endsWith('A')) {
   return true;
} else {
   qie.throwAckError("Ack Error: " + ackMessage.getNode('MSA-3'), false);
}

What this script does:

  1. Empty / missing response → return false (message to error queue).
  2. Parse the response as HL7 with qie.parseHL7String.
  3. Confirm the ACK's MSA-2 (Message Control ID being acknowledged) matches the outbound message's MSH-10. If not, retry via throwAckError.
  4. If MSA-1 ends with A (i.e. AA, Application Accept), return true. Anything else (AE, Application Error, and AR, Application Reject) is permanent: throwAckError with resendMessage=false sends to the error queue with the MSA-3 text as the error.

DICOM

The DICOM default uses the responses array binding and checks the DIMSE status code (/0000,0900). See the source for the full default; it loops the response array, treats 0x0000 as success and 0xFF00/0xFF01 as pending (more responses to come), and converts 0xFE00/0xA700/0xA900/0x0122 and any other status into an error via qie.throwAckError("Ack Error", false).

REST / SOAP

Web service destinations fall back to the same HL7 MSA-validating default as HL7/ASTM/socket destinations. It expects the response to be an HL7 ACK whose MSA-2 echoes the sent MSH-10. That check cannot succeed for an arbitrary REST/SOAP response (with or without a body), so a Web Service destination using Wait for Ack effectively requires a custom ack script. There is no universal "success" check for an arbitrary HTTP response. Write the script to match what the endpoint actually returns.

Patterns

REST endpoint returning a JSON status

if (response == null || response.length === 0) {
   qie.simulateSendError('Empty response from REST endpoint.');
}
var json = qie.createJSONMessage(response);
if (json.getNode('/status') === 'ok') {
   return true;
}
message.error('Endpoint rejected: ' + json.getNode('/error/message'));
return false;

SOAP response carrying a registry error

The full pattern for an XDS.b ITI-41 submission is in Sending a CDA Document to an HIE. The pattern generalizes to any SOAP endpoint where the HTTP layer always returns 200 OK and the real outcome is in the SOAP body:

if (response == null || response.length === 0) {
   qie.simulateSendError('No SOAP response received.');
}
if (response.indexOf('<faultstring>') >= 0 || response.indexOf('RegistryErrorList') >= 0) {
   var xml = qie.createXMLMessage(response);
   var err = xml.getNode('//faultstring') || xml.getNode('//RegistryError/@codeContext');
   message.error('SOAP fault: ' + err);
   return false;
}
return true;

SOAP-wrapped HL7 ACK

When you send HL7 over a SOAP web service, the endpoint typically returns the HL7 ACK wrapped inside the SOAP envelope rather than as raw HL7. A typical response body looks like:

<Envelope>
   <Body>
      <HL7RequestResponse>
         <HL7RequestResult>MSH|^~\&amp;||QVERA|||20210921124548||ACK^Q11^ACK_Q11|3304421|P|2.5.1|||ER||||||
MSA|AA|1664213420006120
</HL7RequestResult>
      </HL7RequestResponse>
   </Body>
</Envelope>

Two extra lines at the top of the default HL7 ack script are enough to unwrap it, the rest of the default works unchanged once the HL7 is back in response:

if (response === null || response.length === 0) {
   return false;
}

response = qie.parseXMLString(response);
response = qie.parseHL7String(response.getNode('/Envelope/Body/HL7RequestResponse/HL7RequestResult'));

var ackMessage = qie.parseHL7String(response);
var msgId = message.getNode('MSH-10');
var ackId = ackMessage.getNode('MSA-2');
if ((msgId.equals('') && ackId.equals('')) || !msgId.equals(ackId)) {
   throw "Ack id (" + ackId + ") not equal to message id (" + msgId + "): '" + response + "'";
}
return ackMessage.getNode('MSA-1').endsWith('A');

Note

The wrapping element names (HL7RequestResponse, HL7RequestResult) and the envelope path vary by endpoint. Adjust the getNode(...) XPath to match what your endpoint actually sends.

How the script interacts with destination settings

The script's behavior is shaped by these destination-node fields (see Destination Nodes -> Additional Parameters for the full list):

Field Effect
Wait for Ack When unchecked, the ack script does not run at all. Every send is treated as successful.
Stop on Error When checked, a negative ACK (or an ack-timeout) stops the channel and sends an email alert.
Ack Timeout (default 15000 ms) How long the destination waits for the endpoint's response before timing out. A timeout triggers the same retry path as qie.throwAckError(msg).
Max Resend (default 5) After this many timeouts in a row, the message is sent to the error queue.
Error Management -> Error msg after XXX consecutive send errors Governs qie.simulateSendError. After this many in a row, the message is sent to the error queue.
Error Management -> Stop after XXX consecutive send errors After this many simulateSendError calls in a row, the channel is also stopped.

Debugging an ack script

  1. Put the channel in debug mode before running test messages, the status log records [path=N-N]: SOAP Request: / [path=N-N]: SOAP Response: (web service destinations) or the inbound ACK content (socket destinations).
  2. Open the response from the status log to verify what the endpoint actually sent.
  3. Use qie.info(msg), qie.warn(msg), or qie.debug(msg) inside the script to log intermediate values without affecting the return value.
  4. If the script throws an unexpected JavaScript error, the destination treats the error like no-ack-received, the message retries until Max Resend, then go to the error queue. Look for the underlying error in the channel's log entries.

Log: "Discarded prior ACK"

If a destination retries a send before the previous attempt's ACK has been processed, for example, the channel hits Ack Timeout and resends, then the late ACK from the original attempt finally arrives, QIE keeps the newer ACK and logs a WARN line on the older one:

Discarded prior ACK : <prior ACK content>

The message itself is still being processed against the newer ACK; the warning is informational and is emitted by the HL7 MLLP, ASTM, Web Service, and DICOM destinations alike. If it appears repeatedly without an obvious reason (the downstream is not measurably slow, Ack Timeout was not recently lowered) investigate whether the receiver is genuinely exceeding the timeout. Raising Ack Timeout or lowering Max Resend is usually the right response; both fields live in the destination's Additional Parameters.