Yes. In addition, to prevent the default Ens.BusinessService behavior to call adapter OnTask() every CallInterval seconds, you can override the OnTask() method of the business service.
And while we are at it 😁 add  a TriggerService() class method, and use it in the scheduled task to trigger the service adapter just once :
 


Class dc.sql.TriggeredSQLService Extends EnsLib.SQL.Service.GenericService
{

Method OnTask() As %Status
{
    // do nothing
}

Method Trigger() As %Status
{
    return ..Adapter.OnTask()
}

ClassMethod TriggerService(configName As Ens.DataType.ConfigName) As %Status
{
  #Dim sc as %Status
  #Dim ex as %Exception.AbstractException

  #Dim service as Ens.BusinessService
  
  s sc = $$$OK
  try {
    $$$TOE(sc,##class(Ens.Director).CreateBusinessService(configName,.service))
    if service.%Extends($classname()) {
        $$$TOE(sc,service.Trigger())
    }    
  }
  catch (ex) {
    s sc = ex.AsStatus()
  }
  return sc
}

}

For polling adapters such as EnsLib.SQL.InboundAdapter, calling the adapter's OnTask() method should trigger one run. Something like

ClassMethod CallServiceAdapter(targetConfigName As Ens.DataType.ConfigName) As %Status
{
  #Dim sc as %Status
  #Dim ex as %Exception.AbstractException

  #Dim service as Ens.BusinessService
  
  s sc = $$$OK
  try {
    $$$TOE(sc,##class(Ens.Director).CreateBusinessService(targetConfigName,.service))
    $$$TOE(sc,service.Adapter.OnTask())
  }
  catch (ex) {
    s sc = ex.AsStatus()
  }
  return sc
}

Another caveat is that some systems do not map all Unicode characters to their internal encoding while preserving the original character count.

This becomes problematic in combination with using fixed-length records.

I've seen this in the wild, a target system using an EBCDIC code page and translating Unicode characters with diacritics that do not exist in the code page into a 2 (or more !) character sequence.

This occured, for example, with Eastern Europe first or last names containing characters such as ǎ — Unicode: U+01CE (latin small letter a with "caron") that got translated to a - rather ugly - "a?" sequence.

The encoding behavior of HL7 services and operations in EnsLib.HL7 package is driven by 

  • the value in the message MSH:18 field 
  • the value of the DefCharEncoding property of EnsLib.HL7.Util.IOFraming, which is available on HL7 services and operations

If MSH:18 has a value, it is used to determine character encoding.

If MHS:18 is empty or DefCharEncoding property starts with "!", characters are encoded according to it's value, refer to the documentation.

Beware, I've seen more than once sloppy HL7 v2.x interface implementations that are sending messages with an MSH:18 value mismatching the actual character encoding (use ! to fix this).

To check what is actual encoding of an incoming message, capture the message bytes and open then with a text editor such a Notepad++, or view them with an hex editor, 😅 who's still using one of those ?

Hi Evgeny,
In ISOS, the "quit" command is inherited from MUMPS ("Q[UIT]") and has two forms (see the documentation for details) : 

  • with arguments : terminates a method, function or routine, returning it's arguments as value
  • without arguments : exits the current context (e.g. for, while, ...)

The "return" command was introduced by InterSystems, I think in Caché 2016.2  (@all please, correct me if I'm wrong).  It was added to avoid confusion, making ISOS clearer and also more aligned with 'modern' programming conventions.
"return" is equivalent to "quit" with argument, thus is has only one form and one, clear meaning : returning a value.
When the code intent is to return a value, nowadays I favor using "return".

p.s. IMHO, generative AIs, while useful in some contexts, are nor artificial, because they are trained with human sourced corpus, nor intelligent, because they are not able of any real creativity and are not doing any reasoning (even if they can mimic both rather well) 😇

Hi Evgeny,
Thank you for sharing the details, now I see why you have so much lines in a single class 😊
As an example of what Scott and Robert are explaining in their excellent answers, when dealing with a REST API spec defining (mostly) CRUD operation on many resources, I think one way to divide and, hopefully, conquer is to delegate the implementation of operations related to each resource to a separate class.
Taking as example petstore, for which, of course, this is an overkill, this would result in five classes :

  1. Class dc.petstore.impl Extends %REST.Impl : (generated), all endpoints class methods, with most bodies being single line call to another class method
  2. Class dc.petstore.Commons : logic common to all the classes below
  3. Class dc.petstore.PetOperations Extends Commons : class methods dealing with 'pet' resource (CRUD + other)
  4. Class dc.petstore.StoreOperations Extends Commons : same for 'store' resource
  5. Class dc.petstore.UserOperation Extends Commons : same for 'user' resource

dc.petstore.impl class methods simply delegate the call to resource class, e.g.

ClassMethod findPetsByStatus(status As %String) As %DynamicArray
{
    return ##class(PetOperations).FindByStatus(status)
}

Hope this helps

Why is there so many lines in this class ? 

From code quality point of view, a class exceeding 500 lines is usually considered a candidate for refactoring.

Is the code generated ?

Following these two insightful days, I wanted to thank you for the incredibly warm welcome and kind feedback. 
Here is my Bingo card 😁


🖼 Musée du Quai Branly

  • Photo of something at Musée du Quai Branly that sparked your curiosity

Le Jardin d’O outside the Museum

  • Describe one Musée du Quai Branly exhibit in three words

Much common ancestry!

The Diola mask from Senegal with horns, woven leafs canvas and seeds, share similarities with Celt masks of Cernunos made of plants, fruits and deer horns

  • An exhibit you’d like to revisit and why

I'm particularly interested in the musical instrument collection, we did not have the time to visit

  • Your favorite song sung by Laure Poissonnier at the Gala dinner

« Take Me to the Moon » 🌙

Shoot for the moon, even if you miss, you’ll land among the stars — Oscar Wilde

🎤 Sessions & Ideas

  • A project you discovered at READY and one question you have about it

CHU Toulouse FHIR + interop, a very inspiring design. I wonder what is the strategy for change/release management, such as FHIR release migration.

  • The most unexpected analogy used by a speaker

Zweder Bergman talking about how difficult it is to pick a bakery in Paris and comparing this to the many choices facing us with FHIR interoperabiliy

  • One key takeaway from a session

From « Care Coordination: From Patient Journey to Sustainable Population Health » : public health agencies often do not measure the value of prevention and how quality data exchange networks contribute to cost reduction.

  • A session outside your usual role and what surprised you

Retailer SPAR putting InterSytems technologies to effective and global use on such a large scale is quite surprising to me, I wonder how it initially sparked.

☕️ Informal Moments

  • A READY conversation that made you laugh

Chatting about where we are now, in a world obsessed by AI, with MUMPS developers about my age 😁

  • Your favorite non-session moment of the day

Meeting Egveny Shvarov, Guillaume Rongier, Irène Mykhailova and Anastasia Dyubaylo in real life : we are now more than avatars to each other 😊

  • One insight gained outside a session

The vast collection of python libraries is now available to IRIS, and it’s especially useful when dealing with AI. Thanks @Guillaume Rongier 
 for pointing this out !

  • Your favorite informal discussion (coffee, lunch, evening)

During the evening, chatting about using a FHIR repository a central storage for an hospital, and how it empowers the institution vs data being captive in supplier software. Also outlining how it is difficult to convince hospital boards and software suppliers to invest in the technology, the ROI being on the long term.

🤝 Reflection & Connections

  • Photo of your favorite session

  • Your READY moment of the day (one emoji + one sentence)

Learning more about EHDS initiative 😎

  • Exchange business cards or LinkedIn with two attendees

I offered some cards during informal moments, but cards seems to be somewhat deprecated, and attendees did not have a card to exchange.

  • Meet someone new at READY and tag them in your post

It was a pleasure to meet @Lorenzo Scalese  in real life, and sharing about developping with ObjectScript.

My rules, motivated by readability :

  • use #dim with no inline assignment
  • group dims at the beginning of the method/routine
  • use set to assign variables
Robert Barbiaux · Oct 30, 2025 go to post

By the look of the stack trace, I think the adapter is trying to write request but it fails. Check the SSLConfiguration setting of the operation, it should be the name of an existing, valid, TLS configuration.

Robert Barbiaux · Sep 25, 2025 go to post

It is also possible to specify an ODBC connection string instead of system DSN name :

Driver={ODBC Driver 13 for SQL Server};server=localhost;database=WideWorldImporters;trusted_connection=Yes;

Robert Barbiaux · Aug 27, 2025 go to post

I would check out EnsLib.REST.DynamicObjVDoc, looks like it can parse something like JSONPath.

Robert Barbiaux · Jun 26, 2025 go to post

In interop Ens.MessageHeader, we have TimeCreated and TimeProcessed. I would along the same lines and have TimeCreated and TimeUpdated. I find Time… more appropriate as those are time stamps, not dates.

Robert Barbiaux · Jun 25, 2025 go to post

Use the concatenation operator, _ (underscore) in the value of the assign action. Something like : source.{EVN:5.2}_source.{EVN:5.3}

Robert Barbiaux · Apr 30, 2025 go to post

Absolutely. The new nomenclature does not reflect the conceptual differences between services and operations : the origin of the initial event that triggers processing. This could cause confusion for beginners.

I like ‘Services’ and ‘Operations’ better. The words also keep the link with the actual class names and wording in the documentation.

Robert Barbiaux · Mar 22, 2025 go to post

Error message 

ERROR #5001: element 'sql' is not allowed for content model '(annotation?,((true,false)|(false,true)|(true)|(false)))'
Indicates the <sql> element in the DTL XData block is in an unexpected location in the xml document, and that expected elements in that location are <annotation>, <true> or <false>. Maybe it is directly under <if> ?

Robert Barbiaux · Mar 10, 2025 go to post

Congratulations everyone, and thank you so much for making such an excellent community of developers possible !💐

Robert Barbiaux · Feb 22, 2025 go to post

You can query the message body class table with a left join to Ens.MessageHeader to get the orphaned message body identifiers :

select 
  %NOLOCK bod.Id
from
  MySample.BodyTable bod
  left join Ens.MessageHeader hdr on bod.Id=hdr.MessageBodyId and hdr.MessageBodyClassName='MySample.BodyTable'
where
 hdr.MessageBodyId is NULL
Robert Barbiaux · Feb 6, 2025 go to post

An approach using an operation that, on message : 

  • enqueue incoming message in a queue named after patient identifier
  • dequeue and process message from all existing patient queues

If PoolSize = 1, it will process 1 message at a time.

If PoolSize > 1, multiple jobs will process incoming messages from other operations in sequence for each patient

Class dc.interop.PatientMessageOperation Extends Ens.BusinessOperation
{

Parameter SETTINGS = "DeQueueTimeout,MinProcessTime,MaxProcessTime";
Property DeQueueTimeout As %Integer [ InitialExpression = 1 ];
Property MinProcessTime As %Integer [ InitialExpression = 1 ];
Property MaxProcessTime As %Integer [ InitialExpression = 5 ];
Method HandlePatientMessage(request As test.src.ut.ks.lib.interop.PatientMessage, Output response As %Persistent) As %Status
{
    #Dim sc as %Status
    #Dim ex as %Exception.AbstractException
    s sc = $$$OK
    try {
        $$$TOE(sc,..EnqueueRequest(request))
        $$$TOE(sc,..ProcessPatientMessages())
    } catch (ex) {
      s sc = ex.AsStatus()
    }
    return sc
}

Method EnqueueRequest(request As PatientMessage) As %Status
{
    #Dim sc as %Status
    #Dim ex as %Exception.AbstractException
    #Dim queue as Ens.Queue
    #Dim queueName as %String
    #Dim s as EnsLib.File.InboundAdapter
    
    s sc = $$$OK
    try {
        s queueName = ..%ConfigName_".patient."_request.PatientId
        $$$TOE(sc,##class(Ens.Queue).Create(queueName))
        s ..%RequestHeader.TargetQueueName = queueName        
        $$$TOE(sc,##class(Ens.Queue).EnQueue(..%RequestHeader))   
        s $$$EnsRuntimeAppData(..%ConfigName,"queues",queueName) = 1
    } catch (ex) {
      s sc = ex.AsStatus()
    }
    return sc
}

Method ProcessPatientMessages() As %Status
{
    #Dim sc as %Status
    #Dim ex as %Exception.AbstractException
    s sc = $$$OK
    try {
      s queueName = ""
      s queueName = $order($$$EnsRuntimeAppData(..%ConfigName,"queues",queueName))
      while queueName '= "" {
        $$$TOE(sc,..ProcessQueuedMessages(queueName))
        s queueName = $order($$$EnsRuntimeAppData(..%ConfigName,"queues",queueName))
      }
    } catch (ex) {
      s sc = ex.AsStatus()
    }
    return sc
}

Method ProcessQueuedMessages(queueName As %String) As %Status
{
    #Dim sc as %Status
    #Dim ex as %Exception.AbstractException
    #Dim header as Ens.MessageHeader
    #Dim timedOut as %Boolean
    s sc = $$$OK
    try {
        $$$TOE(sc,##class(Ens.Queue).DeQueue(queueName,.header,..DeQueueTimeout,.timedOut,1))
        while 'timedOut && $isobject(header) {          
          $$$TOE(sc,..ProcessHeader(header))
          $$$TOE(sc,##class(Ens.Queue).DeQueue(queueName,.header,..DeQueueTimeout,.timedOut,1))
        }
    } catch (ex) {
      s sc = ex.AsStatus()
    }
    return sc
}

Method ProcessHeader(header As Ens.MessageHeader) As %Status
{
    #Dim sc as %Status
    #Dim ex as %Exception.AbstractException
    #Dim msg as PatientMessage
    
    s sc = $$$OK
    try {
        s msg = $classmethod(header.MessageBodyClassName,"%OpenId",header.MessageBodyId)
        $$$TOE(sc,..ProcessMessage(msg))
    } catch (ex) {
      s sc = ex.AsStatus()
    }
    return sc
}

Method ProcessMessage(msg As PatientMessage) As %Status
{
    #Dim sc as %Status
    #Dim ex as %Exception.AbstractException
    #Dim processTime as %Integer
    s sc = $$$OK
    try {
        $$$TRACE("job "_$job_" is processing message for patient "_msg.PatientId_" seq "_msg.Seq)
        s processTime = $random(..MaxProcessTime-..MinProcessTime)+..MinProcessTime
        hang processTime
        $$$TRACE("job "_$job_" processed message for patient "_msg.PatientId_" seq "_msg.Seq_" in "_processTime_" seconds")
    } catch (ex) {
      s sc = ex.AsStatus()
    }
    return sc
}

Method OnTearDown() As %Status
{
    #Dim sc as %Status
    #Dim ex as %Exception.AbstractException
    s sc = $$$OK
    try {
      s queueName = ""
      s queueName = $order($$$EnsRuntimeAppData(..%ConfigName,"queues",queueName))
      while queueName '= "" {
        $$$TOE(sc,##class(Ens.Queue).Delete(queueName,"*"))
        s queueName = $order($$$EnsRuntimeAppData(..%ConfigName,"queues",queueName))
      }
    } catch (ex) {
      s sc = ex.AsStatus()
    }
    return sc
}

XData MessageMap
{
<MessageMap>
     <MapItem MessageType="ut.ks.lib.interop.PatientMessage">
      <Method>HandlePatientMessage</Method>
     </MapItem>
    </MessageMap>
}

}

The message class

Class ut.ks.lib.interop.PatientMessage Extends Ens.MessageBody
{

Property PatientId As %Integer;
Property Seq As %Integer;
Property Payload As %String;
Storage Default
{
<Data name="PatientMessageDefaultData">
<Subscript>"PatientMessage"</Subscript>
<Value name="1">
<Value>PatientId</Value>
</Value>
<Value name="2">
<Value>Seq</Value>
</Value>
<Value name="3">
<Value>Payload</Value>
</Value>
</Data>
<DefaultData>PatientMessageDefaultData</DefaultData>
<Type>%Storage.Persistent</Type>
}

}

Sample test method

ClassMethod SendMessages(patientCount As %Integer = 100, messageCount = 1000) As %Status
{
    #Dim sc as %Status
    #Dim ex as %Exception.AbstractException
    s sc = $$$OK
    try {
        
        for i = 1:1:messageCount {
            s msg = ##class(PatientMessage).%New()
            s msg.PatientId = $random(patientCount)+1
            s msg.Seq = $increment(seq(msg.PatientId))
            s msg.Payload = "message "_msg.Seq_" for "_msg.PatientId
            $$$TOE(sc,##class(EnsLib.Testing.Service).SendTestRequest("PatientOperation",msg))
        }        
    } catch (ex) {
      s sc = ex.AsStatus()
    }
    return sc
}
Robert Barbiaux · Feb 5, 2025 go to post

Another approach would be to write a custom router class extending EnsLib.MsgRouter.RoutingEngine, overriding OnRequest() method.

In OnRequest(), before calling ##super(), assign the JSON %DynamicAbstractObject build from the input request stream to a property.

Use the property in the router rules.

Robert Barbiaux · Jan 24, 2025 go to post

You can override the Page() method of %CSP.Page and redirect output to a stream, process the resulting stream and write it to the original device the page is using to send data back to client.
Here is a quick and dirty example, using IO-Redirect package available on OpenExchange.
The redirecting page : 

Class test.src.RedirectedPage Extends %CSP.Page
{

ClassMethod Page(skipHeader As %Boolean = 1) As %Status [ ServerOnly = 1 ]
{
    #dim sc as %Status
    #dim ex as %Exception.AbstractException
    #dim pageStream,processedPageStream As %Stream.Object
    #dim len as %Integer
    #dim buffer as %String
    
    s sc = $$$OK
    try {
    
    Set pageStream = ##class(%Stream.GlobalCharacter).%New()
    Do ##class(IORedirect.Redirect).ToStream(pageStream)
    $$$TOE(sc,##super(skipHeader))
    Do ##class(IORedirect.Redirect).RestoreIO()
    Set pageStream = ##class(IORedirect.Redirect).Get()
    $$$TOE(sc,..ProcessPageStream(pageStream,.processedPageStream))
    while 'processedPageStream.AtEnd {
        s len = 32768
        s buffer = processedPageStream.Read(.len,.sc)
        $$$TOE(sc,sc)
        write buffer
    }
    } catch (ex) {
      s sc = ex.AsStatus()
    }
    return sc
}

ClassMethod ProcessPageStream(pageStream As %Stream.Object, Output processedPageStream As %Stream.Object) As %Status
{
    #dim sc as %Status
    #dim ex as %Exception.AbstractException
    s sc = $$$OK
    try {
        s processedPageStream = ##class(%Stream.TmpCharacter).%New()
        $$$TOE(sc,processedPageStream.CopyFrom(pageStream))
        d processedPageStream.Write("<div><span>original page had "_pageStream.Size_" bytes </span></div>")
    } catch (ex) {
      s sc = ex.AsStatus()
    }
    return sc
}

}

The original page :

Class test.src.OriginalPage Extends test.src.RedirectedPage
{

ClassMethod OnPage() As %Status [ ServerOnly = 1 ]
{
    &html<
        <div>
         <span>Hello, world, again</span>
        </div>
    >
    return $$$OK
}