That's correct (although from your example I would use $get(%request.Data("Q31",1)) over the "Get" method). I had another look and an assumption I had made in my previous post was partially incorrect. At least in questionnaires you can safely check the IsNew property of %UDF - this is correctly set when a questionnaire is created using either an API or through the UI. This value is derived from a variable that is set at the very top of the save so it allows us to identify a new added record. Here is a fairly simple (if a little crude) example:


ClassMethod Test()
{
	tstart
	do ..GetAPI(.objAPI)
	set objAPI.QUESPAPatMasDR = 5878
	
	set objAPI.QUESPAAdmDR = 11480
	set objAPI.Q01 = "Y"
	set rc = objAPI.Save()
	zwrite {"Q01":(objAPI.Q01)}
	set id = objAPI.ID
	kill objAPI
	set obj = ##class(questionnaire.QAUXXARP).%OpenId(id)
	
	zwrite {"Q01":(obj.Q01)}
	kill obj
	do ..GetAPI(.objAPI)	
	set rc = objAPI.Open("",id)
	set objAPI.Q01 = "Y"
	set rc = objAPI.Save()
	zwrite {"Q01":(objAPI.Q01)}
	kill objAPI
	set obj = ##class(questionnaire.QAUXXARP).%OpenId(id)
	zwrite {"Q01":(obj.Q01)}
	trollback
}

ClassMethod OnAfterSave() As %Boolean
{
	#dim %UDF as websys.UDFExt.TrakEvent
	set sc = $$$OK
	write !,"%UDF.IsNew = "_%UDF.IsNew,!
	if %UDF.IsNew {
		set obj = ##class(questionnaire.QAUXXARP).%OpenId(%UDF.Id,,.sc)
		if $isobject(obj) {
			set obj.Q01 = "N"
			set sc = obj.%Save()
		}
	}
	if $$$ISERR(sc) {
		do %UDF.Message.Status(sc)
	}
	return 1
}

do ##class(Custom.AUXX.UDF.questionnaire.QAUXXARP).Test()

%UDF.IsNew = 1
{"Q01":"Y"}  ; <DYNAMIC OBJECT>
{"Q01":"N"}  ; <DYNAMIC OBJECT>

%UDF.IsNew = 0
{"Q01":"Y"}  ; <DYNAMIC OBJECT>
{"Q01":"Y"}  ; <DYNAMIC OBJECT>

As you can see, the value of "Q01" is only set to "N" on the first save and is not updated afterwards.

Ah. Thanks for clarifying. That makes a bit more sense now but also raises another question. Are you creating the questionnaire through an API call, or is the questionnaire being added/updated as a result of an API call to a different component? If it's the former, you should just be able to populate the property when it's created. 

I can't think of any reason why OnBeforeSave wouldn't fire in any case. I can have a look on Monday when I'm in front of my work computer. I've done similar to what you're trying to do and I would usually set a flag using a PPG in OnBeforeSave if %request.Data("ID",1)="" and then check that in the OnAfterSave and update the item then. Alternatively if there's no ID (like above), you could set %request.Data(<component item name>,1) = 1 in OnBeforeSave and the %request value would be used when it populates the object values to save. 

There are a few items to consider with the example above:

  1. I would expect that UDFs are being triggered through the "Features Framework" which acts as a "wrapper" of sorts for User Defined Functions. It allows us to define multiple methods for a class and event which can be enabled or disabled based on site requirements. If you're adding calls to websys.UserDefinedFunctions and the Features Framework is enabled, you will lose the manually added call next time a patch is applied.
  2. The features framework explicitly protects %d and %old so they cannot be overwritten or modified inside an SQL trigger as doing so can have unpredictable results - a large number of TrakCare classes use SQLStorage.
  3. We need to know what you're trying to achieve. There are a couple of approaches to setting default values like this.
  4. UDFs return %Boolean values, not %Status codes.
Jolyon Smith · Aug 7, 2025 go to post

Using a device mnemonic routine and redirecting IO also seems to work. I used this very basic routine to log to IO:

ZJES01	;
rchr(c)      quit
	#;Read a string - we don't care about reading
rstr(sz,to)  quit
	#;Write a character - call the output label
wchr(s)      do output($char(s))  quit
	#;Write a form feed - call the output label
wff()        do output($char(12))  quit
	#;Write a newline - call the output label
wnl()        do output($char(13,10))  quit
	#;Write a string - call the output label
wstr(s)      do output(s)  quit
	#;Write a tab - call the output label
wtab(s)      do output($char(9))  quit
	#;Output label - this is where you would handle what you actually want to do.
	#;  in our case, we want to write to str
output(s) set ^ZJES($increment(^ZJES))=s quit

And then used the following classmethod to test:

ClassMethod TestRedirect()
{
	
	set MnemonicRoutine = ##class(%Device).GetMnemonicRoutine()
	use $io::("^ZJES01")
	set RedirectIO=##class(%Device).ReDirectIO(1)
	do ..PythonPrint()
	do ##class(%Device).ReDirectIO(RedirectIO)
	use $io::("^"_MnemonicRoutine)
	quit
}

Which gave me:

zwrite ^ZJES
^ZJES=3
^ZJES(1)="Python print this line to the buffer"
^ZJES(2)=$c(13,10)
^ZJES(3)="IRIS write this line to the buffer"
Jolyon Smith · Aug 7, 2025 go to post

How is the buffer class capturing the output? I did a very quick testing using the spool device and a USE statement and the output appeared in ^SPOOL as expected:

/// Open spool device and call Embedded Python method.
ClassMethod TestSpool()
{
	set SpoolFile = $order(^SPOOL(""),-1)+1
	open 2:SpoolFile
	use 2
	do ..PythonPrint()
	close 2
	break
}

