- A script with typed parameters rejects bad calls
- Optional parameters and default values
- Read a script back to build a form
- Patch a script with its generated update request
- Idempotent deployment with script merge
- Back up a script before deleting it
- Copy, rename and delete a module by prefix
- Stack and delay options of a batch script
- Grant a script to a group, then revert
- Business types on the parameters of a service
A script with typed parameters rejects bad calls
An invoice-line service must refuse bad input before its code runs. The script declares four typed parameters (varchar, integer, decimal, enum), then one valid call and four invalid ones show the exact messages the server returns.
#1. A POST script with typed parameters; script create post "tuto.sc.invoice.line" false 1 (param (var "[sku]" {type is_varchar [sku] 12} "Product code, 12 chars max" is_null:false is_empty:false "BK-0042") (var "[qty]" {type is_integer [qty] 4} "Quantity, 4 digits max" is_null:false is_empty:false "3") (var "[price]" {type is_decimal [price] 8 2} "Unit price" is_null:false is_empty:false "19.90") (var "[vat]" {type is_enum [vat] "0,5.5,10,20"} "VAT rate in percent" is_null:false is_empty:false "20") ;) "Compute a gross invoice line" { -> "[net]" (math round (* [qty] [price]) 2); concat [sku] ": " [qty] " x " [price] " = " [net] " net, " (math round (* [net] (+ 1 (/ [vat] 100))) 2) " gross"; } "A sentence with the net and gross amounts"; #2. A valid call; -> "[ok]" (execute "tuto.sc.invoice.line.post" "[sku]" "BK-0042" "[qty]" 3 "[price]" 19.90 "[vat]" 20); #3. Invalid calls: the server rejects them before the code runs; -> "[errors]" ""; for (-> "[i]" 1) (<= [i] 4) (++ "[i]") { try { case (== [i] 1) {execute "tuto.sc.invoice.line.post" "[sku]" "BK-0042" "[qty]" "three" "[price]" 19.90 "[vat]" 20;} (== [i] 2) {execute "tuto.sc.invoice.line.post" "[sku]" "BK-0042" "[qty]" 3 "[price]" 19.90 "[vat]" 21;} (== [i] 3) {execute "tuto.sc.invoice.line.post" "[sku]" "" "[qty]" 3 "[price]" 19.90 "[vat]" 20;} (== [i] 4) {execute "tuto.sc.invoice.line.post" "[sku]" "BK-0042" "[qty]" 3;} ; } { -> "[errors]" (concat [errors] [_n_] "- " (string regexp_extract [err] "\[\[\[ Sorry, (.*) \]\]\]" 1)); } "[err]"; }; #4. Remove the tutorial script; script delete "tuto.sc.invoice.line.post"; concat "OK: " [ok] [_n_] "Rejected:" [errors];
OK: BK-0042: 3 x 19.90 = 59.7 net, 71.64 gross Rejected: - the variable '[qty]' in the script 'tuto.sc.invoice.line.post' is not valid (type is_integer [qty] 4;). - the variable '[vat]' in the script 'tuto.sc.invoice.line.post' is not valid (type is_enum [vat] "0,5.5,10,20";). - the variable '[sku]' in the script 'tuto.sc.invoice.line.post' cannot not be empty. - the script 'tuto.sc.invoice.line.post' does not has a valid number of parameters.
- Parameters are positional: the error says which one is expected in which position.
- Use
type is_iban,type is_siretortype is_emailfor business identifiers. string regexp_extractturns the server message into a clean API error.
Optional parameters and default values
A greeting service has a required name and two optional parameters. A null optional value skips the type check, the defaults are applied in the code, and the is_null, is_empty and type rules refuse the bad calls.
#1. Optional parameters: null allowed, the type check runs only on a value; script create post "tuto.sc.greeting" false 1 (param (var "[name]" {type is_varchar [name] 30} "Customer name" is_null:false is_empty:false "Alice") (var "[lang]" {type is_enum [lang] "en,fr,es"} "Language, en when null" is_null:true is_empty:false "fr") (var "[title]" {type is_varchar [title] 10} "Title, may be null or empty" is_null:true is_empty:true "Dr") ;) "Build a greeting with default values" { #2. Apply the defaults inside the script; if (is null [lang]) {-> "[lang]" "en";}; if (is null or empty [title]) {-> "[who]" [name];} {-> "[who]" (concat [title] " " [name]);}; concat (switch [lang] ("fr") {"Bonjour"} ("es") {"Hola"} {"Hello"}) " " [who] " - lang " [lang]; } "The greeting"; #3. Same script, three calls; -> "[r]" (concat (execute "tuto.sc.greeting.post" "[name]" "Alice" "[lang]" "fr" "[title]" "Dr") [_n_] (execute "tuto.sc.greeting.post" "[name]" "Bob" "[lang]" null "[title]" null) [_n_] (execute "tuto.sc.greeting.post" "[name]" "Carmen" "[lang]" "es" "[title]" "") ); #4. Refused: a null or empty name, an empty language, a language out of the list; json load "tuto_sc_bad" "[[null, \"en\"], [\"\", \"en\"], [\"Dan\", \"\"], [\"Dan\", \"de\"]]"; for (-> "[i]" 0) (< [i] 4) (++ "[i]") { try { execute "tuto.sc.greeting.post" "[name]" (json select "tuto_sc_bad" (concat "/[" [i] "]/[0]")) "[lang]" (json select "tuto_sc_bad" (concat "/[" [i] "]/[1]")) "[title]" null; } { -> "[r]" (concat [r] [_n_] "refused: " (string regexp_extract [err] "\[\[\[ (.+?) \]\]\]" 1)); } "[err]"; }; json unload "tuto_sc_bad"; script delete "tuto.sc.greeting.post"; [r];
Bonjour Dr Alice - lang fr Hello Bob - lang en Hola Carmen - lang es refused: Sorry, the variable '[name]' in the script 'tuto.sc.greeting.post' cannot not be null. refused: Sorry, the variable '[name]' in the script 'tuto.sc.greeting.post' cannot not be empty. refused: Sorry, the variable '[lang]' in the script 'tuto.sc.greeting.post' cannot not be empty. refused: Sorry, the variable '[lang]' in the script 'tuto.sc.greeting.post' is not valid (type is_enum [lang] "en,fr,es";).
- The type check runs only on a value: with is_null:true a null skips it, no
or (is null [x]) ...guard is needed. - is_null and is_empty are independent:
[lang]accepts null but refuses an empty string. string empty_if_nullis another way to normalise the input.
Read a script back to build a form
A front-end developer needs a form for a search service. The script definition and its parameters are read back with `script get` and `script show params`, then turned into a JSON form description: field, label, required flag and check.
#1. A script to describe; script create get "tuto.sc.customer.search" false 1 (param (var "[q]" {type is_varchar [q] 50} "Text to search in the name" is_null:false is_empty:false "dupont") (var "[page]" {type is_integer [page] 3} "Page number" is_null:false is_empty:false "1") (var "[email]" {or (is null [email]) (type is_email [email])} "Exact email filter" is_null:true is_empty:true "a@b.test") ;) "Search the customers" { concat "searching " [q]; } "A JSON array of customers"; #2. Read it back: definition and parameters; -> "[name]" "tuto.sc.customer.search.get"; json load "tuto_sc_def" (script get [name]); json load "tuto_sc_par" (script show params [name]); #3. Build a form description for a front-end; json load "tuto_sc_form" "{\"fields\": []}"; json iobject "tuto_sc_form" "/" "script" (json select "tuto_sc_def" "/k") STR; json iobject "tuto_sc_form" "/" "title" (json select "tuto_sc_def" "/desc") STR; json iobject "tuto_sc_form" "/" "returns" (json select "tuto_sc_def" "/return") STR; for (-> "[i]" 0) (< [i] (json count "tuto_sc_par" "/")) (++ "[i]") { -> "[p]" (concat "/[" [i] "]"); json iarray "tuto_sc_form" "/fields" (concat "{\"field\": \"" (string regexp_extract (json select "tuto_sc_par" (concat [p] "/name")) "\[(.*)\]" 1) "\", \"label\": \"" (json select "tuto_sc_par" (concat [p] "/desc")) "\", \"required\": " (== (json select "tuto_sc_par" (concat [p] "/isNull")) "0") ", \"check\": \"" (string json_escape (json select "tuto_sc_par" (concat [p] "/type"))) "\"}") OBJ; }; -> "[form]" (json doc "tuto_sc_form"); #4. Clean up; json unload "tuto_sc_def"; json unload "tuto_sc_par"; json unload "tuto_sc_form"; script delete [name]; [form];
{
"returns": "A JSON array of customers",
"fields": [
{
"field": "q",
"label": "Text to search in the name",
"check": "type is_varchar [q] 50;",
"required": 1
},
{
"field": "page",
"label": "Page number",
"check": "type is_integer [page] 3;",
"required": 1
},
{
"field": "email",
"label": "Exact email filter",
"check": "or (is null [email]) (type is_email [email]);",
"required": 0
}
],
"title": "Search the customers",
"script": "tuto.sc.customer.search.get"
}- The same loop can produce an HTML form with
concat. script show userstells you who will be able to submit the form.- The
varsfield ofscript getholds the raw param block.
Patch a script with its generated update request
The shipping rate changed and parcels over 30 kg become free. The update request is generated from the running script, patched with string replacements, applied with `eval`, and the new behaviour is checked.
#1. Version 1: a shipping fee with a hard-coded rate; script create post "tuto.sc.shipping.fee" false 1 (param (var "[kg]" {type is_double [kg]} "Weight in kg" is_null:false is_empty:false "2.5") ;) "Shipping fee of a parcel" { math round (+ 4.90 (* [kg] 1.20)) 2; } "The fee in EUR"; -> "[v1]" (execute "tuto.sc.shipping.fee.post" "[kg]" 2.5); #2. Generate the update request, exactly what the editor shows; -> "[code]" (script generate update "tuto.sc.shipping.fee.post"); #3. Patch it: new rate, logging on, a free shipping threshold; -> "[code]" (string replace [code] "1.20" "1.35"); -> "[code]" (string replace [code] "math round" "if (> [kg] 30) {0;} {math round"); -> "[code]" (string replace [code] "2;" "2;};"); -> "[code]" (string replace [code] "\"tuto.sc.shipping.fee.post\" false" "\"tuto.sc.shipping.fee.post\" true"); #4. Apply the update and check the new behaviour; -> "[applied]" (eval [code]); -> "[v2]" (execute "tuto.sc.shipping.fee.post" "[kg]" 2.5); -> "[v3]" (execute "tuto.sc.shipping.fee.post" "[kg]" 35); -> "[log]" (json load select "tuto_sc_s" (script get "tuto.sc.shipping.fee.post") "/activateLog"); json unload "tuto_sc_s"; script delete "tuto.sc.shipping.fee.post"; concat "v1: " [v1] " EUR" [_n_] [applied] [_n_] "v2: " [v2] " EUR, 35 kg: " [v3] " EUR, activity log " [log] [_n_] "Request applied:" [_n_] [code];
v1: 7.9 EUR
Script updated with successful.
v2: 8.28 EUR, 35 kg: 0 EUR, activity log 1
Request applied:
script update "tuto.sc.shipping.fee.post" true 1
(param
(var "[kg]" {type is_double [kg]} "Weight in kg" is_null:false is_empty:false "2.5")
;)
"Shipping fee of a parcel"
{
if (> [kg] 30) {0;} {math round (+ 4.90 (* [kg] 1.35)) 2;};
} "The fee in EUR";- Keep the generated request in your versioning tool before changing it.
script getthen/mqlshows only the code, without the header.- For many scripts, see
script generate mergeand the versioning functions.
Idempotent deployment with script merge
A deployment request must work on a fresh server and on a server that already has the script. `script merge` creates the script the first time and updates it the second time; the generated create, update and merge headers are compared.
#1. Deployment request kept in the project: merge creates or updates; -> "[deploy]" "script merge \"tuto.sc.discount.rate.post\" false 1 (param (var \"[segment]\" {type is_enum [segment] \"retail,pro,vip\"} \"Customer segment\" is_null:false is_empty:false \"pro\") ;) \"Discount rate of a segment\" { case (equal [segment] \"vip\") {15;} (equal [segment] \"pro\") {8;} {0;}; } \"Rate in percent\";"; #2. First deployment: the script does not exist yet; -> "[before]" (script exist "tuto.sc.discount.rate.post"); -> "[run1]" (eval [deploy]); -> "[r1]" (execute "tuto.sc.discount.rate.post" "[segment]" "vip"); #3. Second deployment of a new version: same request, no error; -> "[deploy]" (string replace [deploy] "{15;}" "{20;}"); -> "[run2]" (eval [deploy]); -> "[r2]" (execute "tuto.sc.discount.rate.post" "[segment]" "vip"); #4. The three requests the server can generate for this script; -> "[gen]" (concat "create: " (atom get_first (script generate create "tuto.sc.discount.rate.post") [_n_]) [_n_] "update: " (atom get_first (script generate update "tuto.sc.discount.rate.post") [_n_]) [_n_] "merge: " (atom get_first (script generate merge "tuto.sc.discount.rate.post") [_n_])); script delete "tuto.sc.discount.rate.post"; concat "exists before: " [before] [_n_] "deploy 1: " [run1] " vip=" [r1] "%" [_n_] "deploy 2: " [run2] " vip=" [r2] "%" [_n_] [gen] [_n_] "exists after delete: " (script exist "tuto.sc.discount.rate.post");
exists before: 0 deploy 1: Script merged with successful. vip=15% deploy 2: Script merged with successful. vip=20% create: script create post "tuto.sc.discount.rate" false 1 update: script update "tuto.sc.discount.rate.post" false 1 merge: script merge "tuto.sc.discount.rate.post" false 1 exists after delete: 0
script generate mergegives the merge request of any existing script.- Chain several merge requests in one file to deploy a module.
- After a merge, read the stack and delay options back with
script get.
Back up a script before deleting it
Before a risky change, the create request of a password policy script is kept as a backup with its SHA-256. The script is deleted, restored with `eval`, and the definition is proven identical.
#1. A script someone is about to delete by mistake; script create post "tuto.sc.password.policy" false 1 (param (var "[pwd]" {type is_varchar [pwd] 64} "Candidate password" is_null:false is_empty:false "S3cure!pass") ;) "Check the password policy" { -> "[ok]" (and (>= (string length [pwd]) 10) (and (string matches [pwd] ".*[0-9].*") (string matches [pwd] ".*[A-Z].*"))); if [ok] {"accepted";} {"rejected";}; } "accepted or rejected"; #2. Backup: the create request holds everything; -> "[backup]" (script generate create "tuto.sc.password.policy.post"); -> "[sha]" (string sha_256 [backup]); #3. Oops; script delete "tuto.sc.password.policy.post"; -> "[gone]" (script exist "tuto.sc.password.policy.post"); #4. Restore from the backup and test again; -> "[restored]" (eval [backup]); -> "[same]" (equal [sha] (string sha_256 (script generate create "tuto.sc.password.policy.post"))); -> "[t]" (concat (execute "tuto.sc.password.policy.post" "[pwd]" "S3cure!passw0rd") " / " (execute "tuto.sc.password.policy.post" "[pwd]" "short1A")); script delete "tuto.sc.password.policy.post"; concat "after delete: exists=" [gone] [_n_] "restore: " [restored] [_n_] "identical definition: " [same] [_n_] "tests: " [t];
after delete: exists=0 restore: Script added with successful. identical definition: 1 tests: accepted / rejected
- Store the backup in a
tuto_node or a file under tmp/ to keep it across sessions. - Compare two servers by hashing the generated create requests.
string matchesis used for the password rules;string bcrypt_hashto store a password.
Copy, rename and delete a module by prefix
A team branches a small module: one script is copied to another method and renamed, the whole module is copied to v2 then promoted to stable, the module is listed from `script show`, and everything is deleted by prefix.
#1. A small module of two scripts; script create post "tuto.sc.release.v1.price" false 1 (param (var "[x]" {type is_double [x]} "Net" is_null:false is_empty:false "10");) "Price" {math round (* [x] 1.2) 2;} "Gross"; script create get "tuto.sc.release.v1.stock" false 1 (param (var "[sku]" {type is_varchar [sku] 12} "Code" is_null:false is_empty:false "A1");) "Stock" {concat [sku] ": 12 in stock";} "Text"; #2. Copy one script to another method, then rename the copy; script copy "tuto.sc.release.v1.price.post" get "tuto.sc.release.v1.price_preview"; script rename "tuto.sc.release.v1.price_preview.get" get "tuto.sc.release.v1.quote"; #3. Branch the module into v2, then promote v2 to stable - both prefixes end with a dot; script copy all "tuto.sc.release.v1." "tuto.sc.release.v2."; script rename all "tuto.sc.release.v2." "tuto.sc.release.stable."; #4. List the scripts of the module; json load "tuto_sc_all" (script show); -> "[list]" ""; -> "[keys]" (json fields "tuto_sc_all" "/"); json load "tuto_sc_k" [keys]; for (-> "[i]" 0) (< [i] (json count "tuto_sc_k" "/")) (++ "[i]") { -> "[k]" (json select "tuto_sc_k" (concat "/[" [i] "]")); if (string matches [k] "tuto\.sc\.release\..*") {-> "[list]" (concat [list] [_n_] "- " [k]);}; }; json unload "tuto_sc_k"; json unload "tuto_sc_all"; #5. The stable copy answers like the original; -> "[test]" (concat (execute "tuto.sc.release.stable.price.post" "[x]" 100) " / " (execute "tuto.sc.release.v1.quote.get" "[x]" 100)); #6. Delete the module by its prefix; script delete all "tuto.sc.release."; concat "Scripts:" [list] [_n_] "scripts without any group: " (script show ghost) [_n_] "stable price and quote of 100: " [test] [_n_] "left after delete all: " (script exist "tuto.sc.release.v1.price.post") " " (script exist "tuto.sc.release.stable.stock.get");
Scripts:
- tuto.sc.release.stable.quote.get
- tuto.sc.release.stable.price.post
- tuto.sc.release.stable.stock.get
- tuto.sc.release.v1.price.post
- tuto.sc.release.v1.quote.get
- tuto.sc.release.v1.stock.get
scripts without any group: {}
stable price and quote of 100: 120 / 120
left after delete all: 0 0script copy allandscript rename allkeep the final dot:tuto.sc.release.v1.totuto.sc.release.v2.turnsv1.price.postintov2.price.post.- Use a prefix that only your scripts have before any
allfunction. script show ghostlists scripts without a group after such moves.
Stack and delay options of a batch script
A nightly export is run through the stack: priority 5, two runs in parallel, replayable after an error, and a delay condition that postpones it by 10 minutes before 1 am. The options are read back and the requests to replay them elsewhere are generated.
#1. A nightly export that may be retried 3 times by the stack; script create post "tuto.sc.export.nightly" true 3 (param (var "[day]" {type is_date [day]} "Day to export" is_null:false is_empty:false "2026-11-01") ;) "Export the orders of one day" { concat "export of " [day] " done"; } "A status sentence"; #2. Stack options: priority 5, 2 runs in parallel max, replayable after an error; script set stack "tuto.sc.export.nightly.post" 5 2 true; #3. Delay: run only after 1 am, otherwise the stack retries 10 minutes later; script set delay "tuto.sc.export.nightly.post" 10 min { >= (date hour (date systime)) 1; }; #4. Read the options back; json load "tuto_sc_opt" (script get "tuto.sc.export.nightly.post"); -> "[opt]" (concat "priority=" (json select "tuto_sc_opt" "/priority") ", threads=" (json select "tuto_sc_opt" "/nb_in_thread") ", replayable=" (json select "tuto_sc_opt" "/is_replayable") ", attempts=" (json select "tuto_sc_opt" "/nbAttempt") ", log=" (json select "tuto_sc_opt" "/activateLog") ", delay=" (json select "tuto_sc_opt" "/delay_value") " " (json select "tuto_sc_opt" "/delay_type")); json unload "tuto_sc_opt"; #5. The requests to replay these options on another server, and a stack call; -> "[delay]" (string replace (script generate delay "tuto.sc.export.nightly.post") "j23i88m90m76i39t04r09y35p14a96y09e57t36" ""); -> "[stack]" (string regexp_replace (script generate stack "tuto.sc.export.nightly.post") "[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]{8}" "YYYY-MM-DD hh:mm:ss"); script delete "tuto.sc.export.nightly.post"; concat [opt] [_n_] [delay] [_n_] [stack];
priority=5, threads=2, replayable=1, attempts=3, log=1, delay=10 min
script set delay "tuto.sc.export.nightly.post" 10 min {
>= (date hour (date systime)) 1;
};
script set stack "tuto.sc.export.nightly.post" 5 2 true;
stack_execute "YYYY-MM-DD hh:mm:ss" "tuto.sc.export.nightly.post"
"[day]" "2026-11-01"
;script generate delaystarts with an internal marker used by the editor: remove it withstring replacewhen you keep the text.script generate stackgives astack_executecall with the current date.- See the execution-modes recipe with a gate parameter to watch the delay in action.
Grant a script to a group, then revert
A new price service is published to the existing group sample: groups and users before and after, the grant check, the user's rights, then the grant is revoked and the script deleted.
#1. A new web service and its default rights; script create get "tuto.sc.catalog.price" false 1 (param (var "[sku]" {type is_varchar [sku] 12} "Product code" is_null:false is_empty:false "BK-0042");) "Public price of a product" {concat "{\"sku\": \"" [sku] "\", \"price\": 19.9}";} "JSON"; -> "[s]" "tuto.sc.catalog.price.get"; -> "[g]" "sample"; json load "tuto_sc_grp" (group get script [g]); -> "[n0]" (json count "tuto_sc_grp" "/"); json unload "tuto_sc_grp"; json load "tuto_sc_g" (script show groups [s]); -> "[before]" (concat "groups " (string regexp_replace (json fields "tuto_sc_g" "/") "\s+" "") ", in " [g] ": " (group is granted script [s] [g])); json unload "tuto_sc_g"; #2. Publish it to the existing group sample; -> "[grant]" (group grant script [s] [g]); json load "tuto_sc_grp" (group get script [g]); json load "tuto_sc_g" (script show groups [s]); -> "[after]" (concat "groups " (string regexp_replace (json fields "tuto_sc_g" "/") "\s+" "") ", in " [g] ": " (group is granted script [s] [g]) ", scripts of " [g] ": " [n0] " -> " (json count "tuto_sc_grp" "/")); json unload "tuto_sc_grp"; json unload "tuto_sc_g"; #3. Who can run it now; json load "tuto_sc_usr" (script show users [s]); -> "[users]" (concat "granted users " (string regexp_replace (json fields "tuto_sc_usr" "/") "\s+" "") ", admin granted: " (script is granted [s] "admin") ", admin in " [g] ": " (group is granted user "admin" [g])); json unload "tuto_sc_usr"; #4. Revert: ungrant, check, delete; -> "[revoke]" (group ungrant script [s] [g]); -> "[final]" (concat "in " [g] ": " (group is granted script [s] [g])); script delete [s]; concat [before] [_n_] [grant] [_n_] [after] [_n_] [users] [_n_] [revoke] [_n_] [final];
groups ["admin","sys"], in sample: 0 Script granted with successful. groups ["admin","sys","sample"], in sample: 1, scripts of sample: 41 -> 42 granted users ["ai","admin","mentdb"], admin granted: 1, admin in sample: 1 Script ungranted with successful. in sample: 0
group get scriptlists every script of a group.group grant_all scriptpublishes all scripts with a prefix in one request.script is grantedanswers for one login,group is granted userfor one group.
Business types on the parameters of a service
A supplier onboarding service types its eleven parameters with business checks: SIRET, IBAN, BIC, French phone, URL, IPv4, date, hour, small int, Y/N boolean and JSON. The valid examples pass, then each field is sent once with a wrong value to see which check refuses it.
#1. A supplier onboarding service: every parameter has a business type; script create post "tuto.sc.supplier.onboard" false 1 (param (var "[siret]" {type is_siret [siret]} "SIRET of the company" is_null:false is_empty:false "732 829 320 00074") (var "[iban]" {type is_iban [iban]} "Bank account" is_null:false is_empty:false "FR76 3000 6000 0112 3456 7890 189") (var "[bic]" {type is_bic [bic]} "Bank BIC" is_null:false is_empty:false "BNPAFRPPXXX") (var "[phone]" {type is_phone_fr [phone]} "Phone" is_null:false is_empty:false "+262 692 12 34 56") (var "[site]" {type is_url [site]} "Web site" is_null:false is_empty:false "https://www.acme.test") (var "[ip]" {type is_ipv4 [ip]} "IP allowed to call the API" is_null:false is_empty:false "192.168.1.254") (var "[start]" {type is_valid_date [start] "yyyy-MM-dd"} "Contract start" is_null:false is_empty:false "2026-12-01") (var "[cutoff]" {type is_hour_without_sec [cutoff]} "Daily order cutoff" is_null:false is_empty:false "17:30") (var "[days]" {type is_small_int [days]} "Payment terms in days" is_null:false is_empty:false "30") (var "[vat]" {type is_bool [vat] "Y" "N"} "Subject to VAT, Y or N" is_null:false is_empty:false "Y") (var "[meta]" {type is_json [meta]} "Extra data" is_null:false is_empty:false "{\"tier\": 2}") ;) "Register a supplier" { concat "supplier " (string regexp_replace [siret] "\s" "") " registered, terms " [days] " days"; } "A confirmation"; #2. The valid example values, then one wrong value at a time; -> "[good]" "732 829 320 00074|FR76 3000 6000 0112 3456 7890 189|BNPAFRPPXXX|+262 692 12 34 56|https://www.acme.test|192.168.1.254|2026-12-01|17:30|30|Y|{\"tier\": 2}"; -> "[bad]" "732 829 320 00075|FR76 3000 6000 0112 3456 7890 180|BNP1|0262 12|www acme|192.168.1.300|2026-02-30|17:75|70000|maybe|{tier: 2"; -> "[names]" "siret,iban,bic,phone,site,ip,start,cutoff,days,vat,meta"; -> "[out]" (concat "all valid: " (execute "tuto.sc.supplier.onboard.post" "[siret]" (atom get [good] 1 "|") "[iban]" (atom get [good] 2 "|") "[bic]" (atom get [good] 3 "|") "[phone]" (atom get [good] 4 "|") "[site]" (atom get [good] 5 "|") "[ip]" (atom get [good] 6 "|") "[start]" (atom get [good] 7 "|") "[cutoff]" (atom get [good] 8 "|") "[days]" (atom get [good] 9 "|") "[vat]" (atom get [good] 10 "|") "[meta]" (atom get [good] 11 "|"))); for (-> "[f]" 1) (<= [f] 11) (++ "[f]") { #3. Replace field f by its wrong value; for (-> "[i]" 1) (<= [i] 11) (++ "[i]") { env set var (concat "[v" [i] "]") (if (== [i] [f]) {atom get [bad] [i] "|"} {atom get [good] [i] "|"}); }; try { execute "tuto.sc.supplier.onboard.post" "[siret]" [v1] "[iban]" [v2] "[bic]" [v3] "[phone]" [v4] "[site]" [v5] "[ip]" [v6] "[start]" [v7] "[cutoff]" [v8] "[days]" [v9] "[vat]" [v10] "[meta]" [v11]; -> "[out]" (concat [out] [_n_] (atom get [names] [f] ",") ": accepted?"); } { -> "[out]" (concat [out] [_n_] (atom get [names] [f] ",") " '" (atom get [bad] [f] "|") "' refused by " (string regexp_extract [err] "\((type [a-z_0-9]+)" 1)); } "[err]"; }; script delete "tuto.sc.supplier.onboard.post"; [out];
all valid: supplier 73282932000074 registered, terms 30 days
siret '732 829 320 00075' refused by type is_siret
iban 'FR76 3000 6000 0112 3456 7890 180' refused by type is_iban
bic 'BNP1' refused by type is_bic
phone '0262 12' refused by type is_phone_fr
site 'www acme' refused by type is_url
ip '192.168.1.300' refused by type is_ipv4
start '2026-02-30' refused by type is_valid_date
cutoff '17:75' refused by type is_hour_without_sec
days '70000' refused by type is_small_int
vat 'maybe' refused by type is_bool
meta '{tier: 2' refused by type is_json- SIRET and IBAN carry a check key: one wrong digit is enough to be refused.
type is_valid_datetakes a Java pattern: 2026-02-30 is refused.- Use
type is_bool [x] "Y" "N"to accept your own true and false words.
