diff --git a/all.sas b/all.sas
index 3faa7ca4..ac2e2f53 100644
--- a/all.sas
+++ b/all.sas
@@ -23114,6 +23114,236 @@ run;
%mend ms_testservice;
/**
+ @file
+ @brief Triggers a SASjs Server STP using the /SASjsApi/stp/trigger endpoint
+ @details Triggers the STP and returns the sessionId
+
+ Example:
+
+ %ms_triggerstp(/some/stored/program
+ ,debug=131
+ ,outds=work.myresults
+ )
+
+ @param [in] pgm The full path to the Stored Program in SASjs Drive (_program
+ parameter)
+ @param [in] debug= (131) The value to supply to the _debug URL parameter
+ @param [in] mdebug= (0) Set to 1 to enable DEBUG messages
+ @param [in] inputparams=(_null_) A dataset containing name/value pairs in the
+ following format:
+ |name:$32|value:$10000|
+ |---|---|
+ |stpmacname|some value|
+ |mustbevalidname|can be anything, oops, %abort!!|
+ @param [in] inputfiles= (_null_) A dataset containing fileref/name/filename in
+ the following format:
+ |fileref:$8|name:$32|filename:$256|
+ |---|---|--|
+ |someref|some_name|some_filename.xls|
+ |fref2|another_file|zyx_v2.csv|
+ @param [in] expiresaftermins= (15) The number of minutes to retain the session
+ folder after the session ends.
+
+ @param [out] outds= (work.ms_triggerstp) Set to the name of a dataset to
+ contain the sessionId. If this dataset already exists, and contains the
+ sessionId, it will be appended to.
+ Format:
+ |sessionId:$36|
+ |---|
+ |20241028074744-54132-1730101664824|
+
+
SAS Macros
+ @li mf_getuniquefileref.sas
+ @li mf_getuniquelibref.sas
+ @li mf_getuniquename.sas
+ @li mp_abort.sas
+ @li mp_dropmembers.sas
+ @li mf_nobs.sas
+
+**/
+
+%macro ms_triggerstp(pgm
+ ,debug=131
+ ,inputparams=_null_
+ ,inputfiles=_null_
+ ,expiresAfterMins=15
+ ,outds=work.ms_triggerstp
+ ,mdebug=0
+ );
+ %local dbg mainref authref boundary libref triggered_sid;
+ %let mainref=%mf_getuniquefileref();
+ %let authref=%mf_getuniquefileref();
+ %let boundary=%mf_getuniquename();
+ %if &inputparams=0 %then %let inputparams=_null_;
+
+ %if &mdebug=1 %then %do;
+ %put &sysmacroname entry vars:;
+ %put _local_;
+ %end;
+ %else %let dbg=*;
+
+
+ %mp_abort(iftrue=("&pgm"="")
+ ,mac=&sysmacroname
+ ,msg=%str(Program not provided)
+ )
+ %mp_abort(iftrue=("&outds"="")
+ ,mac=&sysmacroname
+ ,msg=%str(Output dataset not provided)
+ )
+
+ /* avoid sending bom marker to API */
+ %local optval;
+ %let optval=%sysfunc(getoption(bomfile));
+ options nobomfile;
+
+ /* Add params to the content */
+ data _null_;
+ file &mainref termstr=crlf lrecl=32767 mod;
+ length line $1000 name $32 value $32767;
+ if _n_=1 then call missing(of _all_);
+ set &inputparams;
+ put "--&boundary";
+ line=cats('Content-Disposition: form-data; name="',name,'"');
+ put line;
+ put ;
+ put value;
+ run;
+
+ /* parse input file list */
+ %local webcount;
+ %let webcount=0;
+ data _null_;
+ set &inputfiles end=last;
+ length fileref $8 name $32 filename $256;
+ call symputx(cats('webref',_n_),fileref,'l');
+ call symputx(cats('webname',_n_),name,'l');
+ call symputx(cats('webfilename',_n_),filename,'l');
+ if last then do;
+ call symputx('webcount',_n_);
+ call missing(of _all_);
+ end;
+ run;
+
+ /* write out the input files to the content */
+ %local i;
+ %do i=1 %to &webcount;
+ data _null_;
+ file &mainref termstr=crlf lrecl=32767 mod;
+ infile &&webref&i lrecl=32767;
+ if _n_ = 1 then do;
+ length line $32767;
+ line=cats(
+ 'Content-Disposition: form-data; name="'
+ ,"&&webname&i"
+ ,'"; filename="'
+ ,"&&webfilename&i"
+ ,'"'
+ );
+ put "--&boundary";
+ put line;
+ put "Content-Type: text/plain";
+ put ;
+ end;
+ input;
+ put _infile_; /* add the actual file to be sent */
+ run;
+ %end;
+
+ /* Add footer to the content */
+ data _null_;
+ file &mainref termstr=crlf mod;
+ put / "--&boundary--";
+ run;
+
+ data _null_;
+ file &authref lrecl=1000;
+ infile "&_sasjs_tokenfile" lrecl=1000;
+ input;
+ if _n_=1 then put "Content-Type: multipart/form-data; boundary=&boundary";
+ put _infile_;
+ run;
+
+ %if &mdebug=1 %then %do;
+ data _null_;
+ if _n_ eq 1 then putlog "NOTE: ***** authref=&authref content *****";
+ infile &authref;
+ input;
+ put _infile_;
+ data _null_;
+ if _n_ eq 1 then putlog "NOTE: ***** mainref=&mainref content *****";
+ infile &mainref;
+ input;
+ put _infile_;
+ run;
+ %end;
+
+ %local resp_path outref;
+ %let resp_path=%sysfunc(pathname(work))/%mf_getuniquename();
+ %let outref=%mf_getuniquefileref();
+ filename &outref "&resp_path" lrecl=32767;
+
+ /* prepare request*/
+ proc http method='POST' headerin=&authref in=&mainref out=&outref
+ url="&_sasjs_apiserverurl/SASjsApi/stp/trigger?%trim(
+ )_program=&pgm%str(&)_debug=131%str(&)expiresAfterMins=&expiresaftermins";
+ %if &mdebug=1 %then %do;
+ debug level=2;
+ %end;
+ run;
+
+ %if (&SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201)
+ or &mdebug=1
+ %then %do;
+ data _null_;
+ if _n_ eq 1 then putlog "NOTE: ***** outref=&outref content *****";
+ infile &outref;
+ input;
+ putlog _infile_;
+ run;
+ %end;
+ %mp_abort(
+ iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200
+ and &SYS_PROCHTTP_STATUS_CODE ne 201)
+ ,mac=&sysmacroname
+ ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE)
+ )
+
+ /* reset options */
+ options &optval;
+
+ %let libref=%mf_getuniquelibref();
+ libname &libref JSON fileref=&outref;
+ %let triggered_sid=%mf_getuniquename(prefix=triggered_sid_);
+
+ data work.&triggered_sid (keep=sessionid);
+ set &libref..root;
+
+ %if &mdebug=1 %then %do;
+ putlog (_all_)(=);
+ %end;
+ run;
+
+ %if %mf_nobs(work.&triggered_sid)>0 %then %do;
+ proc append base=&outds data=work.&triggered_sid;
+ run;
+ %end;
+
+ %if &mdebug=1 %then %do;
+ %put &sysmacroname exit vars:;
+ %put _local_;
+ %end;
+ %else %do;
+ /* clear refs */
+ filename &authref;
+ filename &mainref;
+ filename &outref;
+ libname &libref clear;
+ /* and remove temp dataset */
+ %mp_dropmembers(&triggered_sid,libref=work);
+ %end;
+
+%mend ms_triggerstp;/**
@file
@brief Send data to/from sasjs/server
@details This macro should be added to the start of each web service,
diff --git a/server/ms_triggerstp.sas b/server/ms_triggerstp.sas
new file mode 100644
index 00000000..293a2ded
--- /dev/null
+++ b/server/ms_triggerstp.sas
@@ -0,0 +1,231 @@
+/**
+ @file
+ @brief Triggers a SASjs Server STP using the /SASjsApi/stp/trigger endpoint
+ @details Triggers the STP and returns the sessionId
+
+ Example:
+
+ %ms_triggerstp(/some/stored/program
+ ,debug=131
+ ,outds=work.myresults
+ )
+
+ @param [in] pgm The full path to the Stored Program in SASjs Drive (_program
+ parameter)
+ @param [in] debug= (131) The value to supply to the _debug URL parameter
+ @param [in] mdebug= (0) Set to 1 to enable DEBUG messages
+ @param [in] inputparams=(_null_) A dataset containing name/value pairs in the
+ following format:
+ |name:$32|value:$10000|
+ |---|---|
+ |stpmacname|some value|
+ |mustbevalidname|can be anything, oops, %abort!!|
+ @param [in] inputfiles= (_null_) A dataset containing fileref/name/filename in
+ the following format:
+ |fileref:$8|name:$32|filename:$256|
+ |---|---|--|
+ |someref|some_name|some_filename.xls|
+ |fref2|another_file|zyx_v2.csv|
+ @param [in] expiresaftermins= (15) The number of minutes to retain the session
+ folder after the session ends.
+
+ @param [out] outds= (work.ms_triggerstp) Set to the name of a dataset to
+ contain the sessionId. If this dataset already exists, and contains the
+ sessionId, it will be appended to.
+ Format:
+ |sessionId:$36|
+ |---|
+ |20241028074744-54132-1730101664824|
+
+ SAS Macros
+ @li mf_getuniquefileref.sas
+ @li mf_getuniquelibref.sas
+ @li mf_getuniquename.sas
+ @li mp_abort.sas
+ @li mp_dropmembers.sas
+ @li mf_nobs.sas
+
+**/
+
+%macro ms_triggerstp(pgm
+ ,debug=131
+ ,inputparams=_null_
+ ,inputfiles=_null_
+ ,expiresAfterMins=15
+ ,outds=work.ms_triggerstp
+ ,mdebug=0
+ );
+ %local dbg mainref authref boundary libref triggered_sid;
+ %let mainref=%mf_getuniquefileref();
+ %let authref=%mf_getuniquefileref();
+ %let boundary=%mf_getuniquename();
+ %if &inputparams=0 %then %let inputparams=_null_;
+
+ %if &mdebug=1 %then %do;
+ %put &sysmacroname entry vars:;
+ %put _local_;
+ %end;
+ %else %let dbg=*;
+
+
+ %mp_abort(iftrue=("&pgm"="")
+ ,mac=&sysmacroname
+ ,msg=%str(Program not provided)
+ )
+ %mp_abort(iftrue=("&outds"="")
+ ,mac=&sysmacroname
+ ,msg=%str(Output dataset not provided)
+ )
+
+ /* avoid sending bom marker to API */
+ %local optval;
+ %let optval=%sysfunc(getoption(bomfile));
+ options nobomfile;
+
+ /* Add params to the content */
+ data _null_;
+ file &mainref termstr=crlf lrecl=32767 mod;
+ length line $1000 name $32 value $32767;
+ if _n_=1 then call missing(of _all_);
+ set &inputparams;
+ put "--&boundary";
+ line=cats('Content-Disposition: form-data; name="',name,'"');
+ put line;
+ put ;
+ put value;
+ run;
+
+ /* parse input file list */
+ %local webcount;
+ %let webcount=0;
+ data _null_;
+ set &inputfiles end=last;
+ length fileref $8 name $32 filename $256;
+ call symputx(cats('webref',_n_),fileref,'l');
+ call symputx(cats('webname',_n_),name,'l');
+ call symputx(cats('webfilename',_n_),filename,'l');
+ if last then do;
+ call symputx('webcount',_n_);
+ call missing(of _all_);
+ end;
+ run;
+
+ /* write out the input files to the content */
+ %local i;
+ %do i=1 %to &webcount;
+ data _null_;
+ file &mainref termstr=crlf lrecl=32767 mod;
+ infile &&webref&i lrecl=32767;
+ if _n_ = 1 then do;
+ length line $32767;
+ line=cats(
+ 'Content-Disposition: form-data; name="'
+ ,"&&webname&i"
+ ,'"; filename="'
+ ,"&&webfilename&i"
+ ,'"'
+ );
+ put "--&boundary";
+ put line;
+ put "Content-Type: text/plain";
+ put ;
+ end;
+ input;
+ put _infile_; /* add the actual file to be sent */
+ run;
+ %end;
+
+ /* Add footer to the content */
+ data _null_;
+ file &mainref termstr=crlf mod;
+ put / "--&boundary--";
+ run;
+
+ data _null_;
+ file &authref lrecl=1000;
+ infile "&_sasjs_tokenfile" lrecl=1000;
+ input;
+ if _n_=1 then put "Content-Type: multipart/form-data; boundary=&boundary";
+ put _infile_;
+ run;
+
+ %if &mdebug=1 %then %do;
+ data _null_;
+ if _n_ eq 1 then putlog "NOTE: ***** authref=&authref content *****";
+ infile &authref;
+ input;
+ put _infile_;
+ data _null_;
+ if _n_ eq 1 then putlog "NOTE: ***** mainref=&mainref content *****";
+ infile &mainref;
+ input;
+ put _infile_;
+ run;
+ %end;
+
+ %local resp_path outref;
+ %let resp_path=%sysfunc(pathname(work))/%mf_getuniquename();
+ %let outref=%mf_getuniquefileref();
+ filename &outref "&resp_path" lrecl=32767;
+
+ /* prepare request*/
+ proc http method='POST' headerin=&authref in=&mainref out=&outref
+ url="&_sasjs_apiserverurl/SASjsApi/stp/trigger?%trim(
+ )_program=&pgm%str(&)_debug=131%str(&)expiresAfterMins=&expiresaftermins";
+ %if &mdebug=1 %then %do;
+ debug level=2;
+ %end;
+ run;
+
+ %if (&SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201)
+ or &mdebug=1
+ %then %do;
+ data _null_;
+ if _n_ eq 1 then putlog "NOTE: ***** outref=&outref content *****";
+ infile &outref;
+ input;
+ putlog _infile_;
+ run;
+ %end;
+ %mp_abort(
+ iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200
+ and &SYS_PROCHTTP_STATUS_CODE ne 201)
+ ,mac=&sysmacroname
+ ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE)
+ )
+
+ /* reset options */
+ options &optval;
+
+ %let libref=%mf_getuniquelibref();
+ libname &libref JSON fileref=&outref;
+ %let triggered_sid=%mf_getuniquename(prefix=triggered_sid_);
+
+ data work.&triggered_sid (keep=sessionid);
+ set &libref..root;
+
+ %if &mdebug=1 %then %do;
+ putlog (_all_)(=);
+ %end;
+ run;
+
+ %if %mf_nobs(work.&triggered_sid)>0 %then %do;
+ proc append base=&outds data=work.&triggered_sid;
+ run;
+ %end;
+
+ %if &mdebug=1 %then %do;
+ %put &sysmacroname exit vars:;
+ %put _local_;
+ %end;
+ %else %do;
+ /* clear refs */
+ filename &authref;
+ filename &mainref;
+ filename &outref;
+ libname &libref clear;
+ /* and remove temp dataset */
+ %mp_dropmembers(&triggered_sid,libref=work);
+ %end;
+
+%mend ms_triggerstp;
\ No newline at end of file
diff --git a/tests/serveronly/ms_triggerstp.test.sas b/tests/serveronly/ms_triggerstp.test.sas
new file mode 100644
index 00000000..a87cd62d
--- /dev/null
+++ b/tests/serveronly/ms_triggerstp.test.sas
@@ -0,0 +1,90 @@
+/**
+ @file
+ @brief Testing ms_triggerstp.sas macro
+
+ SAS Macros
+ @li mf_getuniquename.sas
+ @li mp_assert.sas
+ @li mp_assertscope.sas
+ @li ms_createfile.sas
+ @li ms_triggerstp.sas
+ @li mf_existds.sas
+ @li mp_assertdsobs.sas
+ @li mp_assertcols.sas
+ @li mf_getvartype.sas
+ @li ms_deletefile.sas
+
+**/
+
+/* first, create multiple STPs to run */
+filename stpcode1 temp;
+data _null_;
+ file stpcode1;
+ put '%put hello world;';
+ put '%put _all_;';
+ put 'data _null_; file _webout1; put "triggerstp test 1";run;';
+run;
+filename stpcode2 temp;
+data _null_;
+ file stpcode2;
+ put '%put Lorem Ipsum;';
+ put '%put _all_;';
+ put 'data _null_; file _webout2; put "triggerstp test 2";run;';
+run;
+options mprint;
+%let fname1=%mf_getuniquename();
+%let fname2=%mf_getuniquename();
+
+%ms_createfile(/sasjs/tests/&fname1..sas
+ ,inref=stpcode1
+ ,mdebug=1
+)
+%ms_createfile(/sasjs/tests/&fname2..sas
+ ,inref=stpcode2
+)
+
+%mp_assertscope(SNAPSHOT)
+ %ms_triggerstp(/sasjs/tests/&fname1
+ ,debug=131
+ ,outds=work.mySessions
+ )
+ %ms_triggerstp(/sasjs/tests/&fname2
+ ,outds=work.mySessions
+ )
+%mp_assertscope(COMPARE
+ ,ignorelist=MCLIB0_JADP1LEN MCLIB0_JADPNUM MCLIB0_JADVLEN)
+
+%mp_assert(iftrue=%str(%mf_existds(work.mySessions)=1)
+ ,desc=Testing output exists
+ ,outds=work.test_results
+)
+
+%mp_assertdsobs(work.mySessions,
+ test=EQUALS 2,
+ desc=Testing observations,
+ outds=work.test_results
+)
+%mp_assertcols(work.mySessions,
+ cols=sessionid,
+ test=ALL,
+ desc=Testing column exists,
+ outds=work.test_results
+)
+
+data _null_;
+ retain contentCheck 1;
+ set work.mySessions end=last;
+ if missing(sessionID) then contentCheck = 0;
+ if last then do;
+ call symputx("contentCheck",contentCheck,"l");
+ end;
+run;
+%let typeCheck = %mf_getvartype(work.mySessions,sessionid);
+
+%mp_assert(iftrue=%str(&typeCheck = C and &contentCheck = 1)
+ ,desc=Testing type and content of output
+ ,outds=work.test_results
+)
+
+%ms_deletefile(/sasjs/tests/&fname1..sas)
+%ms_deletefile(/sasjs/tests/&fname2..sas)
\ No newline at end of file