ClassMethod PythonPrint() [ Language = python ]
{
	import iris 
	print("Python print this line to the buffer")
	iris.execute('write "IRIS write this line to the buffer"')
}
zwrite SpoolFile
SpoolFile=956

zwrite ^SPOOL(956)
^SPOOL(956,1)="Python print this line to the buffer"_$c(10)
^SPOOL(956,2)="IRIS write this line to the buffer"
^SPOOL(956,2147483647)="{67424,57942{3{"
Jolyon Smith · Jul 27, 2025 go to post

You can use the SQL DATEDIFF function in ObjectScript to get the number of minutes and divide by 60:

write $system.SQL.Functions.DATEDIFF("mi","2024-07-12 08:30:00","2024-07-12 15:15:00")/60  
6.75

Documentation is here: https://docs.intersystems.com/iris20252/csp/documatic/%25CSP.Documatic…

You can pass "hh" in as the first argument to get hours, but only returns whole hours rounded up. If you need second accuracy, you can pass in "ss" for "seconds" and divide by 3600 instead.

Jolyon Smith · Jun 6, 2023 go to post

You can also set this at a workspace or user level by going into Settings -> Extensions -> InterSystems ObjectScript and updating the "ObjectScript Compile Flags" setting:

 

Jolyon Smith · Oct 20, 2022 go to post

You don't need to change the "%" to a "_" in this instance as the cls method is being passed a string literal. This works fine:

print(iris.cls('%SYSTEM.OBJ').Version())
InterSystems IRIS Version 2022.1.0.209

Jolyon Smith · Oct 6, 2022 go to post

There's a method on the iris.gref class called "data".

set ^zJES(1)="$data = 1"
set ^zJES(2,0)="$data = 10"
set ^zJES(4) = ""
set ^zJES(4,0) = ""

The result ends up matching $data:

>>> glb = iris.gref("^zJES") 
>>> print(glb.data())
10
>>> print(glb.data([1])) 
1
>>> print(glb.data([2]))
10
>>> print(glb.data([3]))
0
>>> print(glb.data([4]))
11

I found running help(iris) at the Python shell helpful for working this kind of stuff out.

Edit: Unlike @Robert Cemper's solution this does not return the value of the node we're testing (like the 2 parameter usage $data)

Jolyon Smith · Jul 13, 2022 go to post

It's probably worth mentioning that this was always the limit. Prior to IRIS 2020.1 (from memory) any characters after the first 31 were just ignored, so in the above example the global would be truncated to ^Jobcosting.JobActivityGroupGrou. There is now a hard stop in the class compiler that prevents global names longer than the limit as there was the potential for truncated global names to clash.

Jolyon Smith · Jun 21, 2022 go to post

One option is $select:

    set a=$select(a>b:1,1:0)

You could could also put the if..else on a single line:

    if (a<b) { set a=1 } else { set a=0 }

This would also achieve the same result, but is a bit more limited:

    set a=(a>b)
Jolyon Smith · Jun 7, 2022 go to post

I've found that going to Help -> Toggle Developer Tools can be a useful way to identify connection issues with HTTPS. VSCode is built on Chromium, so you have access to the same debug console.

Jolyon Smith · Dec 4, 2018 go to post

In Studio if you go to "Options"-> "Editor" -> "Keyword Expansion", you can change whether it uses uppercase, lower case, mixed case or retains the original case when you CTRL+E

Jolyon Smith · Sep 14, 2018 go to post

A couple of small pieces of feedback: 

  1. %Close() doesn't do anything. You can call result.Close(), but if your intention is to destroy the object, you should just kill it.
  2. Using $get(result.Data("Name")) will be quicker than using GetDataByName.
Jolyon Smith · Mar 14, 2018 go to post

You can also try overriding the GUIDENABLED parameter from %Persistent:

Class User.Test1 Extends %Persistent

{

Parameter GUIDENABLED = 1;

Property Property1 as %String;

Storage Default

{

<Data name="Test1DefaultData">

<Value name="1">

<Value>%%CLASSNAME</Value>

</Value>

<Value name="2">

<Value>Property1</Value>

</Value>

</Data>

<DataLocation>^User.Test1D</DataLocation>

<DefaultData>Test1DefaultData</DefaultData>

<IdLocation>^User.Test1D</IdLocation>

<IndexLocation>^User.Test1I</IndexLocation>

<StreamLocation>^User.Test1S</StreamLocation>

<Type>%Library.CacheStorage</Type>

}


}

Will give you:

USER>set obj = ##class(Test1).%New()

USER>set obj.Property1 = "Hello"

USER>w obj.%Save()

1

USER>w obj."%%GUID"

FC381F94-277E-11E8-82F0-005056B479AA

The ^OBJ.GUID index that this creates can be accessed via %ExtentMgr.GUID.

Jolyon Smith · Sep 20, 2016 go to post

You could improve that further by using the 3 parameter version of $order to fetch "Data". i.e:

Set Sub3=$Order(^Trans(Sub1,Sub2,Sub3))
If Sub3="" Quit
Write ?6,Sub3," = "
Set Data=(^Trans(Sub1,Sub2,Sub3))
Write Data,!

Becomes:

Set Sub3=$Order(^Trans(Sub1,Sub2,Sub3),1,Data)
If Sub3="" Quit
Write ?6,Sub3," = ",Data
Write Data,!

Jolyon Smith · Jul 19, 2016 go to post

It might be worth mentioning that you can insert a line directly after the last offset that's been zprinted by simply hitting <tab> , entering the line(s) and zsaving.