- execute runs in a new environment
- call shares the caller's variables
- include vs call: checks and activity log
- sub_include: build include requests with current values
- Errors propagate through nested executes
- JSON results computed in parallel
- Run the generated execute, call and include
- Stack run postponed until a gate opens
- Activity log of a run and its children
- Lost updates and synchronized
- Context of a run: user, session, pid, traces
execute runs in a new environment
A script reads a variable it does not declare and sets another one. Called with `execute`, it only sees its parameters: the caller's currency is undefined inside and the caller's tax is untouched after.
#1. A script that reads a variable it does not declare and sets another one; script create post "tuto.sc.env.probe" false 1 (param (var "[amount]" {type is_double [amount]} "Amount" is_null:false is_empty:false "100") ;) "Show what the script sees" { -> "[seen]" (if (env exist var "[currency]") {[currency]} {"undefined"}); -> "[tax]" (math round (* [amount] 0.2) 2); json load "tuto_sc_env" (env show); -> "[vars]" (string regexp_replace (json fields "tuto_sc_env" "/") "\s+" ""); json unload "tuto_sc_env"; concat "inside: amount=" [amount] ", currency=" [seen] ", tax=" [tax] [_n_] "variables inside: " [vars]; } "A sentence"; #2. The caller has its own variables; -> "[currency]" "EUR"; -> "[tax]" "caller value"; #3. execute runs the script in a NEW environment; -> "[r]" (execute "tuto.sc.env.probe.post" "[amount]" 250); script delete "tuto.sc.env.probe.post"; concat [r] [_n_] "caller after execute: tax=" [tax] ", currency=" [currency];
inside: amount=250, currency=undefined, tax=50 variables inside: ["[amount]","[PPID]","[tax]","[seen]"] caller after execute: tax=caller value, currency=EUR
- Pass everything the script needs as a parameter: that is what makes it a reusable service.
[PPID]inside is the pid of the caller when the caller is a logged script.env del varremoves a variable you do not want to leak.
include vs call: checks and activity log
The same logged fee script is run with `call` and `include`. call refuses a missing or invalid parameter and gets an activity pid; include just copies the variables and runs the code: it uses the caller's pct, has no pid, and the bad value fails inside the code.
#1. A logged script with two typed parameters; script create post "tuto.sc.fee.calc" true 1 (param (var "[base]" {type is_double [base]} "Base amount" is_null:false is_empty:false "100") (var "[pct]" {type is_double [pct]} "Fee in percent" is_null:false is_empty:false "2.5") ;) "Compute a fee" { concat "fee=" (math round (* [base] (/ [pct] 100)) 2) ", pid=" (if (is null or empty [PID]) {"none"} {"set"}); } "fee and whether an activity pid exists"; -> "[pct]" 3; #2. call checks every parameter, in order, and writes the activity log; -> "[r1]" (call "tuto.sc.fee.calc.post" "[base]" 200 "[pct]" 1.5); try {call "tuto.sc.fee.calc.post" "[base]" 200;} {-> "[r2]" (string regexp_extract [err] "Sorry, (.*)\." 1);} "[err]"; try {call "tuto.sc.fee.calc.post" "[base]" "abc" "[pct]" 1;} {-> "[r3]" (string regexp_extract [err] "Sorry, (.*) \(" 1);} "[err]"; #3. include only copies the variables, then runs the code: no check, no log; -> "[pct]" 3; -> "[i1]" (include "tuto.sc.fee.calc.post" "[base]" 200); try {include "tuto.sc.fee.calc.post" "[base]" "abc";} {-> "[i2]" (concat "error inside the code: " (string regexp_extract [err] "\[\[\[ (.*) \]\]\]" 1));} "[err]"; script delete "tuto.sc.fee.calc.post"; concat "call OK: " [r1] [_n_] "call with 1 param: " [r2] [_n_] "call with abc: " [r3] [_n_] "include with 1 param, pct from the caller: " [i1] [_n_] "include with abc: " [i2];
call OK: fee=3, pid=set call with 1 param: the script 'tuto.sc.fee.calc.post' does not has a valid number of parameters call with abc: the variable '[base]' in the script 'tuto.sc.fee.calc.post' is not valid include with 1 param, pct from the caller: fee=6, pid=none include with abc: error inside the code: Sorry, the number to round cannot be null.
- For a restricted user, include checks the parameters like call.
- Use include for code fragments of your own scripts, never for public input.
log searchwith the script name shows that include writes no Begin/End lines.
sub_include: build include requests with current values
`script generate sub_include` gives an expression that writes an include request with the values of the current variables, safely encoded. Two requests are built for two products, kept as text, and run later.
#1. A script that formats a label; script create post "tuto.sc.label.format" false 1 (param (var "[name]" {type is_varchar [name] 40} "Product name" is_null:false is_empty:false "Desk") (var "[price]" {type is_double [price]} "Price" is_null:false is_empty:false "99") ;) "Format a product label" { concat (string upper [name]) " - " [price] " EUR"; } "The label"; #2. sub_include gives an expression that writes the include request with the CURRENT values; -> "[template]" (script generate sub_include "tuto.sc.label.format.post"); #3. Build requests for two products, quotes are encoded safely; -> "[name]" "Desk \"Oslo\""; -> "[price]" 249; -> "[req1]" (eval [template]); -> "[name]" "Lamp"; -> "[price]" 39.9; -> "[req2]" (eval [template]); #4. Run the generated requests later, for example from a job or a queue; -> "[out]" (concat (eval [req1]) " | " (eval [req2])); script delete "tuto.sc.label.format.post"; concat "template:" [_n_] [template] [_n_] "request 1:" [_n_] [req1] [_n_] "results: " [out];
template: concat "include \"tuto.sc.label.format.post\" \"[name]\" \"" (mql encode [name]) "\" \"[price]\" \"" (mql encode [price]) "\" ;" request 1: include "tuto.sc.label.format.post" "[name]" "Desk \"Oslo\"" "[price]" "249" ; results: DESK "OSLO" - 249 EUR | LAMP - 39.9 EUR
- The quotes of
Desk "Oslo"are encoded bymql encode. - Put the requests in a job or a stack entry to run them later.
script generate includegives the same request with the example values.
Errors propagate through nested executes
An order script executes a stock script that raises business errors with an id. The top-level caller catches the error two levels down, maps the id to a code and counts the stack lines of the trace.
#1. A low-level script that raises business errors with an id; script create post "tuto.sc.stock.reserve" false 1 (param (var "[sku]" {type is_varchar [sku] 12} "Product code" is_null:false is_empty:false "BK-0042") (var "[qty]" {type is_integer [qty] 4} "Quantity" is_null:false is_empty:false "2") ;) "Reserve stock" { -> "[stock]" (switch [sku] ("BK-0042") {5} ("BK-0099") {0} {-1}); if (== [stock] -1) {exception ("404") (concat "unknown product " [sku]);}; if (> [qty] [stock]) {exception ("409") (concat "only " [stock] " left for " [sku]);}; concat "reserved " [qty] " x " [sku]; } "A confirmation"; #2. A top-level script that executes it without catching anything; script create post "tuto.sc.order.place" false 1 (param (var "[sku]" {type is_varchar [sku] 12} "Product code" is_null:false is_empty:false "BK-0042") (var "[qty]" {type is_integer [qty] 4} "Quantity" is_null:false is_empty:false "2") ;) "Place an order" { -> "[res]" (execute "tuto.sc.stock.reserve.post" "[sku]" [sku] "[qty]" [qty]); concat "order OK, " [res]; } "A confirmation"; #3. The caller catches the error two levels down and maps it to a code; -> "[report]" ""; for (-> "[i]" 0) (< [i] 3) (++ "[i]") { -> "[sku]" (atom get "BK-0042,BK-0099,ZZ-1" (+ [i] 1) ","); try { -> "[line]" (execute "tuto.sc.order.place.post" "[sku]" [sku] "[qty]" 3); } { -> "[code]" (string regexp_extract [err] "([0-9]{3}): ([^\n]*)$" 1); -> "[line]" (concat "error " [code] ": " (string regexp_extract [err] "([0-9]{3}): ([^\n]*)$" 2) " - " (string regexp_count [err] ">>>") " stack lines"); } "[err]"; -> "[report]" (concat [report] [sku] " -> " [line] [_n_]); }; script delete "tuto.sc.order.place.post"; script delete "tuto.sc.stock.reserve.post"; [report];
BK-0042 -> order OK, reserved 3 x BK-0042 BK-0099 -> error 409: only 0 left for BK-0099 - 6 stack lines ZZ-1 -> error 404: unknown product ZZ-1 - 6 stack lines
- The message keeps every level as a
line n >>>line: useful in logs, too long for an API answer. - Catch at the level that knows what to answer, let the others propagate.
stack replay_error_idreplays a stacked run that failed with an error.
JSON results computed in parallel
The KPI of three regions are computed at the same time with `parallel` and `execute`. Each run returns a JSON document; the caller reads the status and result of each run and consolidates sales, orders and the average basket.
#1. A script that returns a JSON document; script create post "tuto.sc.region.kpi" false 1 (param (var "[region]" {type is_enum [region] "north,south,east"} "Region" is_null:false is_empty:false "north") ;) "KPI of a region" { -> "[sales]" (switch [region] ("north") {1250.5} ("south") {980} {1430.25}); -> "[orders]" (switch [region] ("north") {41} ("south") {37} {52}); concat "{\"region\": \"" [region] "\", \"sales\": " [sales] ", \"orders\": " [orders] "}"; } "JSON: region, sales, orders"; #2. Three regions computed at the same time; json load "tuto_sc_par" (parallel {execute "tuto.sc.region.kpi.post" "[region]" "north";} {execute "tuto.sc.region.kpi.post" "[region]" "south";} {execute "tuto.sc.region.kpi.post" "[region]" "east";} ); #3. Read each returned JSON and consolidate; json load "tuto_sc_tot" "{\"regions\": [], \"sales\": 0, \"orders\": 0}"; for (-> "[i]" 0) (< [i] (json count "tuto_sc_par" "/")) (++ "[i]") { json load "tuto_sc_one" (json select "tuto_sc_par" (concat "/[" [i] "]/result")); json iarray "tuto_sc_tot" "/regions" (concat (json select "tuto_sc_one" "/region") " " (json select "tuto_sc_par" (concat "/[" [i] "]/status"))) STR; json uobject "tuto_sc_tot" "/" "sales" (+ (json select "tuto_sc_tot" "/sales") (json select "tuto_sc_one" "/sales")) NUM; json uobject "tuto_sc_tot" "/" "orders" (+ (json select "tuto_sc_tot" "/orders") (json select "tuto_sc_one" "/orders")) INT; json unload "tuto_sc_one"; }; json iobject "tuto_sc_tot" "/" "avg_basket" (math round (/ (json select "tuto_sc_tot" "/sales") (json select "tuto_sc_tot" "/orders")) 2) NUM; -> "[out]" (json doc "tuto_sc_tot"); json unload "tuto_sc_par"; json unload "tuto_sc_tot"; script delete "tuto.sc.region.kpi.post"; [out];
{
"regions": [
"north OK",
"south OK",
"east OK"
],
"orders": 130,
"avg_basket": 28.16,
"sales": 3660.75
}- parallel returns an array of {result, status}: check status before reading result.
- Use
synchronizedif the parallel runs write to the same object. json sortcan rank the regions in the final document.
Run the generated execute, call and include
The editor generates the three kinds of calls of a loan script, filled with the documented examples. They are run as they are, then with other values after a string replacement.
#1. A script with documented examples on its parameters; script create post "tuto.sc.loan.monthly" false 1 (param (var "[capital]" {type is_double [capital]} "Borrowed amount" is_null:false is_empty:false "200000") (var "[rate]" {type is_double [rate]} "Yearly rate in percent" is_null:false is_empty:false "3.6") (var "[years]" {type is_integer [years] 2} "Duration in years" is_null:false is_empty:false "25") ;) "Monthly payment of a loan" { -> "[r]" (/ [rate] 1200); -> "[n]" (* [years] 12); math round (/ (* [capital] [r]) (- 1 (math pow (+ 1 [r]) (- 0 [n])))) 2; } "The monthly payment"; #2. The editor can generate the three kinds of calls, filled with the examples; -> "[gen_exe]" (script generate execute "tuto.sc.loan.monthly.post"); -> "[gen_call]" (script generate call "tuto.sc.loan.monthly.post"); -> "[gen_inc]" (script generate include "tuto.sc.loan.monthly.post"); #3. Run them as they are, then with other values; -> "[a]" (eval [gen_exe]); -> "[b]" (eval (string replace [gen_call] "\"25\"" "\"15\"")); -> "[c]" (eval (string replace (string replace [gen_inc] "\"200000\"" "\"150000\"") "\"3.6\"" "\"3.1\"")); script delete "tuto.sc.loan.monthly.post"; concat [gen_exe] [_n_] "example values: " [a] [_n_] "call over 15 years: " [b] [_n_] "include, 150000 at 3.1%: " [c];
execute "tuto.sc.loan.monthly.post" "[capital]" "200000" "[rate]" "3.6" "[years]" "25" ; example values: 1012.01 call over 15 years: 1439.61 include, 150000 at 3.1%: 719.14
- Good examples on the parameters make the generated calls runnable test cases.
script generate stackgives the stack version of the same call.- Store the generated calls as smoke tests of a deployment.
Stack run postponed until a gate opens
A nightly import waits for a gate parameter. It is pushed in the stack now; on the first pass of the stack the delay condition is false, so the run is postponed by 5 s; the gate is opened and the next pass imports the file. Takes about 30 to 60 seconds.
#1. A gate parameter and a node to receive the result; parameter add "tuto_sc_gate" "closed" "Tutorial: opens the nightly import" false; node create "tuto_sc_import"; #2. The import must wait until the gate is open; script create post "tuto.sc.import.batch" false 5 (param (var "[file]" {type is_varchar [file] 40} "File to import" is_null:false is_empty:false "orders.csv") ;) "Import a file when the gate is open" { node iobject "tuto_sc_import" "/" "done" (concat [file] " imported at " (date systime)) STR; "imported"; } "imported"; script set delay "tuto.sc.import.batch.post" 5 sec { equal (parameter get value "tuto_sc_gate") "open"; }; #3. Push it in the stack now, the server checks the stack every 30 s; -> "[pid]" (stack (date now) "tuto.sc.import.batch.post" "[file]" "orders_2026-11-01.csv"); -> "[t0]" (date systimestamp); #4. First pass: the condition is false, the run is postponed; -> "[n]" 0; while (and (< [n] 40) (is null (json load select "tuto_sc_st" (stack get [pid]) "/lastattempt"))) {wait 1000; ++ "[n]";}; -> "[first]" (concat "pass 1 after ~" [n] " s: state " (json load select "tuto_sc_st" (stack get [pid]) "/state") ", next run " (json load select "tuto_sc_st" (stack get [pid]) "/dtexe")); #5. Open the gate and wait for the next pass; parameter update "tuto_sc_gate" "open"; -> "[n]" 0; while (and (< [n] 40) (not (node exist "tuto_sc_import") )) {wait 1000; ++ "[n]";}; while (and (< [n] 40) (is null (node select "tuto_sc_import" "/done"))) {wait 1000; ++ "[n]";}; -> "[second]" (concat "pass 2 after ~" [n] " s: " (node select "tuto_sc_import" "/done") ", closed: " (not (is null (json load select "tuto_sc_st" (stack get [pid]) "/dtclosed")))); #6. Clean up the stack history, the script, the node and the parameter; stack delete_closed_script "tuto.sc.import.batch.post"; script delete "tuto.sc.import.batch.post"; node delete "tuto_sc_import"; parameter remove "tuto_sc_gate"; concat [first] [_n_] [second];
pass 1 after ~19 s: state W, next run 2026-11-01 17:26:46 pass 2 after ~30 s: orders_2026-11-01.csv imported at 17:27:30, closed: 1
- The stack is processed every 30 s, so the postponed run waits for the next pass.
stack searchlists the closed or failed runs of a script.- Clean the history with
stack delete_closed_scriptfor your own script only.
Activity log of a run and its children
A logged batch executes three logged steps; step 3 fails and the batch goes on. The batch returns its pid, and `restricted show_pid` returns the log lines of the run and of its child processes: begin, messages, errors, end.
#1. Two logged scripts: a batch that executes a step; script create post "tuto.sc.batch.step" true 1 (param (var "[n]" {type is_integer [n] 3} "Step number" is_null:false is_empty:false "1");) "One step of the batch" { log write (concat "step " [n] " processed") "OK" "step" [n]; if (== [n] 3) {exception ("500") ("step 3 failed: disk full");}; "ok"; } "ok"; script create post "tuto.sc.batch.run" true 1 (param (var "[steps]" {type is_integer [steps] 3} "Number of steps" is_null:false is_empty:false "3");) "Run the steps of the batch" { for (-> "[i]" 1) (<= [i] [steps]) (++ "[i]") { try {execute "tuto.sc.batch.step.post" "[n]" [i];} {log write "a step failed, batch continues" "KO" "step" [i];} "[e]"; }; [PID]; } "The pid of the run"; #2. Run the batch, it returns its own pid; -> "[pid]" (execute "tuto.sc.batch.run.post" "[steps]" 3); #3. Read the activity log of the run and of its linked processes; json load "tuto_sc_log" (string regexp_extract (restricted show_pid [pid]) "(\{.*)$" 1); -> "[trace]" ""; for (-> "[i]" 0) (< [i] (json count "tuto_sc_log" "/data")) (++ "[i]") { -> "[row]" (concat "/data/[" [i] "]"); -> "[trace]" (concat [trace] [_n_] (string upper (json select "tuto_sc_log" (concat [row] "/status"))) " " (string replace (json select "tuto_sc_log" (concat [row] "/script")) "tuto.sc." "") " " (if (equal (json select "tuto_sc_log" (concat [row] "/parent_pid")) "") {"root "} {"child"}) " " (string regexp_extract (json select "tuto_sc_log" (concat [row] "/msg")) "(?:.*; )?([^;\n]*)$" 1)); }; json unload "tuto_sc_log"; #4. Server settings of the log; -> "[cfg]" (concat "retention " (log retention day) " days, archive files of " (log archive size) " MB"); script delete "tuto.sc.batch.run.post"; script delete "tuto.sc.batch.step.post"; concat (string trim [trace]) [_n_] [cfg];
OK batch.run.post root [---- Begin script ----] OK batch.step.post child [---- Begin script ----] OK batch.step.post child step 1 processed OK batch.step.post child [---- End script ----] OK batch.step.post child [---- Begin script ----] OK batch.step.post child step 2 processed OK batch.step.post child [---- End script ----] OK batch.step.post child [---- Begin script ----] OK batch.step.post child step 3 processed KO batch.step.post child 500: step 3 failed: disk full KO batch.run.post root a step failed, batch continues OK batch.run.post root [---- End script ----] retention 50 days, archive files of 30 MB
- After a failed step the caller's script name is restored: the KO line written by the batch is stamped
batch.run.post, not with the step's name. log searchfilters by status, script, client key and value.log show_timegives the same lines for a pid.
Lost updates and synchronized
Four parallel hits increment a shared counter stored in a node. Without a lock they read the same value and the counter ends at 1; inside a `synchronized` block the four updates are kept.
#1. A shared counter in a node; node create "tuto_sc_counter"; node iobject "tuto_sc_counter" "/" "unsafe" 0 INT; node iobject "tuto_sc_counter" "/" "safe" 0 INT; #2. A script that reads, waits, then writes back: a classic lost update; script create post "tuto.sc.counter.hit" false 1 (param (var "[mode]" {type is_enum [mode] "unsafe,safe"} "Protection" is_null:false is_empty:false "safe");) "Increment a shared counter" { if (equal [mode] "safe") { synchronized ("tuto_sc_counter_lock") { -> "[v]" (node select "tuto_sc_counter" "/safe"); wait 100; node uobject "tuto_sc_counter" "/" "safe" (+ [v] 1) INT; }; } { -> "[v]" (node select "tuto_sc_counter" "/unsafe"); wait 100; node uobject "tuto_sc_counter" "/" "unsafe" (+ [v] 1) INT; }; "ok"; } "ok"; #3. Four hits at the same time, without and with synchronized; parallel {execute "tuto.sc.counter.hit.post" "[mode]" "unsafe";} {execute "tuto.sc.counter.hit.post" "[mode]" "unsafe";} {execute "tuto.sc.counter.hit.post" "[mode]" "unsafe";} {execute "tuto.sc.counter.hit.post" "[mode]" "unsafe";}; parallel {execute "tuto.sc.counter.hit.post" "[mode]" "safe";} {execute "tuto.sc.counter.hit.post" "[mode]" "safe";} {execute "tuto.sc.counter.hit.post" "[mode]" "safe";} {execute "tuto.sc.counter.hit.post" "[mode]" "safe";}; -> "[out]" (concat "4 parallel hits - without lock: " (node select "tuto_sc_counter" "/unsafe") ", with synchronized: " (node select "tuto_sc_counter" "/safe")); script delete "tuto.sc.counter.hit.post"; node delete "tuto_sc_counter"; [out];
4 parallel hits - without lock: 1, with synchronized: 4
- The lock key is a string: use one key per protected object.
- Keep the synchronized block short, other runs wait.
- For a simple id,
sequence incrementis the usual tool.
Context of a run: user, session, pid, traces
A logged script reports who runs it, in which session and with which pid. Two executes run in the caller's session with a new pid each; the open sessions are counted without printing them, and the two traces are found back in the text log by pid.
#1. A logged script that reports its context; script create post "tuto.sc.context.probe" true 1 (param (var "[step]" {type is_varchar [step] 20} "Step name" is_null:false is_empty:false "check");) "Report the run context" { log trace (concat "tuto.sc context " [step] " pid " [PID]); concat "{\"user\": \"" (who) "\", \"sid\": " (sid) ", \"pid\": \"" [PID] "\"}"; } "JSON"; #2. The caller context, before; -> "[sid]" (sid); #3. Two runs: same user and session, a new pid each time; json load "tuto_sc_a" (execute "tuto.sc.context.probe.post" "[step]" "first"); json load "tuto_sc_b" (execute "tuto.sc.context.probe.post" "[step]" "second"); -> "[p1]" (json select "tuto_sc_a" "/pid"); -> "[p2]" (json select "tuto_sc_b" "/pid"); -> "[runs]" (concat "run 1 pid " [p1] ", run 2 pid " [p2] ", same session as the caller: " (and (equal (json select "tuto_sc_a" "/sid") [sid]) (equal (json select "tuto_sc_b" "/sid") [sid])) ", user " (json select "tuto_sc_b" "/user")); json unload "tuto_sc_a"; json unload "tuto_sc_b"; #4. The other sessions open on the server, counted but not printed; json load "tuto_sc_ses" (sessions); -> "[ctx]" (concat "open sessions: count sessions " (count sessions) ", sessions list " (json count "tuto_sc_ses" "/") ", commands of this connection " (cmdid)); json unload "tuto_sc_ses"; #5. The traces of these two runs in the text log; -> "[log]" (log show 50); -> "[traces]" (concat (string regexp_extract [log] (concat "(tuto\.sc context \w+ pid " [p1] ")(\s|$)") 1) [_n_] (string regexp_extract [log] (concat "(tuto\.sc context \w+ pid " [p2] ")(\s|$)") 1)); script delete "tuto.sc.context.probe.post"; concat [runs] [_n_] [ctx] [_n_] (string trim [traces]);
run 1 pid 47, run 2 pid 48, same session as the caller: 1, user admin open sessions: count sessions 4, sessions list 4, commands of this connection 2 tuto.sc context first pid 47 tuto.sc context second pid 48
