diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..bddc9f221adca60c6a5ab3a3c72b1cafce1219bd
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "building-blocks"]
+ path = building-blocks
+ url = git@git.qoretechnologies.com:qorus/building-blocks.git
diff --git a/03_basics_building_blocks/01_exchange_rates_app/01_job/BASIC-TRAINING-EXCHANGE-APP.qgroup.yaml b/03_basics_building_blocks/01_exchange_rates_app/01_job/BASIC-TRAINING-EXCHANGE-APP.qgroup.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..30349632a7486b567b7b2a44bf2aa2f3f668d6eb
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/01_job/BASIC-TRAINING-EXCHANGE-APP.qgroup.yaml
@@ -0,0 +1,4 @@
+# This is a generated file, don't edit!
+type: group
+name: BASIC-TRAINING-EXCHANGE-APP
+desc: "Basic training exchange application objects"
diff --git a/03_basics_building_blocks/01_exchange_rates_app/01_job/README.md b/03_basics_building_blocks/01_exchange_rates_app/01_job/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a42fb20db482bc6e2c5d1f409e527a3096f1504b
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/01_job/README.md
@@ -0,0 +1,101 @@
+# Implementing Qorus Jobs
+
+Jobs are simple tasks executed on a schedule, similar to a cron job. The information and status of processing is stored in the database and is reported through system APIs.
+
+# Job Definition Files
+
+Qorus jobs are defined using two files, one file for the job metadata in the YAML format and the other for the job code. In the YAML file there is code tag that is used to reference to the job's code, the path is relative. Job definition files are used to create job definitions in the Qorus schema that will be run according to their schedule.
+
+**NOTE**: It's recommended to have YAML definition files in the same directory with the user code.
+
+Jobs can be easily created using the Qorus Developer Tools extension that will generate job metadata and code:
+
+
+
+**NOTE**: the YAML file generated by the extension should not be manually edited otherwise it may cause code and metadata misalignments and hence problems with the extension usability.
+
+The metadata file consists of metadata tags defining the job. The code file contains Qore or Java language code that makes up the job.
+
+## Example Job Definition
+
+Metadata:
+```yaml
+# This is a generated file, don't edit!
+type: job
+name: basics-logging-job
+desc: "Job implementation example. The job simply logs every hour."
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusJob
+class-name: BasicsLoggingJob
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+lang: qore
+schedule:
+ minutes: "0"
+ hours: "*"
+ days: "*"
+ months: "*"
+ dow: "*"
+version: "1.0"
+code: basics-logging-job-1.0.qjob
+```
+
+The training job in this section simply logs a message in the job log file; it is built with a no-code approach using the `BBM_LogMessage` building block.
+
+Handcoded Qore equivalent:
+```php
+%strict-args
+%require-types
+%new-style
+%enable-all-warnings
+
+class BasicsLoggingJob inherits QorusJob {
+ run() {
+ logInfo("job info: %y", getInfo());
+ }
+}
+```
+
+Information on how to implement Qorus Objects using YAML format can be found [here](https://qoretechnologies.com/manual/qorus/latest/qorus/implementingqorusobjectsusingyaml.html).
+
+The YAML schema used for validation of the job metadata can be downloaded [here](https://qoretechnologies.com/manual/qorus/latest/qorus/job_schema.yaml).
+
+The job's code must have a **run()** method. This function will be called when the job is scheduled. In the example above the run() method will be called every hour.
+
+Whenever a job is executed, a record in the **JOB_INSTANCE** table is created. If an error is raised by calling [errWithInfo()](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1Job_1_1JobApi.html#a88cd263ee9118e7735838ca3aac2990f) with a severity greater than [OMQ::ES_Warning](https://qoretechnologies.com/manual/qorus/latest/qorus/group__JobStatusDescriptions.html), the job instance will get a status of [OMQ::JS_Error](https://qoretechnologies.com/manual/qorus/latest/qorus/group__JobStatusDescriptions.html#gac0c70076e9afec841a5455b36cb4713c), otherwise the job instance will get a status of [OMQ::JS_Complete](https://qoretechnologies.com/manual/qorus/latest/qorus/group__JobStatusDescriptions.html#ga814061c22780aa392c167ba7b776a75a). In the case that the Qorus system crashes while the job has a [OMQ::JS_InProgress](https://qoretechnologies.com/manual/qorus/latest/qorus/group__JobStatusDescriptions.html#ga959be579cfa577351bfcd12ba55fa840) status, then, when the system is recovered, the job_instance row will get a [OMQ::JS_Crash](https://qoretechnologies.com/manual/qorus/latest/qorus/group__JobStatusDescriptions.html#gab536e747502e01363a328ecb99e2efb7) status.
+
+Job instances can never be recovered; the status is saved for information purposes only.
+
+To save information about processing status, call the [saveInfo()](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1Job_1_1JobApi.html#a4b7d0fa64af46b39d37fbebeff622131) function.
+
+The log() function is used to save information in the job's log file; see [System, Service, Workflow, and Job Logging](https://qoretechnologies.com/manual/qorus/latest/qorus/logging.html) for more information about log file locations and file formats.
+
+# Job Cron Schedule
+
+It basically follows the format of a cron job time specification; it is made up of 5 fields separated by spaces as follows:
+
+
+
Field
Values
+
+
minutes
0-59
+
+
hours
0-23
+
+
days of month
1-31
+
+
months
1-12 (or 3-letter English month abbreviations: Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec)
+
+
days of week
0-7 (0 and 7 are Sunday, or 3-letter English day abbreviations: Sun, Mon, Tue, Wed, Thu, Fri, Sat)
+
+
+[More about job cron schedule](https://qoretechnologies.com/manual/qorus/latest/qorus/implementingjobs.html#jobschedule)
+
+---
+
+
diff --git a/03_basics_building_blocks/01_exchange_rates_app/01_job/img/testjob.gif b/03_basics_building_blocks/01_exchange_rates_app/01_job/img/testjob.gif
new file mode 100644
index 0000000000000000000000000000000000000000..83ce0b47ee41e08e03372de0c85d84f2a70ddc81
Binary files /dev/null and b/03_basics_building_blocks/01_exchange_rates_app/01_job/img/testjob.gif differ
diff --git a/03_basics_building_blocks/01_exchange_rates_app/01_job/java/bb-basics-logging-job-java-1.0.qjob.yaml b/03_basics_building_blocks/01_exchange_rates_app/01_job/java/bb-basics-logging-job-java-1.0.qjob.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..da7b2910cef4213d40f26f403fa4cebbb7f08a00
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/01_job/java/bb-basics-logging-job-java-1.0.qjob.yaml
@@ -0,0 +1,47 @@
+# This is a generated file, don't edit!
+type: job
+name: bb-basics-logging-job-java
+desc: Job implementation example. The job simply logs every hour.
+lang: java
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusJob
+class-name: BasicsLoggingJobJava
+classes:
+ - BBM_LogMessage
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+schedule:
+ minutes: "0"
+ hours: "0"
+ days: "*"
+ months: "*"
+ dow: "*"
+version: '1.0'
+code: bb_basics_logging_job_java_1_0_job/BasicsLoggingJobJava.java
+class-connections:
+ log:
+ - class: BBM_LogMessage
+ connector: logMessage
+ trigger: run
+config-items:
+ - name: log-message-level
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-string
+ value:
+ "job info: %y"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-args
+ value:
+ "$info:*"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ value_true_type: string
diff --git a/03_basics_building_blocks/01_exchange_rates_app/01_job/java/bb_basics_logging_job_java_1_0_job/BasicsLoggingJobJava.java b/03_basics_building_blocks/01_exchange_rates_app/01_job/java/bb_basics_logging_job_java_1_0_job/BasicsLoggingJobJava.java
new file mode 100644
index 0000000000000000000000000000000000000000..c005939576dd901e5fbf445d2f665d1388b34a4e
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/01_job/java/bb_basics_logging_job_java_1_0_job/BasicsLoggingJobJava.java
@@ -0,0 +1,75 @@
+import qore.OMQ.*;
+import qore.OMQ.UserApi.*;
+import qore.OMQ.UserApi.Job.*;
+import org.qore.jni.QoreJavaApi;
+import org.qore.jni.QoreObject;
+import java.util.Map;
+import org.qore.jni.Hash;
+import java.lang.reflect.Method;
+import java.lang.reflect.InvocationTargetException;
+import qore.BBM_LogMessage;
+
+class BasicsLoggingJobJava extends QorusJob {
+ // ==== GENERATED SECTION! DON'T EDIT! ==== //
+ ClassConnections_BasicsLoggingJobJava classConnections;
+
+ // ======== GENERATED SECTION END ========= //
+ public BasicsLoggingJobJava() throws Throwable {
+ super();
+ // ==== GENERATED SECTION! DON'T EDIT! ==== //
+ classConnections = new ClassConnections_BasicsLoggingJobJava();
+ // ======== GENERATED SECTION END ========= //
+ }
+
+ // ==== GENERATED SECTION! DON'T EDIT! ==== //
+ public void run() throws Throwable {
+ classConnections.log(null);
+ }
+ // ======== GENERATED SECTION END ========= //
+}
+
+// ==== GENERATED SECTION! DON'T EDIT! ==== //
+class ClassConnections_BasicsLoggingJobJava {
+ // map of prefixed class names to class instances
+ private final Hash classMap;
+
+ ClassConnections_BasicsLoggingJobJava() throws Throwable {
+ classMap = new Hash();
+ UserApi.startCapturingObjectsFromJava();
+ try {
+ classMap.put("BBM_LogMessage", QoreJavaApi.newObjectSave("BBM_LogMessage"));
+ } finally {
+ UserApi.stopCapturingObjectsFromJava();
+ }
+ }
+
+ Object callClassWithPrefixMethod(final String prefixedClass, final String methodName,
+ Object params) throws Throwable {
+ UserApi.logDebug("ClassConnections_BasicsLoggingJobJava: callClassWithPrefixMethod: method: %s class: %y", methodName, prefixedClass);
+ final Object object = classMap.get(prefixedClass);
+
+ if (object instanceof QoreObject) {
+ QoreObject qoreObject = (QoreObject)object;
+ return qoreObject.callMethod(methodName, params);
+ } else {
+ final Method method = object.getClass().getMethod(methodName, Object.class);
+ try {
+ return method.invoke(object, params);
+ } catch (InvocationTargetException ex) {
+ throw ex.getCause();
+ }
+ }
+ }
+
+ public Object log(Object params) throws Throwable {
+ // convert varargs to a single argument if possible
+ if (params != null && params.getClass().isArray() && ((Object[])params).length == 1) {
+ params = ((Object[])params)[0];
+ }
+ UserApi.logDebug("log called with data: %y", params);
+
+ UserApi.logDebug("calling logMessage: %y", params);
+ return callClassWithPrefixMethod("BBM_LogMessage", "logMessage", params);
+ }
+}
+// ======== GENERATED SECTION END ========= //
diff --git a/03_basics_building_blocks/01_exchange_rates_app/01_job/python/bb-basics-logging-job-python-1.0.qjob.py b/03_basics_building_blocks/01_exchange_rates_app/01_job/python/bb-basics-logging-job-python-1.0.qjob.py
new file mode 100644
index 0000000000000000000000000000000000000000..219baf8172ced1af8f4c062d132a227191c84ae0
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/01_job/python/bb-basics-logging-job-python-1.0.qjob.py
@@ -0,0 +1,38 @@
+from job import QorusJob
+from qore.__root__ import BBM_LogMessage
+
+class BasicsLoggingJobPython(QorusJob):
+ def __init__(self):
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ self.class_connections = ClassConnections_BasicsLoggingJobPython()
+ ############ GENERATED SECTION END ############
+
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ def run(self):
+ self.class_connections.Connection_1()
+ ############ GENERATED SECTION END ############
+
+####### GENERATED SECTION! DON'T EDIT! ########
+class ClassConnections_BasicsLoggingJobPython:
+ def __init__(self):
+ UserApi.startCapturingObjectsFromPython()
+ # map of prefixed class names to class instances
+ self.class_map = {
+ 'BBM_LogMessage': BBM_LogMessage(),
+ }
+ UserApi.stopCapturingObjectsFromPython()
+
+ def callClassWithPrefixMethod(self, prefixed_class, method, *argv):
+ UserApi.logDebug("ClassConnections_BasicsLoggingJobPython: callClassWithPrefixMethod: method: %s class: %y", method, prefixed_class)
+ return getattr(self.class_map[prefixed_class], method)(*argv)
+
+ def Connection_1(self, *params):
+ UserApi.logDebug("Connection_1 called with data: %y", params)
+ # convert varargs to a single argument if possible
+ if (type(params) is list or type(params) is tuple) and (len(params) == 1):
+ params = params[0]
+ UserApi.logDebug("calling logMessage: %y", params)
+ params = self.callClassWithPrefixMethod("BBM_LogMessage", "logMessage", params)
+ UserApi.logDebug("output from logMessage: %y", params)
+ return params
+############ GENERATED SECTION END ############
diff --git a/03_basics_building_blocks/01_exchange_rates_app/01_job/python/bb-basics-logging-job-python-1.0.qjob.yaml b/03_basics_building_blocks/01_exchange_rates_app/01_job/python/bb-basics-logging-job-python-1.0.qjob.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..bc0bdd7e62a923e48b64918f2ecfb3f9df92d561
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/01_job/python/bb-basics-logging-job-python-1.0.qjob.yaml
@@ -0,0 +1,47 @@
+# This is a generated file, don't edit!
+type: job
+name: bb-basics-logging-job-python
+desc: Job implementation example. The job simply logs every hour.
+lang: python
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusJob
+class-name: BasicsLoggingJobPython
+classes:
+ - BBM_LogMessage
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+schedule:
+ minutes: "0"
+ hours: "0"
+ days: "*"
+ months: "*"
+ dow: "*"
+version: '1.0'
+code: bb-basics-logging-job-python-1.0.qjob.py
+class-connections:
+ Connection_1:
+ - class: BBM_LogMessage
+ connector: logMessage
+ trigger: run
+config-items:
+ - name: log-message-level
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-string
+ value:
+ "job info: %y"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-args
+ value:
+ "$info:*"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ value_true_type: string
diff --git a/03_basics_building_blocks/01_exchange_rates_app/01_job/qore/bb-basics-logging-job-1.0.qjob b/03_basics_building_blocks/01_exchange_rates_app/01_job/qore/bb-basics-logging-job-1.0.qjob
new file mode 100644
index 0000000000000000000000000000000000000000..e382198e43b97404753c81841398b212caca97d1
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/01_job/qore/bb-basics-logging-job-1.0.qjob
@@ -0,0 +1,45 @@
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+class BasicsLoggingJob inherits QorusJob {
+ private {
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ ClassConnections_BasicsLoggingJob class_connections();
+ ############ GENERATED SECTION END ############
+ }
+
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ run() {
+ class_connections.run();
+ }
+ ############ GENERATED SECTION END ############
+}
+
+####### GENERATED SECTION! DON'T EDIT! ########
+class ClassConnections_BasicsLoggingJob {
+ private {
+ # map of prefixed class names to class instances
+ hash class_map;
+ }
+
+ constructor() {
+ class_map = {
+ "BBM_LogMessage": new BBM_LogMessage(),
+ };
+ }
+
+ auto callClassWithPrefixMethod(string prefixed_class, string method) {
+ UserApi::logDebug("ClassConnections_BasicsLoggingJob: callClassWithPrefixMethod: method: %s class: %y", method, prefixed_class);
+ return call_object_method_args(class_map{prefixed_class}, method, argv);
+ }
+
+ auto run(auto params) {
+ UserApi::logDebug("run called with data: %y", params);
+
+ UserApi::logDebug("calling logMessage: %y", params);
+ return callClassWithPrefixMethod("BBM_LogMessage", "logMessage", params);
+ }
+}
+############ GENERATED SECTION END ############
diff --git a/03_basics_building_blocks/01_exchange_rates_app/01_job/qore/bb-basics-logging-job-1.0.qjob.yaml b/03_basics_building_blocks/01_exchange_rates_app/01_job/qore/bb-basics-logging-job-1.0.qjob.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ac51a4873ed859162156529ab9061e8071667dd6
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/01_job/qore/bb-basics-logging-job-1.0.qjob.yaml
@@ -0,0 +1,47 @@
+# This is a generated file, don't edit!
+type: job
+name: bb-basics-logging-job
+desc: Job implementation example. The job simply logs every hour.
+lang: qore
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusJob
+class-name: BasicsLoggingJob
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+schedule:
+ minutes: "0"
+ hours: "0"
+ days: "*"
+ months: "*"
+ dow: "*"
+version: '1.0'
+classes:
+ - BBM_LogMessage
+code: bb-basics-logging-job-1.0.qjob
+class-connections:
+ run:
+ - class: BBM_LogMessage
+ connector: logMessage
+ trigger: run
+config-items:
+ - name: log-message-level
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-string
+ value:
+ "job info: %y"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-args
+ value:
+ "$info:*"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ value_true_type: string
diff --git a/03_basics_building_blocks/01_exchange_rates_app/02_service/README.md b/03_basics_building_blocks/01_exchange_rates_app/02_service/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..7a19c9508b21ddacf2e84212905e3bd2f6a97cbb
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/02_service/README.md
@@ -0,0 +1,99 @@
+# Implementing Qorus Services
+
+User services are like named and versioned API sets that can be live-upgraded. User service methods are available to all workflows and jobs (and to other services) and can be automatically exported to other applications through lightweight web services (RPC protocols and REST) through the [HTTP server](https://qoretechnologies.com/manual/qorus/latest/qorus/sysarch.html#httpserver) (in fact, this is the default behavior, but can be inhibited by setting the internal flag on the service method).
+
+Additionally, services can also have one or more background threads, and therefore do not have to wait for input data to perform some processing.
+
+Services are loaded from the database into their own program objects and have their own [internal API](https://qoretechnologies.com/manual/qorus/latest/qorus/serviceapi.html). All user-defined services must be of the type "user". System services are delivered with Qorus and should not be modified, as this is likely to affect system stability.
+
+The following diagram illustrates the attributes of a service:
+
+
+
+[Service methods](https://qoretechnologies.com/manual/qorus/latest/qorus/implementingservices.html#servicemethods) define the user-visible interface of the service. The logic in a service program is defined by the method code and any library objects (functions, classes, or constant definitions) loaded into the service's program object.
+
+Besides the name, version, type (always "user" for user-defined services), and description of the service, there is the *"autostart"* flag. If the *"autostart"* flag is set to True, then the service will be started automatically whenever Qorus is started or whenever its RBAC access group is renabled.
+
+## Service Loading
+
+Otherwise, a service is loaded and initialized any time a method of that service is called and the service has not already been loaded. In this case all the service's methods are loaded and parsed into the same program object, along with any library function, classes, and constants associated with the service. Any parse exceptions during service loading will prohibit the service from being loaded.
+
+## Service Initialization
+
+If a service has an **init()** method, this method is called as soon as the service has been loaded and parsed, and before any call to a service method is made. For example, if a service with an **init()** method is loaded because a call to another method is made, the **init()** method will be called first, and then the called method will be called and the value returned to the caller.
+
+An exception when running the **init()** method will cause the service to be unloaded, and the exception will be returned to the caller.
+
+If the service has a **start()** method, then it must also have a **stop()** method. The **start()** method will be run in a separate thread and is expected to run until the **stop()** method is called. The **stop()** method should signal the routine running in the **start()** method to exit.
+
+When the **start()** method returns, the service is automatically unloaded. To start threads in user code in a service, use the [**startThread()**](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1Service_1_1ServiceApi.html#a80a104bcc6723c7bf68a1a950f365372) or [**startThreadArgs()**](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1Service_1_1ServiceApi.html#a68268ee2277e4a0bdcda70b10643c44e) functions (in which case, to use these functions, the service must also have a **stop()** method).
+
+## Service Definition File
+
+Qorus services are defined using two files. Service metadata tags are defined in the YAML format and service code written in Java/Qore language. In the YAML file there is code tag that is used to reference to the service's code, the path is relative.
+
+**NOTE**: It's recommended to have YAML definition files in the same directory with the user code.
+
+Service definition files are used to create service objects in the Qorus schema that can then be loaded and their methods can be called from any Qorus code, and, for external methods, from external applications through lightweight web-service protocols exported through the [HTTP server](https://qoretechnologies.com/manual/qorus/latest/qorus/sysarch.html#httpserver) as well.
+
+Services can be easily created using the Qorus Developer Tools extension that will generate service metadata and code:
+
+
+
+**NOTE**: the YAML file generated by the extension should not be manually edited otherwise it may cause code and metadata misalignments and hence problems with the extension usability.
+
+### Example Service Definition
+
+Metadata:
+```yaml
+# This is a generated file, don't edit!
+type: service
+name: basics-simple-service
+desc: "Service simple example"
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusService
+class-name: BasicsSimpleService
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+lang: qore
+autostart: true
+version: "1.0"
+servicetype: USER
+code: basics-simple-service-1.0.qsd
+methods:
+ - name: init
+ desc: initialization of the service
+```
+
+The training job in this section simply logs a message in the job log file; it is built with a no-code approach using the `BBM_LogMessage` building block.
+
+Handcoded Qore equivalent:
+```php
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+class BasicsSimpleService inherits QorusService {
+ init() {
+ logInfo("service is initialized");
+ }
+}
+```
+
+Information on how to implement Qorus Objects using YAML format can be found [here](https://qoretechnologies.com/manual/qorus/latest/qorus/implementingqorusobjectsusingyaml.html).
+
+The YAML schema used for validation of the service metadata can be downloaded [here](https://qoretechnologies.com/manual/qorus/latest/qorus/service_schema.yaml).
+
+Service must have at least one method defined. The tags are parsed and validated according to the schema mentioned above by the [oload](https://qoretechnologies.com/manual/qorus/latest/qorus/commandline.html#oload) program, which will create the service objects in the Qorus database according to the definitions in the file.
+
+Service method definitions are defined in the service metadata under the `methods` tag.
+
+---
+
diff --git a/03_basics_building_blocks/01_exchange_rates_app/02_service/img/testservice.gif b/03_basics_building_blocks/01_exchange_rates_app/02_service/img/testservice.gif
new file mode 100644
index 0000000000000000000000000000000000000000..c52812c1a3874a7cbd38e2dbecaae1046390d14f
Binary files /dev/null and b/03_basics_building_blocks/01_exchange_rates_app/02_service/img/testservice.gif differ
diff --git a/03_basics_building_blocks/01_exchange_rates_app/02_service/java/bb-basics-simple-service-java-1.0.qsd.yaml b/03_basics_building_blocks/01_exchange_rates_app/02_service/java/bb-basics-simple-service-java-1.0.qsd.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..dc6944586c4d4dd54225ca2b5cb9b3c6bb7631ab
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/02_service/java/bb-basics-simple-service-java-1.0.qsd.yaml
@@ -0,0 +1,43 @@
+# This is a generated file, don't edit!
+type: service
+name: bb-basics-simple-service-java
+desc: Service simple example
+lang: java
+author:
+ - Qore Technologies, s.r.o.
+autostart: true
+base-class-name: QorusService
+class-name: BasicsSimpleServiceJava
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+version: '1.0'
+classes:
+ - BBM_LogMessage
+servicetype: USER
+code: bb_basics_simple_service_java_1_0_service/BasicsSimpleServiceJava.java
+class-connections:
+ init_log:
+ - class: BBM_LogMessage
+ connector: logMessage
+ trigger: init
+config-items:
+ - name: log-message-level
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-string
+ value:
+ "service is initialized"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-args
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+methods:
+ - name: init
+ desc: initialization of the service
diff --git a/03_basics_building_blocks/01_exchange_rates_app/02_service/java/bb_basics_simple_service_java_1_0_service/BasicsSimpleServiceJava.java b/03_basics_building_blocks/01_exchange_rates_app/02_service/java/bb_basics_simple_service_java_1_0_service/BasicsSimpleServiceJava.java
new file mode 100644
index 0000000000000000000000000000000000000000..cc7991b26e4d8a83c0b5936ee2e11587779a0211
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/02_service/java/bb_basics_simple_service_java_1_0_service/BasicsSimpleServiceJava.java
@@ -0,0 +1,75 @@
+import qore.OMQ.*;
+import qore.OMQ.UserApi.*;
+import qore.OMQ.UserApi.Service.*;
+import org.qore.jni.QoreJavaApi;
+import org.qore.jni.QoreObject;
+import java.util.Map;
+import org.qore.jni.Hash;
+import java.lang.reflect.Method;
+import java.lang.reflect.InvocationTargetException;
+import qore.BBM_LogMessage;
+
+class BasicsSimpleServiceJava extends QorusService {
+ // ==== GENERATED SECTION! DON'T EDIT! ==== //
+ ClassConnections_BasicsSimpleServiceJava classConnections;
+
+ // ======== GENERATED SECTION END ========= //
+ public BasicsSimpleServiceJava() throws Throwable {
+ super();
+ // ==== GENERATED SECTION! DON'T EDIT! ==== //
+ classConnections = new ClassConnections_BasicsSimpleServiceJava();
+ // ======== GENERATED SECTION END ========= //
+ }
+
+ // ==== GENERATED SECTION! DON'T EDIT! ==== //
+ public void init() throws Throwable {
+ classConnections.init_log(null);
+ }
+ // ======== GENERATED SECTION END ========= //
+}
+
+// ==== GENERATED SECTION! DON'T EDIT! ==== //
+class ClassConnections_BasicsSimpleServiceJava {
+ // map of prefixed class names to class instances
+ private final Hash classMap;
+
+ ClassConnections_BasicsSimpleServiceJava() throws Throwable {
+ classMap = new Hash();
+ UserApi.startCapturingObjectsFromJava();
+ try {
+ classMap.put("BBM_LogMessage", QoreJavaApi.newObjectSave("BBM_LogMessage"));
+ } finally {
+ UserApi.stopCapturingObjectsFromJava();
+ }
+ }
+
+ Object callClassWithPrefixMethod(final String prefixedClass, final String methodName,
+ Object params) throws Throwable {
+ UserApi.logDebug("ClassConnections_BasicsSimpleServiceJava: callClassWithPrefixMethod: method: %s class: %y", methodName, prefixedClass);
+ final Object object = classMap.get(prefixedClass);
+
+ if (object instanceof QoreObject) {
+ QoreObject qoreObject = (QoreObject)object;
+ return qoreObject.callMethod(methodName, params);
+ } else {
+ final Method method = object.getClass().getMethod(methodName, Object.class);
+ try {
+ return method.invoke(object, params);
+ } catch (InvocationTargetException ex) {
+ throw ex.getCause();
+ }
+ }
+ }
+
+ public Object init_log(Object params) throws Throwable {
+ // convert varargs to a single argument if possible
+ if (params != null && params.getClass().isArray() && ((Object[])params).length == 1) {
+ params = ((Object[])params)[0];
+ }
+ UserApi.logDebug("init_log called with data: %y", params);
+
+ UserApi.logDebug("calling logMessage: %y", params);
+ return callClassWithPrefixMethod("BBM_LogMessage", "logMessage", params);
+ }
+}
+// ======== GENERATED SECTION END ========= //
diff --git a/03_basics_building_blocks/01_exchange_rates_app/02_service/python/bb-basics-simple-service-python-1.0.qsd.py b/03_basics_building_blocks/01_exchange_rates_app/02_service/python/bb-basics-simple-service-python-1.0.qsd.py
new file mode 100644
index 0000000000000000000000000000000000000000..b8ae5e241042443c314ee4d7a7fb692281d4f457
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/02_service/python/bb-basics-simple-service-python-1.0.qsd.py
@@ -0,0 +1,38 @@
+from svc import QorusService
+from qore.__root__ import BBM_LogMessage
+
+class BasicsSimpleServicePython(QorusService):
+ def __init__(self):
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ self.class_connections = ClassConnections_BasicsSimpleServicePython()
+ ############ GENERATED SECTION END ############
+
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ def init(self):
+ self.class_connections.log_init()
+ ############ GENERATED SECTION END ############
+
+####### GENERATED SECTION! DON'T EDIT! ########
+class ClassConnections_BasicsSimpleServicePython:
+ def __init__(self):
+ UserApi.startCapturingObjectsFromPython()
+ # map of prefixed class names to class instances
+ self.class_map = {
+ 'BBM_LogMessage': BBM_LogMessage(),
+ }
+ UserApi.stopCapturingObjectsFromPython()
+
+ def callClassWithPrefixMethod(self, prefixed_class, method, *argv):
+ UserApi.logDebug("ClassConnections_BasicsSimpleServicePython: callClassWithPrefixMethod: method: %s class: %y", method, prefixed_class)
+ return getattr(self.class_map[prefixed_class], method)(*argv)
+
+ def log_init(self, *params):
+ UserApi.logDebug("log_init called with data: %y", params)
+ # convert varargs to a single argument if possible
+ if (type(params) is list or type(params) is tuple) and (len(params) == 1):
+ params = params[0]
+ UserApi.logDebug("calling logMessage: %y", params)
+ params = self.callClassWithPrefixMethod("BBM_LogMessage", "logMessage", params)
+ UserApi.logDebug("output from logMessage: %y", params)
+ return params
+############ GENERATED SECTION END ############
diff --git a/03_basics_building_blocks/01_exchange_rates_app/02_service/python/bb-basics-simple-service-python-1.0.qsd.yaml b/03_basics_building_blocks/01_exchange_rates_app/02_service/python/bb-basics-simple-service-python-1.0.qsd.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..f4f46f2c033716a5f22f9e8d1b51a33f02179c28
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/02_service/python/bb-basics-simple-service-python-1.0.qsd.yaml
@@ -0,0 +1,43 @@
+# This is a generated file, don't edit!
+type: service
+name: bb-basics-simple-service-python
+desc: Service simple example
+lang: python
+author:
+ - Qore Technologies, s.r.o.
+autostart: true
+base-class-name: QorusService
+class-name: BasicsSimpleServicePython
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+version: '1.0'
+classes:
+ - BBM_LogMessage
+servicetype: USER
+code: bb-basics-simple-service-python-1.0.qsd.py
+class-connections:
+ log_init:
+ - class: BBM_LogMessage
+ connector: logMessage
+ trigger: init
+config-items:
+ - name: log-message-level
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-string
+ value:
+ "service is initialized"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-args
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+methods:
+ - name: init
+ desc: initialization of the service
diff --git a/03_basics_building_blocks/01_exchange_rates_app/02_service/qore/bb-basics-simple-service-1.0.qsd b/03_basics_building_blocks/01_exchange_rates_app/02_service/qore/bb-basics-simple-service-1.0.qsd
new file mode 100644
index 0000000000000000000000000000000000000000..75812f70352cf42da0342e7618f503f6c607e084
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/02_service/qore/bb-basics-simple-service-1.0.qsd
@@ -0,0 +1,45 @@
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+class BasicsSimpleService inherits QorusService {
+ private {
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ ClassConnections_BasicsSimpleService class_connections();
+ ############ GENERATED SECTION END ############
+ }
+
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ init() {
+ class_connections.log_init();
+ }
+ ############ GENERATED SECTION END ############
+}
+
+####### GENERATED SECTION! DON'T EDIT! ########
+class ClassConnections_BasicsSimpleService {
+ private {
+ # map of prefixed class names to class instances
+ hash class_map;
+ }
+
+ constructor() {
+ class_map = {
+ "BBM_LogMessage": new BBM_LogMessage(),
+ };
+ }
+
+ auto callClassWithPrefixMethod(string prefixed_class, string method) {
+ UserApi::logDebug("ClassConnections_BasicsSimpleService: callClassWithPrefixMethod: method: %s class: %y", method, prefixed_class);
+ return call_object_method_args(class_map{prefixed_class}, method, argv);
+ }
+
+ auto log_init(auto params) {
+ UserApi::logDebug("log_init called with data: %y", params);
+
+ UserApi::logDebug("calling logMessage: %y", params);
+ return callClassWithPrefixMethod("BBM_LogMessage", "logMessage", params);
+ }
+}
+############ GENERATED SECTION END ############
diff --git a/03_basics_building_blocks/01_exchange_rates_app/02_service/qore/bb-basics-simple-service-1.0.qsd.yaml b/03_basics_building_blocks/01_exchange_rates_app/02_service/qore/bb-basics-simple-service-1.0.qsd.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..2d4fd2fa2925a087cbd233a08f1fa286aac0bcbd
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/02_service/qore/bb-basics-simple-service-1.0.qsd.yaml
@@ -0,0 +1,43 @@
+# This is a generated file, don't edit!
+type: service
+name: bb-basics-simple-service
+desc: Service simple example
+lang: qore
+author:
+ - Qore Technologies, s.r.o.
+autostart: true
+base-class-name: QorusService
+class-name: BasicsSimpleService
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+version: '1.0'
+classes:
+ - BBM_LogMessage
+servicetype: USER
+code: bb-basics-simple-service-1.0.qsd
+class-connections:
+ log_init:
+ - class: BBM_LogMessage
+ connector: logMessage
+ trigger: init
+config-items:
+ - name: log-message-level
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-string
+ value:
+ "service is initialized"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-args
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+methods:
+ - name: init
+ desc: initialization of the service
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/README.md b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2397f00451915084e41f82fb927d574ee1584a5a
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/README.md
@@ -0,0 +1,106 @@
+# Implementing Qorus Workflows
+
+Workflows are made up a set of interdependent steps that each perform one traced and restartable action. To design a workflow, a series of logical steps and their dependencies must be defined and then represented by a workflow definition file. Once the high-level design for the workflow has been done, then the logic for the steps can be implemented and the workflow definition and the functions can be loaded into the database and executed.
+
+The following table defines the major elements used when designing and implementing an Qorus workflow:
+
+
+
+
+
Element
+
Description
+
+
+
[Step](#step-definition)
The lowest element in a workflow, represents one traced and restartable action. Each step is defined by at least a primary step code containing the logic for the step, and optionally other code (such as a validation code, run when the step is run in error recovery mode, or an asynchronous back-end code, required for asynchronous steps) and other option attributes.
+
+
+
[Workflow](#workflow-definition)
+
The workflow is the highest level element defining the logic as a set of steps and inter-step dependencies, along with other attributes; workflows process workflow order data instances that in turn contain the data and the status of processing (status of all steps). A running workflow is called a workflow execution instance and can be run either in batch mode (OMQ::WM_Normal), batch recovery mode (OMQ::WM_Recovery), or synchronous mode.
Workflow synchronization events allow multiple workflow orders to synchronize their processing based on a single event
+
+
+
+
+## Workflow Definition
+Workflow definition files define workflow metadata including the steps and dependencies between steps.
+
+Workflows are defined using two files. One file for defining workflow metadata tags in the YAML format and workflow code written in Java/Qore language. In the YAML file there is code tag that is used to reference to the workflow's code, the path is relative. The code tag is optional.
+
+**NOTE**: It's recommended to have YAML definition files in the same directory with the user code.
+
+### Workflow Parameters
+
+The workflow object consists of the step dependencies and other attributes. The following diagram illustrates the attributes of a workflow.
+
+
+
+Workflows can be easily created using the Qorus Developer Tools extension that will generate workflow metadata and code:
+
+
+
+During workflow creation process it's possible to use existing steps and also to define new one as shown above.
+
+**NOTE**: the YAML file generated by the extension should not be manually edited otherwise it may cause code and metadata misalignments and hence problems with the extension usability.
+
+### Example Workflow Definition
+
+```yaml
+# This is a generated file, don't edit!
+type: workflow
+name: BASICS-SIMPLE-WORKFLOW
+desc: "Simple workflow example"
+author:
+ - Qore Technologies, s.r.o.
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+steps:
+ [
+ "BasicsSimpleWorkflowStep:1.0"
+ ]
+version: "1.0"
+autostart: 1
+#code: Workflow.qwf #optional
+```
+
+The inter-step dependencies are defined in the steps key in the workflow definition. The basic format of this data structure is a list. In the example above, 1 step will be created (or updated if the step already exists) with the name and version specifiers and placed in simple linear dependency in the steps list.
+
+Information on how to implement Qorus Objects using YAML format can be found [here](https://qoretechnologies.com/manual/qorus/latest/qorus/implementingqorusobjectsusingyaml.html).
+
+The YAML schema used for validation of the workflow metadata can be downloaded [here](https://qoretechnologies.com/manual/qorus/latest/qorus/workflow_schema.yaml).
+
+### Primary Method
+
+Every step base class has an abstract primary() method where the primary step logic must be defined. See the class documentation for the specific step class for more information on requirements for the primary step method.
+
+```php
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+class BasicsSimpleWorkflowStep inherits QorusNormalStep {
+ primary() {
+ logInfo("BasicsSimpleWorkflowStep was called");
+ }
+}
+```
+
+# Testing
+To test the workflow the **create_order-post_reload.qscript** script can be used. The script will automatically create a workflow order at the end of the deployment. The deployment can be done by Qorus extension for Visual Studio Code. Right click on the folder you want to deploy and then click on *"deploy the directory"*.
+
+---
+
+
[Next: Workflow serial steps →](../04_workflow_serial_steps)
+
+
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/img/testworkflow.gif b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/img/testworkflow.gif
new file mode 100644
index 0000000000000000000000000000000000000000..893483e0ae699838fad88f81a8ac98cac11b0d59
Binary files /dev/null and b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/img/testworkflow.gif differ
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/BB-BASICS-SIMPLE-WORKFLOW-JAVA-1.0.qwf.yaml b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/BB-BASICS-SIMPLE-WORKFLOW-JAVA-1.0.qwf.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..10352bc54f85fa0e41ae05a88847db95ee92419b
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/BB-BASICS-SIMPLE-WORKFLOW-JAVA-1.0.qwf.yaml
@@ -0,0 +1,14 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-SIMPLE-WORKFLOW-JAVA
+desc: Simple workflow example for java
+author:
+ - Qore Technologies, s.r.o.
+autostart: 1
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+version: "1.0"
+steps:
+ [
+ "BB_BasicsSimpleWorkflowStepJava:1.0"
+ ]
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/BB_BasicsSimpleWorkflowStepJava-1.0.qstep.yaml b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/BB_BasicsSimpleWorkflowStepJava-1.0.qstep.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a2705652c7666025b126c7b742335a0b1e05aeae
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/BB_BasicsSimpleWorkflowStepJava-1.0.qstep.yaml
@@ -0,0 +1,37 @@
+# This is a generated file, don't edit!
+type: step
+name: BB_BasicsSimpleWorkflowStepJava
+desc: Simple worklfow step example
+lang: java
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusNormalStep
+class-name: BasicsSimpleWorkflowStepJava
+classes:
+ - BBM_LogMessage
+version: '1.0'
+steptype: NORMAL
+code: bb_basicssimpleworkflowstepjava_1_0_step/BasicsSimpleWorkflowStepJava.java
+class-connections:
+ primary:
+ - class: BBM_LogMessage
+ connector: logMessage
+ trigger: primary
+config-items:
+ - name: log-message-level
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-string
+ value:
+ "BasicsSimpleWorkflowStep was called"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-args
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/bb_basicssimpleworkflowstepjava_1_0_step/BasicsSimpleWorkflowStepJava.java b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/bb_basicssimpleworkflowstepjava_1_0_step/BasicsSimpleWorkflowStepJava.java
new file mode 100644
index 0000000000000000000000000000000000000000..23c00c4875098d4a35cd0d026091005ee0dddfe0
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/bb_basicssimpleworkflowstepjava_1_0_step/BasicsSimpleWorkflowStepJava.java
@@ -0,0 +1,79 @@
+import qore.OMQ.*;
+import qore.OMQ.UserApi.*;
+import qore.OMQ.UserApi.Workflow.*;
+import org.qore.jni.QoreJavaApi;
+import org.qore.jni.QoreObject;
+import java.util.Map;
+import org.qore.jni.Hash;
+import java.lang.reflect.Method;
+import java.lang.reflect.InvocationTargetException;
+import qore.BBM_LogMessage;
+
+class BasicsSimpleWorkflowStepJava extends QorusNormalStep {
+ // ==== GENERATED SECTION! DON'T EDIT! ==== //
+ ClassConnections_BasicsSimpleWorkflowStepJava classConnections;
+
+ // ======== GENERATED SECTION END ========= //
+ public BasicsSimpleWorkflowStepJava() throws Throwable {
+ super();
+ // ==== GENERATED SECTION! DON'T EDIT! ==== //
+ classConnections = new ClassConnections_BasicsSimpleWorkflowStepJava();
+ // ======== GENERATED SECTION END ========= //
+ }
+
+ // ==== GENERATED SECTION! DON'T EDIT! ==== //
+ public void primary() throws Throwable {
+ classConnections.primary(null);
+ }
+
+ public String validation() throws Throwable {
+ return qore.OMQ.$Constants.StatRetry;
+ }
+ // ======== GENERATED SECTION END ========= //
+}
+
+// ==== GENERATED SECTION! DON'T EDIT! ==== //
+class ClassConnections_BasicsSimpleWorkflowStepJava {
+ // map of prefixed class names to class instances
+ private final Hash classMap;
+
+ ClassConnections_BasicsSimpleWorkflowStepJava() throws Throwable {
+ classMap = new Hash();
+ UserApi.startCapturingObjectsFromJava();
+ try {
+ classMap.put("BBM_LogMessage", QoreJavaApi.newObjectSave("BBM_LogMessage"));
+ } finally {
+ UserApi.stopCapturingObjectsFromJava();
+ }
+ }
+
+ Object callClassWithPrefixMethod(final String prefixedClass, final String methodName,
+ Object params) throws Throwable {
+ UserApi.logDebug("ClassConnections_BasicsSimpleWorkflowStepJava: callClassWithPrefixMethod: method: %s class: %y", methodName, prefixedClass);
+ final Object object = classMap.get(prefixedClass);
+
+ if (object instanceof QoreObject) {
+ QoreObject qoreObject = (QoreObject)object;
+ return qoreObject.callMethod(methodName, params);
+ } else {
+ final Method method = object.getClass().getMethod(methodName, Object.class);
+ try {
+ return method.invoke(object, params);
+ } catch (InvocationTargetException ex) {
+ throw ex.getCause();
+ }
+ }
+ }
+
+ public Object primary(Object params) throws Throwable {
+ // convert varargs to a single argument if possible
+ if (params != null && params.getClass().isArray() && ((Object[])params).length == 1) {
+ params = ((Object[])params)[0];
+ }
+ UserApi.logDebug("primary called with data: %y", params);
+
+ UserApi.logDebug("calling logMessage: %y", params);
+ return callClassWithPrefixMethod("BBM_LogMessage", "logMessage", params);
+ }
+}
+// ======== GENERATED SECTION END ========= //
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/create_order-post_reload.qscript b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/create_order-post_reload.qscript
new file mode 100755
index 0000000000000000000000000000000000000000..9e62f1b1f7a304623bc717d8ac7a20b49cf1866e
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/java/create_order-post_reload.qscript
@@ -0,0 +1,20 @@
+#!/usr/bin/env qore
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusClientCore
+
+QorusClient::initFast();
+
+const WORKFLOW_NAME = "BB-BASICS-SIMPLE-WORKFLOW-JAVA";
+
+const ORDER_DATA = {
+ "staticdata": {
+ "test": "data"
+ }
+};
+
+hash response = qrest.post("workflows/" + WORKFLOW_NAME + "?action=createOrder", ORDER_DATA);
+printf("Response: %N\n", response);
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/BB-BASICS-SIMPLE-WORKFLOW-PYTHON-1.0.qwf.yaml b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/BB-BASICS-SIMPLE-WORKFLOW-PYTHON-1.0.qwf.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..581f8fc6a1a9fd246ed19e1ee635238e456b1bab
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/BB-BASICS-SIMPLE-WORKFLOW-PYTHON-1.0.qwf.yaml
@@ -0,0 +1,14 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-SIMPLE-WORKFLOW-PYTHON
+desc: Simple workflow example in python
+author:
+ - Qore Technologies, s.r.o.
+autostart: 1
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+version: "1.0"
+steps:
+ [
+ "BB_BasicsSimpleWorkflowStepPython:1.0"
+ ]
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/BB_BasicsSimpleWorkflowStepPython-1.0.qstep.py b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/BB_BasicsSimpleWorkflowStepPython-1.0.qstep.py
new file mode 100644
index 0000000000000000000000000000000000000000..e22fe12d5e1501d3dfc8351c602ec45d7d4bc953
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/BB_BasicsSimpleWorkflowStepPython-1.0.qstep.py
@@ -0,0 +1,41 @@
+from wf import QorusNormalStep
+from qore.__root__ import BBM_LogMessage
+
+class BasicsSimpleWorkflowStepPython(QorusNormalStep):
+ def __init__(self):
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ self.class_connections = ClassConnections_BasicsSimpleWorkflowStepPython()
+ ############ GENERATED SECTION END ############
+
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ def primary(self):
+ self.class_connections.primary()
+
+ def validation(self):
+ return StatRetry
+ ############ GENERATED SECTION END ############
+
+####### GENERATED SECTION! DON'T EDIT! ########
+class ClassConnections_BasicsSimpleWorkflowStepPython:
+ def __init__(self):
+ UserApi.startCapturingObjectsFromPython()
+ # map of prefixed class names to class instances
+ self.class_map = {
+ 'BBM_LogMessage': BBM_LogMessage(),
+ }
+ UserApi.stopCapturingObjectsFromPython()
+
+ def callClassWithPrefixMethod(self, prefixed_class, method, *argv):
+ UserApi.logDebug("ClassConnections_BasicsSimpleWorkflowStepPython: callClassWithPrefixMethod: method: %s class: %y", method, prefixed_class)
+ return getattr(self.class_map[prefixed_class], method)(*argv)
+
+ def primary(self, *params):
+ UserApi.logDebug("primary called with data: %y", params)
+ # convert varargs to a single argument if possible
+ if (type(params) is list or type(params) is tuple) and (len(params) == 1):
+ params = params[0]
+ UserApi.logDebug("calling logMessage: %y", params)
+ params = self.callClassWithPrefixMethod("BBM_LogMessage", "logMessage", params)
+ UserApi.logDebug("output from logMessage: %y", params)
+ return params
+############ GENERATED SECTION END ############
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/BB_BasicsSimpleWorkflowStepPython-1.0.qstep.yaml b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/BB_BasicsSimpleWorkflowStepPython-1.0.qstep.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..22efcfa77dd371d4f7c7bd4e6ec286faf6731a19
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/BB_BasicsSimpleWorkflowStepPython-1.0.qstep.yaml
@@ -0,0 +1,37 @@
+# This is a generated file, don't edit!
+type: step
+name: BB_BasicsSimpleWorkflowStepPython
+desc: Simple workflow step example in python
+lang: python
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusNormalStep
+class-name: BasicsSimpleWorkflowStepPython
+version: '1.0'
+classes:
+ - BBM_LogMessage
+steptype: NORMAL
+code: BB_BasicsSimpleWorkflowStepPython-1.0.qstep.py
+class-connections:
+ primary:
+ - class: BBM_LogMessage
+ connector: logMessage
+ trigger: primary
+config-items:
+ - name: log-message-level
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-string
+ value:
+ "BasicsSimpleWorkflowStep was called"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-args
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/create_order-post_reload.qscript b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/create_order-post_reload.qscript
new file mode 100755
index 0000000000000000000000000000000000000000..5614af8b347f656af29f30e7dc7f176cc07754db
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/python/create_order-post_reload.qscript
@@ -0,0 +1,20 @@
+#!/usr/bin/env qore
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusClientCore
+
+QorusClient::initFast();
+
+const WORKFLOW_NAME = "BB-BASICS-SIMPLE-WORKFLOW-PYTHON";
+
+const ORDER_DATA = {
+ "staticdata": {
+ "test": "data"
+ }
+};
+
+hash response = qrest.post("workflows/" + WORKFLOW_NAME + "?action=createOrder", ORDER_DATA);
+printf("Response: %N\n", response);
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/BB-BASICS-SIMPLE-WORKFLOW-1.0.yaml b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/BB-BASICS-SIMPLE-WORKFLOW-1.0.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5cf131f86fec0a0ad663d277ef0da31527168ec9
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/BB-BASICS-SIMPLE-WORKFLOW-1.0.yaml
@@ -0,0 +1,14 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-SIMPLE-WORKFLOW
+desc: "Simple workflow example"
+author:
+ - Qore Technologies, s.r.o.
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+steps:
+ [
+ "BB_BasicsSimpleWorkflowStep:1.0"
+ ]
+version: "1.0"
+autostart: 1
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/BB_BasicsSimpleWorkflowStep-1.0.qstep b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/BB_BasicsSimpleWorkflowStep-1.0.qstep
new file mode 100644
index 0000000000000000000000000000000000000000..5094558d97dc98cee438a0f55dd7f6819fde9ab9
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/BB_BasicsSimpleWorkflowStep-1.0.qstep
@@ -0,0 +1,49 @@
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+class BasicsSimpleWorkflowStep inherits QorusNormalStep {
+ private {
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ ClassConnections_BasicsSimpleWorkflowStep class_connections();
+ ############ GENERATED SECTION END ############
+ }
+
+ ####### GENERATED SECTION! DON'T EDIT! ########
+ primary() {
+ class_connections.primary();
+ }
+
+ string validation() {
+ return OMQ::StatRetry;
+ }
+ ############ GENERATED SECTION END ############
+}
+
+####### GENERATED SECTION! DON'T EDIT! ########
+class ClassConnections_BasicsSimpleWorkflowStep {
+ private {
+ # map of prefixed class names to class instances
+ hash class_map;
+ }
+
+ constructor() {
+ class_map = {
+ "BBM_LogMessage": new BBM_LogMessage(),
+ };
+ }
+
+ auto callClassWithPrefixMethod(string prefixed_class, string method) {
+ UserApi::logDebug("ClassConnections_BasicsSimpleWorkflowStep: callClassWithPrefixMethod: method: %s class: %y", method, prefixed_class);
+ return call_object_method_args(class_map{prefixed_class}, method, argv);
+ }
+
+ auto primary(auto params) {
+ UserApi::logDebug("primary called with data: %y", params);
+
+ UserApi::logDebug("calling logMessage: %y", params);
+ return callClassWithPrefixMethod("BBM_LogMessage", "logMessage", params);
+ }
+}
+############ GENERATED SECTION END ############
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/BB_BasicsSimpleWorkflowStep-1.0.qstep.yaml b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/BB_BasicsSimpleWorkflowStep-1.0.qstep.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..f9a673ce0d5a466a84c7c8b9ab32fe1647986100
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/BB_BasicsSimpleWorkflowStep-1.0.qstep.yaml
@@ -0,0 +1,37 @@
+# This is a generated file, don't edit!
+type: step
+name: BB_BasicsSimpleWorkflowStep
+desc: Simple worklfow step example
+lang: qore
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusNormalStep
+class-name: BasicsSimpleWorkflowStep
+version: '1.0'
+classes:
+ - BBM_LogMessage
+steptype: NORMAL
+code: BB_BasicsSimpleWorkflowStep-1.0.qstep
+class-connections:
+ primary:
+ - class: BBM_LogMessage
+ connector: logMessage
+ trigger: primary
+config-items:
+ - name: log-message-level
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-string
+ value:
+ "BasicsSimpleWorkflowStep was called"
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
+ - name: log-message-args
+ parent:
+ interface-type: class
+ interface-name: BBM_LogMessage
+ interface-version: '1.0'
diff --git a/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/create_order-post_reload.qscript b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/create_order-post_reload.qscript
new file mode 100755
index 0000000000000000000000000000000000000000..e484a9ced7e62aba03a9e7959aa39262ab229a4d
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/03_workflow/qore/create_order-post_reload.qscript
@@ -0,0 +1,20 @@
+#!/usr/bin/env qore
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusClientCore
+
+QorusClient::initFast();
+
+const WORKFLOW_NAME = "BB-BASICS-SIMPLE-WORKFLOW";
+
+const ORDER_DATA = {
+ "staticdata": {
+ "test": "data"
+ }
+};
+
+hash response = qrest.post("workflows/" + WORKFLOW_NAME + "?action=createOrder", ORDER_DATA);
+printf("Response: %N\n", response);
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/04_workflow_serial_steps/README.md b/03_basics_building_blocks/01_exchange_rates_app/04_workflow_serial_steps/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2994ef3f2838fd973e91c1566ebb1e510f31c711
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/04_workflow_serial_steps/README.md
@@ -0,0 +1,71 @@
+# Workflow Serial Steps
+
+## Workflow Definition with Serial Steps
+```yaml
+# This is a generated file, don't edit!
+type: workflow
+name: BASICS-WORKFLOW-SERIAL-STEPS
+desc: "Workflow serial steps example"
+author:
+ - Qore Technologies, s.r.o.
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+steps:
+ [
+ "BasicsWorkflowSerialStep1:1.0",
+ "BasicsWorkflowSerialStep2:1.0"
+ ]
+version: "1.0"
+autostart: 1
+```
+
+In this example; the workflow **BASICS-WORKFLOW-SERIAL-STEPS** 1.0 is created. The step dependencies are linear: *BasicsWorkflowSerialStep1* has no dependencies and therefore is the starting step, *BasicsWorkflowSerialStep2* is dependent only on *BasicsWorkflowSerialStep1* and is the final step in the workflow.
+
+
+## Workflow Autostart Parameter
+
+The workflow "autostart" parameter sets the number of workflow execution instances to be started when the system is started; if the system should ensure that this workflow is generally running, then set this key to a value greater than zero.
+
+If no value is provided for this option, the system will not start the workflow automatically; any workflow execution instances for this workflow must be started manually.
+
+If a non-zero value is provided for this workflow, then the system will attempt to start the workflow at all times if all its dependencies are met, and it is not disabled. Additionally, if the workflow cannot be started for any reason (for example, due to an error in the [onetimeinit](https://qoretechnologies.com/manual/qorus/latest/qorus/designimplworkflows.html#onetimeinit) function or a dependency error), an ongoing system alert will be raised, which is only cleared when the workflow is successfully started (or the autostart parameter is set to zero).
+
+## Workflows and Order Data
+
+Because a running workflow execution instance can be working on several different orders at once in different threads, accessing workflow data is performed through API calls in order to guarantee that the workflow's program code accesses only the correct data for the current order being processed at all times.
+
+Accessing and processing data is done using the Qorus API as outlined in this section; these APIs set up the data context for each thread so that the correct data is accessed.
+
+### Workflow Static Order Data
+
+Static data represents the workflow order data being processed. Workflow static order data cannot be updated or deleted by the Qorus workflow API; it is read-only data representing the order data to be processed or fulfilled by the workflow.
+
+API support:
+
+[getStaticData()](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1Workflow_1_1WorkflowApi.html#a684678e62bafdbd00f9d5ffbaa13ef90)
+
+In the first step of the example static data are printed into a log file as a hash using the method above.
+
+### Workflow Dynamic Data
+
+Dynamic data is associated with the workflow order data instance being processed, but it can be updated and is persistent. Any changes made to dynamic data will be committed to the database before the update method returns, therefore any changes will be available in the future, even in the case of errors and later recovery processing.
+
+Dynamic data is appropriate for storing identifiers and references generated during order processing that are needed in subsequent steps, for example.
+
+API support:
+
+* [getDynamicData()](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1Workflow_1_1WorkflowApi.html#a43e88d6e61a5b069b4c21f3d404a7653)
+* [deleteDynamicDataKey()](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1Workflow_1_1WorkflowApi.html#a4492e6d697f7644a47d7d2c889f2e188)
+* [updateDynamicData()](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1Workflow_1_1WorkflowApi.html#a707e6a020e3b81876977a6ac5ed9d5f4)
+* [DynamicDataHelper class](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1Workflow_1_1DynamicDataHelper.html)
+
+In the second step of the example static data are also printed into a log file as in the first step and the second step prints dynamic data, which were changed by the first step.
+
+---
+
+
+
+## Creating User Connection to Exchange Rates API
+
+New user connection can be added to Qorus by deploying the following YAML:
+
+```yaml
+type: connection
+name: exchange-rates-api
+desc: Exchange rates API
+url: rests://api.exchangeratesapi.io,
+options:
+ timeout: 60000
+ connect_timeout: 30000
+```
+
+Information on how to implement Qorus Objects using YAML format can be found [here](https://qoretechnologies.com/manual/qorus/latest/qorus/implementingqorusobjectsusingyaml.html).
+
+The YAML schema used for validation of the connection definition can be downloaded [here](https://qoretechnologies.com/manual/qorus/latest/qorus/connection_schema.yaml).
+
+### Schemes
+
+
+
+
+
URI Scheme
+
Description
+
+
+
"rest"
+
non-encrypted REST HTTP connections; default port 80 if not present in the URL
+
+
+
"rests"
+
encrypted REST HTTPS connections; default port 443 if not present in the URL
+
+
+
+
+### Connection Options
+
+
+
+
+
Option
+
Description
+
+
+
"connect_timeout"
+
connection timeout to use in milliseconds
+
+
+
"content_encoding"
+
this sets the send encoding (if the "send_encoding" option is not set) and the requested response encoding; for possible values, see EncodingSupport
+
+
+
"data"
+
see DataSerializationOptions for possible values; the default is "auto"; note that it's recommended to use "yaml" when talking to Qorus
+
+
+
"http_version"
+
HTTP version to use ("1.0" or "1.1", defaults to "1.1")
+
+
+
"max_redirects"
+
maximum redirects to support
+
+
+
"proxy"
+
proxy URL to use
+
+
+
"send_encoding"
+
a send data encoding option or the value "auto" which means to use automatic encoding; if not present defaults to no content-encoding on sent message bodies
diff --git a/03_basics_building_blocks/01_exchange_rates_app/08_exchange_rates_datasource_connection/omquser.qconn.yaml b/03_basics_building_blocks/01_exchange_rates_app/08_exchange_rates_datasource_connection/omquser.qconn.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..bc9894ace685ff78068be4f6d73938e781412c2a
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/08_exchange_rates_datasource_connection/omquser.qconn.yaml
@@ -0,0 +1,4 @@
+type: connection
+name: omquser
+desc: omquser datasource connection,
+url: db://pgsql:omquser/omquser@omquser
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/README.md b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..e5ab87f16058af9caff08375006e641e617f9076
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/README.md
@@ -0,0 +1,81 @@
+# Exchange Currency Workflow
+
+## Workflow Definition
+
+```yaml
+# This is a generated file, don't edit!
+type: workflow
+name: BASICS-EXCHANGE-CURRENCY-WORKFLOW
+desc: "Exchange currency workflow"
+author:
+ - Qore Technologies, s.r.o.
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+steps:
+ [
+ "BasicsExchangeRatesInsertToOrdersTable:1.0"
+ ]
+version: "1.0"
+```
+
+The workflow consists of one step, which inserts data into the **"exchange_orders"** table of the **"omquser"** datasource.
+
+## Workflow Definition Keylist Key
+
+The workflow definition option keylist defines one or more "order keys" that can be used to quickly look up workflow order data instances. In this example the **keylist** parameter will be used to prevent creating workflow instances with the same data.
+
+## Step definition
+
+Metadata:
+```yaml
+# This is a generated file, don't edit!
+type: step
+name: BasicsExchangeRatesInsertToOrdersTable
+desc: "Inserts orders from static data into orders table"
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusNormalStep
+class-name: BasicsExchangeRatesInsertToOrdersTable
+lang: qore
+version: "1.0"
+steptype: NORMAL
+code: BasicsExchangeRatesInsertToOrdersTable-1.0.qstep
+```
+
+Code:
+```php
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+class BasicsExchangeRatesInsertToOrdersTable inherits QorusNormalStep {
+ primary() {
+ DatasourcePool omquser_ds = getDatasourcePool("omquser");
+ on_success omquser_ds.commit();
+ on_error omquser_ds.rollback();
+
+ SqlUtil::AbstractTable orders_table = getSqlTable(omquser_ds, "exchange_orders");
+
+ list> orders = getStaticData("orders");
+ foreach hash row in (orders) {
+ logInfo("inserting row: %N", row);
+ orders_table.insert(row);
+ }
+ }
+}
+
+```
+
+This step inserts data received in *orders* list of **static data** of a workflow order into the **exchange_orders** table of the **omquser** database. Each element of the *orders* list is a hash containing one row of the table.
+
+To obtain the datasource [**getDatasourcePool()**](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1UserApi.html#a80461179fe81c80d10c46cf0b47ad1f0) method is used.
+
+---
+
+
[Next: Exchange currency workflow with mapper →](../10_exchange_currency_workflow_with_mapper)
+
+
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/basics-exchange-rates-insert-to-orders-table.qfsm.yaml b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/basics-exchange-rates-insert-to-orders-table.qfsm.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b7463271c9a849f8ec45470b14d7a64abe509f21
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/basics-exchange-rates-insert-to-orders-table.qfsm.yaml
@@ -0,0 +1,82 @@
+# This is a generated file, don't edit!
+type: fsm
+name: basics-exchange-rates-insert-to-orders-table
+desc: Inserts static data to the `exchange_orders` table
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+states:
+ '1':
+ position:
+ x: 88
+ 'y': 45
+ initial: true
+ name: Loop over static data
+ desc: ''
+ type: block
+ id: s8lzBUiGY
+ execution_order: 1
+ block-config:
+ loop:
+ type: string
+ value: '$qore-expr:{$qore-expr-value:{$static:orders}.pairIterator()}'
+ block-type: foreach
+ states:
+ '1':
+ position:
+ x: 55
+ 'y': 31
+ initial: true
+ name: Insert Row
+ desc: ''
+ type: state
+ id: CYU66uSIC
+ action:
+ type: connector
+ value:
+ class: BBM_DataProviderRecordCreate
+ connector: DataProvider Record Create From Config
+ execution_order: 1
+ config-items:
+ - name: dataprovider-create-provider-path
+ value:
+ "datasource/omquser/exchange_orders"
+ parent:
+ interface-type: class
+ interface-name: BBM_DataProviderRecordCreate
+ interface-version: '1.0'
+ - name: dataprovider-create-input
+ value:
+ "$qore-expr:{$qore-expr-value:{$local:input.value} + {\"id\": $local:{input.key}.toInt()}}"
+ parent:
+ interface-type: class
+ interface-name: BBM_DataProviderRecordCreate
+ interface-version: '1.0'
+ value_true_type: string
+ - name: dataprovider-create-upsert
+ parent:
+ interface-type: class
+ interface-name: BBM_DataProviderRecordCreate
+ interface-version: '1.0'
+ - name: dataprovider-create-mapper
+ parent:
+ interface-type: class
+ interface-name: BBM_DataProviderRecordCreate
+ interface-version: '1.0'
+ - name: dataprovider-create-options
+ parent:
+ interface-type: class
+ interface-name: BBM_DataProviderRecordCreate
+ interface-version: '1.0'
+ - name: dataprovider-create-duplicate-handling
+ parent:
+ interface-type: class
+ interface-name: BBM_DataProviderRecordCreate
+ interface-version: '1.0'
+ allowed_values:
+ - "SUCCESS"
+ - "DUPLICATE"
+ - name: dataprovider-create-output-data
+ parent:
+ interface-type: class
+ interface-name: BBM_DataProviderRecordCreate
+ interface-version: '1.0'
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-JAVA-1.0.yaml b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-JAVA-1.0.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c5f3fa6a7d51a4676463676d899c100acb2750c7
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-JAVA-1.0.yaml
@@ -0,0 +1,13 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-JAVA
+desc: "Exchange currency workflow"
+author:
+ - Qore Technologies, s.r.o.
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+steps:
+ [
+ "BB_BasicsExchangeRatesInsertToOrdersTableJava:1.0"
+ ]
+version: "1.0"
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/BB_BasicsExchangeRatesInsertToOrdersTableJava-1.0.qstep.yaml b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/BB_BasicsExchangeRatesInsertToOrdersTableJava-1.0.qstep.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b31aee8d5995d7141c1d38ed0dc900ce5ca2460a
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/BB_BasicsExchangeRatesInsertToOrdersTableJava-1.0.qstep.yaml
@@ -0,0 +1,23 @@
+# This is a generated file, don't edit!
+type: step
+name: BB_BasicsExchangeRatesInsertToOrdersTableJava
+desc: Inserts orders from static data into orders table
+lang: java
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusNormalStep
+class-name: BasicsExchangeRatesInsertToOrdersTableJava
+version: '1.0'
+fsm:
+ [
+ {
+ "name": "basics-exchange-rates-insert-to-orders-table",
+ "triggers": [
+ {
+ "method": "primary"
+ }
+ ]
+ }
+ ]
+steptype: NORMAL
+code: bb_basicsexchangeratesinserttoorderstablejava_1_0_step/BasicsExchangeRatesInsertToOrdersTableJava.java
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/bb_basicsexchangeratesinserttoorderstablejava_1_0_step/BasicsExchangeRatesInsertToOrdersTableJava.java b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/bb_basicsexchangeratesinserttoorderstablejava_1_0_step/BasicsExchangeRatesInsertToOrdersTableJava.java
new file mode 100644
index 0000000000000000000000000000000000000000..c59ebc9e7a3bbbbd15d50bc43730736534c62429
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/bb_basicsexchangeratesinserttoorderstablejava_1_0_step/BasicsExchangeRatesInsertToOrdersTableJava.java
@@ -0,0 +1,17 @@
+import qore.OMQ.*;
+import qore.OMQ.UserApi.*;
+import qore.OMQ.UserApi.Workflow.*;
+
+import qore.SqlUtil.AbstractTable;
+import qore.Qore.SQL.AbstractDatasource;
+
+import org.qore.jni.Hash;
+
+class BasicsExchangeRatesInsertToOrdersTableJava extends QorusNormalStep {
+ public BasicsExchangeRatesInsertToOrdersTableJava() throws Throwable {
+ super();
+ }
+
+ public void primary() throws Throwable {
+ }
+}
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/create_order-post_reload.qscript b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/create_order-post_reload.qscript
new file mode 100755
index 0000000000000000000000000000000000000000..4005051cfa62f6b2322ad0fc39f49d2246a68686
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/java/create_order-post_reload.qscript
@@ -0,0 +1,29 @@
+#!/usr/bin/env qore
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusClientCore
+
+QorusClient::initFast();
+
+const WORKFLOW_NAME = "BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-JAVA";
+
+const ORDER_DATA = {
+ "staticdata": {
+ "orders": {
+ "1": {
+ "currency_to_buy": "czk", "currency_to_sell": "eur",
+ "creation_date": now(), "status": "new", "amount": 1500
+ },
+ "2": {
+ "currency_to_buy": "eur", "currency_to_sell": "czk",
+ "creation_date": now(), "status": "new", "amount": 60
+ }
+ }
+ }
+};
+
+hash response = qrest.post("workflows/" + WORKFLOW_NAME + "?action=createOrder", ORDER_DATA);
+printf("Response: %N\n", response);
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-PYTHON-1.0.qwf.yaml b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-PYTHON-1.0.qwf.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c235cd092de9c5e2fd54889057147289451b872d
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-PYTHON-1.0.qwf.yaml
@@ -0,0 +1,13 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-PYTHON
+desc: '"Exchange currency workflow"'
+version: "1.0"
+author:
+ - Qore Technologies, s.r.o.
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+steps:
+ [
+ "BB_BasicsExchangeRatesInsertToOrdersTablePython:1.0"
+ ]
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/BB_BasicsExchangeRatesInsertToOrdersTablePython-1.0.qstep.py b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/BB_BasicsExchangeRatesInsertToOrdersTablePython-1.0.qstep.py
new file mode 100644
index 0000000000000000000000000000000000000000..a02394bcaf02ca8bcf87d00c5cec66f5313a7122
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/BB_BasicsExchangeRatesInsertToOrdersTablePython-1.0.qstep.py
@@ -0,0 +1,5 @@
+from wf import QorusNormalStep
+
+class BasicsExchangeRatesInsertToOrdersTablePython(QorusNormalStep):
+ def primary(self):
+ pass
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/BB_BasicsExchangeRatesInsertToOrdersTablePython-1.0.qstep.yaml b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/BB_BasicsExchangeRatesInsertToOrdersTablePython-1.0.qstep.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5ff57ed70012efc42bc638702569816133485ebf
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/BB_BasicsExchangeRatesInsertToOrdersTablePython-1.0.qstep.yaml
@@ -0,0 +1,23 @@
+# This is a generated file, don't edit!
+type: step
+name: BB_BasicsExchangeRatesInsertToOrdersTablePython
+desc: Inserts orders from static data into orders table
+lang: python
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusNormalStep
+class-name: BasicsExchangeRatesInsertToOrdersTablePython
+version: '1.0'
+fsm:
+ [
+ {
+ "name": "basics-exchange-rates-insert-to-orders-table",
+ "triggers": [
+ {
+ "method": "primary"
+ }
+ ]
+ }
+ ]
+steptype: NORMAL
+code: BB_BasicsExchangeRatesInsertToOrdersTablePython-1.0.qstep.py
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/create_order-post_reload.qscript b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/create_order-post_reload.qscript
new file mode 100755
index 0000000000000000000000000000000000000000..0da6583ed22a3ddfbf045b331f6cce82d2a0b6c1
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/python/create_order-post_reload.qscript
@@ -0,0 +1,29 @@
+#!/usr/bin/env qore
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusClientCore
+
+QorusClient::initFast();
+
+const WORKFLOW_NAME = "BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-PYTHON";
+
+const ORDER_DATA = {
+ "staticdata": {
+ "orders": {
+ "3": {
+ "currency_to_buy": "czk", "currency_to_sell": "eur",
+ "creation_date": now(), "status": "new", "amount": 1500
+ },
+ "4": {
+ "currency_to_buy": "eur", "currency_to_sell": "czk",
+ "creation_date": now(), "status": "new", "amount": 60
+ }
+ }
+ }
+};
+
+hash response = qrest.post("workflows/" + WORKFLOW_NAME + "?action=createOrder", ORDER_DATA);
+printf("Response: %N\n", response);
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-1.0.yaml b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-1.0.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..4ecd84e9acb570486ac52fc9aa41821888812cfc
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-1.0.yaml
@@ -0,0 +1,13 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW
+desc: "Exchange currency workflow"
+author:
+ - Qore Technologies, s.r.o.
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+steps:
+ [
+ "BB_BasicsExchangeRatesInsertToOrdersTable:1.0"
+ ]
+version: "1.0"
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/BB_BasicsExchangeRatesInsertToOrdersTable-1.0.qstep b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/BB_BasicsExchangeRatesInsertToOrdersTable-1.0.qstep
new file mode 100644
index 0000000000000000000000000000000000000000..f3c5cd94149776bf09774942919b1bf44503eda5
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/BB_BasicsExchangeRatesInsertToOrdersTable-1.0.qstep
@@ -0,0 +1,9 @@
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+class BasicsExchangeRatesInsertToOrdersTable inherits QorusNormalStep {
+ primary() {
+ }
+}
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/BB_BasicsExchangeRatesInsertToOrdersTable-1.0.qstep.yaml b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/BB_BasicsExchangeRatesInsertToOrdersTable-1.0.qstep.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..714b8538da828230c1eb3c47771d173443818dde
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/BB_BasicsExchangeRatesInsertToOrdersTable-1.0.qstep.yaml
@@ -0,0 +1,23 @@
+# This is a generated file, don't edit!
+type: step
+name: BB_BasicsExchangeRatesInsertToOrdersTable
+desc: Inserts orders from static data into orders table
+lang: qore
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusNormalStep
+class-name: BasicsExchangeRatesInsertToOrdersTable
+version: '1.0'
+fsm:
+ [
+ {
+ "name": "basics-exchange-rates-insert-to-orders-table",
+ "triggers": [
+ {
+ "method": "primary"
+ }
+ ]
+ }
+ ]
+steptype: NORMAL
+code: BB_BasicsExchangeRatesInsertToOrdersTable-1.0.qstep
diff --git a/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/create_order-post_reload.qscript b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/create_order-post_reload.qscript
new file mode 100755
index 0000000000000000000000000000000000000000..eca683da679d528b19e2656cabcb04e9929ca8cc
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/09_exchange_currency_workflow/qore/create_order-post_reload.qscript
@@ -0,0 +1,61 @@
+#!/usr/bin/env qore
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusClientCore
+
+QorusClient::initFast();
+
+const WORKFLOW_NAME = "BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW";
+
+const ORDER_DATA = {
+ "staticdata": {
+ "orders": {
+ "41": {
+ "currency_to_buy": "czk", "currency_to_sell": "eur",
+ "creation_date": now(), "status": "new", "amount": 1500
+ },
+ "42": {
+ "currency_to_buy": "eur", "currency_to_sell": "czk",
+ "creation_date": now(), "status": "new", "amount": 60
+ },
+ "43": {
+ "currency_to_buy": "czk", "currency_to_sell": "eur",
+ "creation_date": now(), "status": "new", "amount": 1500
+ },
+ "44": {
+ "currency_to_buy": "eur", "currency_to_sell": "czk",
+ "creation_date": now(), "status": "new", "amount": 60
+ },
+ "45": {
+ "currency_to_buy": "czk", "currency_to_sell": "eur",
+ "creation_date": now(), "status": "new", "amount": 1500
+ },
+ "46": {
+ "currency_to_buy": "eur", "currency_to_sell": "czk",
+ "creation_date": now(), "status": "new", "amount": 60
+ },
+ "47": {
+ "currency_to_buy": "czk", "currency_to_sell": "eur",
+ "creation_date": now(), "status": "new", "amount": 1500
+ },
+ "48": {
+ "currency_to_buy": "eur", "currency_to_sell": "czk",
+ "creation_date": now(), "status": "new", "amount": 60
+ },
+ "49": {
+ "currency_to_buy": "czk", "currency_to_sell": "eur",
+ "creation_date": now(), "status": "new", "amount": 1500
+ },
+ "50": {
+ "currency_to_buy": "eur", "currency_to_sell": "czk",
+ "creation_date": now(), "status": "new", "amount": 60
+ }
+ }
+ }
+};
+
+hash response = qrest.post("workflows/" + WORKFLOW_NAME + "?action=createOrder", ORDER_DATA);
+printf("Response: %N\n", response);
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/10_exchange_currency_workflow_with_mapper/README.md b/03_basics_building_blocks/01_exchange_rates_app/10_exchange_currency_workflow_with_mapper/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..497ff6fdc51fbafd9dd41cbf6e3155ed47898abd
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/10_exchange_currency_workflow_with_mapper/README.md
@@ -0,0 +1,136 @@
+# Exchange Currency Workflow with Mapper
+
+## Mapper Introduction
+
+Qorus Mappers are high-level integration objects that providing a framework for runtime data transformations in Qorus interfaces.
+
+Mapper configurations are loaded into the database with oload and also must be explicitly associated with workflows, services, and jobs to be used in interface code. This way, the system knows (and can display in the system UI) which mappers are associated with which interfaces.
+
+When mappers are used at runtime, every mapper configuration generates a class descended from Mapper, and the basic configuration of Qorus mapper objects is based on the implementation of this base class (it's also possible to get an AbstractIterator object based on a mapper, but this is also based on the Mapper object generated by the mapper).
+
+Mappers have the following high-level attributes:
+
++ *mapperid*: a unique ID for the mapper
++ *name*: the name of the mapper
++ *version*: the version of the mapper; multiple versions of a mapper can be present at any one time in the system, and interfaces must specify the mapper version that they use
++ *patch*: a patchlevel attribute that does not affect version compatibility
++ *desc*: a description of the mapper
++ *author*: the author of the mapper
++ *parse_options*: local parse options for the mapper
++ *type*: the type of mapper
++ *options*: a hash of mapper options, some mappers could require certain options
++ *fields*: a hash of mappings where the keys are output fields and the values describe how the output should be generated
++ *library objects*: a list of objects (functions, classes, and constants) that are imported into the mapper container Program
+
+Mappers are exposed through the REST API at the following URI path: @/api/mappers.
+
+## Implementing Mappers
+
+Mappers are developed in text files, loaded into the system schema with oload and participate in releases built with make-release.
+
+Each mapper has its own sandboxed Program container that contains the mapping logic and any library objects imported into it.
+
+Note that workflows, services, and jobs must declare mappers to be able to access them. The following api call should be used to acquire a mapper in your interface code:
+
+[getMapper()](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1UserApi.html#aa661cce9be732bf43e001669f2ff75c9)
+The return type of the above method will be compatible with Mapper::Mapper, but the exact type of object depends on the mapper type.
+
+Mappers can be created using the Qorus Developer Tools extension, which has strong support for mapper creation of any complexity. The IDE supports user-defined types, dataprovider API allowing to automatically introspect types from user connections, datasource connections and swagger schemas. Users are able to create complex mapping, where fields can be placed in a hierarchy.
+
+
+
+### Mapper Files
+
+Mapper files defining new system mappers take the filename suffix: ".qmapper.yaml".
+
+As with function, class, constant objects, services, jobs, and value maps, mappers are defined by adding metadata tags in the YAML format. The tags are parsed by `oload`, which will create the mappers in the Qorus database according to the information in the file.
+
+### Exchange Currency Orders Mapper
+
+```yaml
+# This is a generated file, don't edit!
+type: mapper
+name: bb-basics-exchange-currency-orders
+desc: "Exchange Currency Orders Mapper"
+author:
+ - Qore Technologies, s.r.o.
+fields:
+ amount:
+ name: amount
+ creation_date:
+ context: '$timestamp:{YYYY-MM-DD HH:mm:SS.xx Z}'
+ currency_to_buy:
+ name: currency_to_buy
+ currency_to_sell:
+ name: currency_to_sell
+ id:
+ name: id
+ status:
+ constant: new
+```
+
+The full mapper example can be found in the same directory. It also defines options, where user defined input is described.
+
+In this example the mapper uses **"omquser"** as a datasource and **"exchange_rates"** table as an output table. The id field is filled from the same field of input data, but also it can be set using some existing sequence from the database.
+
+Information on how to implement Qorus Objects using YAML format can be found [here](https://qoretechnologies.com/manual/qorus/latest/qorus/implementingqorusobjectsusingyaml.html).
+
+The YAML schema used for validation of the workflow metadata can be downloaded [here](https://qoretechnologies.com/manual/qorus/latest/qorus/workflow_schema.yaml).
+
+
+## Workflow Step Definition
+
+Metadata:
+```yaml
+# This is a generated file, don't edit!
+type: step
+name: BasicsExchangeRatesInsertToOrdersTableJava
+desc: "Inserts orders from static data into orders table"
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusNormalStep
+class-name: BasicsExchangeRatesInsertToOrdersTableJava
+lang: java
+version: "1.0"
+steptype: NORMAL
+code: BasicsExchangeRatesInsertToOrdersTableJava.java
+```
+
+Code:
+```java
+import qore.OMQ.UserApi.*;
+import qore.OMQ.UserApi.Workflow.*;
+import qore.Mapper.Mapper;
+
+import org.qore.jni.Hash;
+
+class BasicsExchangeRatesInsertToOrdersTableJava extends QorusNormalStep {
+ private final String mapperName = "bb-basics-exchange-currency-orders";
+
+ @SuppressWarnings("unchecked")
+ public void primary() throws Throwable {
+ Mapper mapper = getMapper(mapperName);
+ Hash orders = (Hash)getStaticData("orders");
+
+ try {
+ mapper.mapAll(orders);
+ } catch (Exception e) {
+ mapper.rollback();
+ throw e;
+ }
+
+ mapper.commit();
+ }
+}
+```
+
+Having the mapper described above it's not needed to use **getDatasourcePool()** method and insert data using **SqlUtil::AbstractTable** object. Instead of that, **getMapper()** method is used. Then **mapAll()** method is called with all orders from static data as an argument.
+
+---
+
+
Run the step function again immediately. If the step is an asynchronous step with queue data with a OMQ::QS_Waiting status, the queue data will be deleted immediately before the step function is run again
For asynchronous steps only, do not run the step function and keep the "ASYNC-WAITING" status. For non-asynchronous steps, raises an error and the return value is treated like OMQ::StatError
+
+
+
any other status
+
an error is raised and the return value is treated like OMQ::StatError
+
+
+
+
+## Workflow Validation Function Example
+
+Step metadata:
+
+```yaml
+# This is a generated file, don't edit!
+type: step
+name: BasicsExchangeRatesInsertToOrdersTable
+desc: "Inserts orders from static data into orders table"
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusNormalStep
+class-name: BasicsExchangeRatesInsertToOrdersTable
+lang: qore
+version: "1.0"
+steptype: NORMAL
+code: BasicsExchangeRatesInsertToOrdersTable-1.0.qstep
+```
+
+Step code:
+```
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+class BasicsExchangeRatesInsertToOrdersTable inherits QorusNormalStep {
+ primary() {
+ InboundTableMapper mapper = getMapper("basics-exchange-currency-orders");
+ on_error mapper.rollback();
+ on_success mapper.commit();
+
+ list> orders = getStaticData("orders");
+ log(LL_DEBUG_1, "inserting data: %N", orders);
+ mapper.queueData(orders);
+ }
+
+ string validation() {
+ DatasourcePool ds = getDatasourcePool("omquser");
+ on_success ds.commit();
+ on_error ds.rollback();
+
+ SqlUtil::AbstractTable orders_table = getSqlTable(ds, "exchange_orders");
+
+ list> orders = getStaticData("orders");
+ foreach hash order in (\orders) {
+ hash sh = {"status": "new", "id": order.id};
+ if (orders_table.findSingle(sh)) {
+ return OMQ::StatComplete;
+ }
+ }
+ return OMQ::StatRetry;
+ }
+}
+```
+
+This validation function is for the **BasicsExchangeRatesInsertToOrdersTable** step. This function checks whether any order with the same id is not already in the database. If yes, then the step will be skipped. In this example the *"status"* field used to filter only orders with *new* status.
+
+Usually, to prevent one order instance to operate with data of the other one a new field called *wfiid* is introduced in database. Basically, when a workflow order is about to operate with some data in the database it simply fills this field with its own workflow instance id (using [**getWfiid()**](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1Workflow_1_1WorkflowApi.html#a8d857a58469c88a3a451dcdf23c53b3a) method).
+
+---
+
+
+
+
[← Previous: Process orders job](../13_process_orders_job)
+
[Next: Workflow error function →](../15_workflow_error_function)
+
+
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-JAVA-1.0.yaml b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-JAVA-1.0.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..8363f9d97d723daace74715f19429ae41abad66f
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-JAVA-1.0.yaml
@@ -0,0 +1,13 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-JAVA
+desc: "Exchange currency workflow"
+author:
+ - Qore Technologies, s.r.o.
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+steps:
+ [
+ "BB_BasicsExchangeRatesInsertToOrdersTableJava:1.1"
+ ]
+version: "1.0"
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/BB_BasicsExchangeRatesInsertToOrdersTableJava-1.1.qstep.yaml b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/BB_BasicsExchangeRatesInsertToOrdersTableJava-1.1.qstep.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6ce4460c47f0e1af11d69f05eaa8c760d4887181
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/BB_BasicsExchangeRatesInsertToOrdersTableJava-1.1.qstep.yaml
@@ -0,0 +1,12 @@
+# This is a generated file, don't edit!
+type: step
+name: BB_BasicsExchangeRatesInsertToOrdersTableJava
+desc: "Inserts orders from static data into orders table"
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusNormalStep
+class-name: BasicsExchangeRatesInsertToOrdersTableJava
+lang: java
+version: "1.1"
+steptype: NORMAL
+code: BasicsExchangeRatesInsertToOrdersTableJava.java
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/BasicsExchangeRatesInsertToOrdersTableJava.java b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/BasicsExchangeRatesInsertToOrdersTableJava.java
new file mode 100644
index 0000000000000000000000000000000000000000..dee2272731657ea23fa466b573b3e6a3bf3bc008
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/BasicsExchangeRatesInsertToOrdersTableJava.java
@@ -0,0 +1,57 @@
+import qore.OMQ.*;
+import qore.OMQ.UserApi.*;
+import qore.OMQ.UserApi.Workflow.*;
+
+import qore.Mapper.Mapper;
+import qore.Qore.SQL.AbstractDatasource;
+import qore.SqlUtil.AbstractTable;
+
+import org.qore.jni.Hash;
+
+class BasicsExchangeRatesInsertToOrdersTableJava extends QorusNormalStep {
+ private final String mapperName = "basics-exchange-currency-orders";
+
+ public BasicsExchangeRatesInsertToOrdersTableJava() throws Throwable {
+ super();
+ }
+
+ @SuppressWarnings("unchecked")
+ public String validation() throws Throwable {
+ AbstractDatasource ds = getDatasourcePool("omquser");
+ AbstractTable table = getSqlTable(ds, "exchange_orders");
+
+ Hash orders = (Hash)getStaticData("orders");
+ try {
+ for (String key : orders.keySet()) {
+ Hash rowMap = orders.getHash(key);
+ rowMap.put("status", "new");
+ rowMap.put("id", Integer.parseInt(key));
+ Hash row = table.selectRow(rowMap);
+ if (!row.isEmpty()) {
+ return qore.OMQ.$Constants.StatComplete;
+ }
+ }
+ } catch (Exception e) {
+ ds.rollback();
+ throw e;
+ }
+
+ ds.commit();
+ return qore.OMQ.$Constants.StatRetry;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void primary() throws Throwable {
+ Mapper mapper = getMapper(mapperName);
+ Hash orders = (Hash)getStaticData("orders");
+
+ try {
+ mapper.mapAll(orders);
+ } catch (Exception e) {
+ mapper.rollback();
+ throw e;
+ }
+
+ mapper.commit();
+ }
+}
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/create_order-post_reload.qscript b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/create_order-post_reload.qscript
new file mode 100755
index 0000000000000000000000000000000000000000..a057ebf2ff2e86a278aba54db236a57fe01dc784
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/java/create_order-post_reload.qscript
@@ -0,0 +1,25 @@
+#!/usr/bin/env qore
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusClientCore
+
+QorusClient::initFast();
+
+const WORKFLOW_NAME = "BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-MAPPER-JAVA";
+
+const ORDER_DATA = {
+ "staticdata": {
+ "orders": {
+ "id": (20, 21),
+ "currency_to_buy": ("usd", "czk"),
+ "currency_to_sell": ("eur", "usd"),
+ "amount": (100, 600),
+ }
+ }
+};
+
+hash response = qrest.post("workflows/" + WORKFLOW_NAME + "?action=createOrder", ORDER_DATA);
+printf("Response: %N\n", response);
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-PYTHON-1.0.qwf.yaml b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-PYTHON-1.0.qwf.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c71d10946adce99a61e039fb4bb207556af36418
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-PYTHON-1.0.qwf.yaml
@@ -0,0 +1,13 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-PYTHON
+desc: '"Exchange currency workflow"'
+version: "1.0"
+author:
+ - Qore Technologies, s.r.o.
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+steps:
+ [
+ "BB_BasicsExchangeRatesInsertToOrdersTablePython:1.1"
+ ]
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep.py b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep.py
new file mode 100644
index 0000000000000000000000000000000000000000..976f211ab0a8572c07c73772f465a5312e75ba36
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep.py
@@ -0,0 +1,28 @@
+from wf import QorusNormalStep
+
+class BB_BasicsExchangeRatesInsertToOrdersTableMapper(QorusNormalStep):
+ def primary(self):
+ mapper = self.getMapper("basics-exchange-currency-orders")
+
+ try:
+ orders = self.getStaticData("orders")
+ UserApi.logInfo("inserting data: %N", orders)
+ mapper.mapAll(orders)
+ except:
+ mapper.rollback()
+ raise
+
+ mapper.commit()
+
+ def validation(self):
+ omquser_ds = self.getDatasourcePool("omquser")
+
+ orders_table = self.getSqlTable(omquser_ds, "exchange_orders")
+
+ orders = self.getStaticData("orders")
+ for order in orders:
+ sh = {"status": "new", "id": order.id}
+ if orders_table.findSingle(sh):
+ return OMQ.StatComplete
+
+ return OMQ.StatRetry
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep.yaml b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..eac34ca7d3ebac63ef0fc1dd1bad7e909c9ff572
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep.yaml
@@ -0,0 +1,12 @@
+# This is a generated file, don't edit!
+type: step
+name: BB_BasicsExchangeRatesInsertToOrdersTableMapper
+desc: Inserts orders from static data into orders table
+lang: python
+base-class-name: QorusNormalStep
+class-name: BB_BasicsExchangeRatesInsertToOrdersTableMapper
+version: "1.1"
+author:
+ - Qore Technologies, s.r.o.
+steptype: NORMAL
+code: BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep.py
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/create_order-post_reload.qscript b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/create_order-post_reload.qscript
new file mode 100755
index 0000000000000000000000000000000000000000..c9a4592aeeb2ff44c89515a2c87fc260b70b86c8
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/python/create_order-post_reload.qscript
@@ -0,0 +1,25 @@
+#!/usr/bin/env qore
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusClientCore
+
+QorusClient::initFast();
+
+const WORKFLOW_NAME = "BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-MAPPER-PYTHON";
+
+const ORDER_DATA = {
+ "staticdata": {
+ "orders": {
+ "id": (22, 23),
+ "currency_to_buy": ("usd", "czk"),
+ "currency_to_sell": ("eur", "usd"),
+ "amount": (100, 600),
+ }
+ }
+};
+
+hash response = qrest.post("workflows/" + WORKFLOW_NAME + "?action=createOrder", ORDER_DATA);
+printf("Response: %N\n", response);
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-1.0.yaml b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-1.0.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a5010fd683f877d885174482542f71c39c94c345
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-1.0.yaml
@@ -0,0 +1,13 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW
+desc: "Exchange currency workflow"
+author:
+ - Qore Technologies, s.r.o.
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+steps:
+ [
+ "BB_BasicsExchangeRatesInsertToOrdersTable:1.1"
+ ]
+version: "1.0"
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep
new file mode 100644
index 0000000000000000000000000000000000000000..8b34c31dd4f8f413d3f848712d4854992a5bd745
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep
@@ -0,0 +1,33 @@
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+class BB_BasicsExchangeRatesInsertToOrdersTableMapper inherits QorusNormalStep {
+ primary() {
+ auto mapper = getMapper("basics-exchange-currency-orders");
+ on_error mapper.rollback();
+ on_success mapper.commit();
+
+ hash orders = WorkflowApi::getStaticData("orders");
+ logDebug("inserting data: %N", orders);
+ mapper.mapAll(orders);
+ }
+
+ string validation() {
+ DatasourcePool ds = getDatasourcePool("omquser");
+ on_success ds.commit();
+ on_error ds.rollback();
+
+ SqlUtil::AbstractTable orders_table = getSqlTable(ds, "exchange_orders");
+
+ list> orders = WorkflowApi::getStaticData("orders");
+ foreach hash order in (\orders) {
+ hash sh = {"status": "new", "id": order.id};
+ if (orders_table.findSingle(sh)) {
+ return OMQ::StatComplete;
+ }
+ }
+ return OMQ::StatRetry;
+ }
+}
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep.yaml b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..03d18d98a4ee5aed59650d108dbf0858061e726f
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep.yaml
@@ -0,0 +1,12 @@
+# This is a generated file, don't edit!
+type: step
+name: BB_BasicsExchangeRatesInsertToOrdersTableMapper
+desc: Inserts orders from static data into orders table
+lang: qore
+author:
+ - Qore Technologies, s.r.o.
+base-class-name: QorusNormalStep
+class-name: BB_BasicsExchangeRatesInsertToOrdersTableMapper
+version: "1.1"
+steptype: NORMAL
+code: BB_BasicsExchangeRatesInsertToOrdersTableMapper-1.1.qstep
diff --git a/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/create_order-post_reload.qscript b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/create_order-post_reload.qscript
new file mode 100755
index 0000000000000000000000000000000000000000..c733f901f4d485444597970882e2f7e87f4f7512
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/14_workflow_validation_code/qore/create_order-post_reload.qscript
@@ -0,0 +1,25 @@
+#!/usr/bin/env qore
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusClientCore
+
+QorusClient::initFast();
+
+const WORKFLOW_NAME = "BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-MAPPER";
+
+const ORDER_DATA = {
+ "staticdata": {
+ "orders": {
+ "id": (24, 25),
+ "currency_to_buy": ("usd", "czk"),
+ "currency_to_sell": ("eur", "usd"),
+ "amount": (100, 600),
+ }
+ }
+};
+
+hash response = qrest.post("workflows/" + WORKFLOW_NAME + "?action=createOrder", ORDER_DATA);
+printf("Response: %N\n", response);
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/README.md b/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f567f6a984391d31b2910acc905c1971d2082ffa
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/README.md
@@ -0,0 +1,81 @@
+# Workflow Error Handling and Recovery
+
+Qorus includes a framework for defining error information and raising errors. If a workflow defines the `errors` tag (see the example below) in the workflow definition the error information is loaded from the file the tag is referencing to and the information is then loaded in the system database in the *GLOBAL_WORKFLOW_ERRORS* and *WORKFLOW_ERRORS* (see Global and Workflow-Specific Error Definitions for more information). The return value of this function is a hash describing how the system should act when certain errors are raised when processing workflow order data.
+
+```yaml
+errors: bb-bb-bb-basics-process-orders-wf-errors.yaml
+```
+
+Workflows raise errors by calling the [stepError()](https://qoretechnologies.com/manual/qorus/latest/qorus/classOMQ_1_1UserApi_1_1Workflow_1_1WorkflowApi.html#a3c816730408849ce515958a44f503804) method or by throwing exceptions. In the case an exception is thrown, the exception name is used as the error name; the system will then use the error name as the
+hash key to look up error information in the workflow error object.
+
+To allow a workflow to recover gracefully from an error, implement a [validation code](../workflow_validation_code) for each step. [Validation code](../workflow_validation_code) allow workflows to recover gracefully from errors such as lost request or response messages or temporary communication failures without requiring manual intervention.
+
+## Global and Workflow-Specific Error Definitions
+
+By default, error definitions are global. A global definition is a workflow error definition that applies to all workflows. Workflow-specific error definitions apply only to a particular workflow configuration.
+
+There are three ways to create workflow-specific error definitions:
+
++ tag the error definition with *"level"* = [OMQ::ErrLevelWorkflow](https://www.qoretechnologies.com/manual/qorus/latest/qorus/group__errleveltypes.html#ga0f322173cd15b5e1083ef05238403427) in the [errorfunction's](#workflow-error-function) return value
++ create the workflow-specific error manually with an API call (ex: [omq.system.update-workflow-error()](https://www.qoretechnologies.com/manual/qorus/latest/qorus/QorusSystemAPI_8qc.html#adad109b9c87f9ba0d057bc59abe0a83f))
++ leave the "level" key unassigned (or assigned to the default: [OMQ::ErrLevelAuto](https://www.qoretechnologies.com/manual/qorus/latest/qorus/group__errleveltypes.html#ga7849e84aaef7b8d2fbe961df7faedfc8)) in the [errorfunction's](#workflow-error-function) return value but give the error a different definition than an existing global error
+
+The last point above implies that if two or more workflows define the same error with different attributes (but leave the error's "level" option either unset or assigned to the default: [OMQ::ErrLevelAuto](https://www.qoretechnologies.com/manual/qorus/latest/qorus/group__errleveltypes.html#ga7849e84aaef7b8d2fbe961df7faedfc8)), the first error will be a global error, and each time the other workflows define the error with a different set of attributes, those new errors will be workflow-specific error definitions.
+
+Information on how to implement Qorus Objects using YAML format can be found [here](https://qoretechnologies.com/manual/qorus/latest/qorus/implementingqorusobjectsusingyaml.html).
+
+The YAML schema used for validation of the workflow errors can be downloaded [here](https://qoretechnologies.com/manual/qorus/latest/qorus/errors_schema.yaml).
+
+## Default Global Workflow Error Definitions
+Qorus includes default error definitions for common technical errors that should normally result in a workflow order instance retry and also template errors that can be re-used in workflows as needed.
+
+The following table lists the default errors delivered with Qorus; note that workflows with a severity level of [OMQ::ES_Warning](https://www.qoretechnologies.com/manual/qorus/latest/qorus/group__ErrorSeverityCodes.html#gab6e9d95ef75ebcd814b8cafcb0b8c5f1) (*"WARNING"*) only result in a warning error record but do not affect the logic flow of the workflow.
+
+Default global workflow error definitions can be found [**here**](https://www.qoretechnologies.com/manual/qorus/latest/qorus/designimplworkflows.html#globalandworkflowerrors)
+
+# Workflow Error Definition
+
+**Expected Return Value**
+A hash where each key is the error string code (ex: *"PAYMENT-MESSAGE-TIMEOUT"*), and each value is a hash with error information as given in the following description of the keys:
+
++ desc: the description of the error
++ severity: the severity code; see Error Severity Codes, default: OMQ::ES_Major
++ status: the status code for the step due to this error; either OMQ::StatRetry or OMQ::StatError (default if not present)
++ business: is this a business error? default: False
++ retry-delay: the default retry time for the error as an integer giving seconds (ie 1200 for 20 minutes) or a relative date/time value (i.e. 2D + 12h or P2D12H, both meaning 2 days and 12 hours); the relative date/time value format is normally recommended as it's more readable
++ level: the error level value; see Error Level Type Constants for possible values; default: OMQ::ErrLevelAuto ("AUTO")
+
+Only the desc key is required. Note:
+
++ if the "severity" key is not present, the error has a default severity of OMQ::ES_Major ("MAJOR")
++ if the "status" key is not present, the default is OMQ::StatError ("ERROR")
++ if the "level" key is not present, the default is OMQ::ErrLevelAuto ("AUTO")
++ the "retry-delay" key should only be given on errors with status set to OMQ::StatRetry ("RETRY")
+
+## Errors Definition Example
+
+```yaml
+type: errors
+errors:
+ - name: CONNECTION-ERROR
+ desc: The given connection is not known
+ status: RETRY
+ business: false
+ retry-delay: 30
+```
+
+This error object defines one error that tells how the **"BASICS-EXCHANGE-CURRENCY-WORKFLOW"** workflow should react in case some of its steps throws the *"CONNECTION-ERROR"* exception. If the exception is thrown it tells Qorus to automatically retry to run a workflow instance in 30 seconds. This exception can be thrown by the *getUserConnection()* method in the **"BasicsSendEmailToUser"** step when **"smtp-email"** user connection is not available.
+
+# Testing
+
+To test the error function the **"smtp-email"** user connection can be simply removed from the Qorus user connections. Then after creation of a new workflow order it will stop in **"BasicsSendEmailToUser"** step with the above error. And the workflow instance will be retried in 30 seconds. By restoring the removed user connection the workflow order should get complete status.
+
+---
+
+
diff --git a/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/bb-basics-process-orders-wf-errors.yaml b/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/bb-basics-process-orders-wf-errors.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5ab410291edf11863cd9d4909d1fda4940bf6ae5
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/bb-basics-process-orders-wf-errors.yaml
@@ -0,0 +1,9 @@
+type: errors
+name: bb-bb-bb-basics-process-orders-wf-errors
+desc: custom error definitions
+errors:
+ - name: CONNECTION-ERROR
+ desc: The given connection is not known
+ status: RETRY
+ business: false
+ retry-delay: 30
diff --git a/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/java/BB-BASICS-PROCESS-ORDERS-WORKFLOW-JAVA-1.0.qwf.yaml b/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/java/BB-BASICS-PROCESS-ORDERS-WORKFLOW-JAVA-1.0.qwf.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..9b5080f5107dc398ac28d8efcf56fd0b4bed79dc
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/java/BB-BASICS-PROCESS-ORDERS-WORKFLOW-JAVA-1.0.qwf.yaml
@@ -0,0 +1,23 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-PROCESS-ORDERS-WORKFLOW-JAVA
+desc: Processes given orders
+author:
+ - Qore Technologies, s.r.o.
+autostart: 1
+classes:
+ - BB_BasicsChangeOrderStatusJava
+errors: ../bb-basics-process-orders-wf-errors.yaml
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+keylist:
+ - ids
+version: '1.0'
+steps:
+ [
+ "BB_BasicsChangeOrderStatusToInProgressJava:1.0",
+ "BB_BasicsSendMoneyToBankJava:1.0",
+ "BB_BasicsSendMoneyToUserJava:1.0",
+ "BB_BasicsSendEmailToUserJava:1.0",
+ "BB_BasicsChangeOrderStatusToCompletedJava:1.0"
+ ]
diff --git a/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/python/BB-BASICS-PROCESS-ORDERS-WORKFLOW-PYTHON-1.0.qwf.yaml b/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/python/BB-BASICS-PROCESS-ORDERS-WORKFLOW-PYTHON-1.0.qwf.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..080abd53a663c6aedbf017bd5694505d041cdce3
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/python/BB-BASICS-PROCESS-ORDERS-WORKFLOW-PYTHON-1.0.qwf.yaml
@@ -0,0 +1,22 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-PROCESS-ORDERS-WORKFLOW-PYTHON
+desc: Processes given orders
+author:
+ - Qore Technologies, s.r.o.
+autostart: 1
+classes:
+ - BB_BasicsChangeOrderStatusPython
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+keylist:
+ - ids
+version: "1.0"
+steps:
+ [
+ "BB_BasicsChangeOrderStatusToInProgressPython:1.0",
+ "BB_BasicsSendMoneyToBankPython:1.0",
+ "BB_BasicsSendMoneyToUserPython:1.0",
+ "BB_BasicsSendEmailToUserPython:1.0",
+ "BB_BasicsChangeOrderStatusToCompletedPython:1.0"
+ ]
diff --git a/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/qore/BB-BASICS-PROCESS-ORDERS-WORKFLOW-1.0.yaml b/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/qore/BB-BASICS-PROCESS-ORDERS-WORKFLOW-1.0.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..bf6779771e14b75dba5806919e76f83ae8f9b07f
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/15_workflow_error_function/qore/BB-BASICS-PROCESS-ORDERS-WORKFLOW-1.0.yaml
@@ -0,0 +1,23 @@
+# This is a generated file, don't edit!
+type: workflow
+name: BB-BASICS-PROCESS-ORDERS-WORKFLOW
+desc: "Processes given orders"
+author:
+ - Qore Technologies, s.r.o.
+classes:
+ - BB_BasicsChangeOrderStatus
+groups:
+ - BASIC-TRAINING-EXCHANGE-APP
+keylist:
+ - ids
+steps:
+ [
+ "BB_BasicsChangeOrderStatusToInProgress:1.0",
+ "BB_BasicsSendMoneyToBank:1.0",
+ "BB_BasicsSendMoneyToUser:1.0",
+ "BB_BasicsSendEmailToUser:1.0",
+ "BB_BasicsChangeOrderStatusToCompleted:1.0"
+ ]
+version: "1.0"
+autostart: 1
+errors: bb-basics-process-orders-wf-errors.yaml
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/16_testing/README.md b/03_basics_building_blocks/01_exchange_rates_app/16_testing/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..7593094f8f943eb967620fa25ae660d9f31cddf1
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/16_testing/README.md
@@ -0,0 +1,172 @@
+# Testing
+
+## Introduction to the QUnit Module
+
+The [QUnit](https://qoretechnologies.com/manual/qorus/current/qore/modules/QUnit/html/index.html#qunitintro) module provides a framework for automated testing.
+
+It contains base classes for creating test cases and test suites. It also provides a dependency injection helper for mocking pre-existing classes without modifying their code.
+
+It also provides a number of pre-defined testing functions for use in assertions.
+
+Example:
+
+```php
+#!/usr/bin/env qore
+# -*- mode: qore; indent-tabs-mode: nil -*-
+%new-style
+%enable-all-warnings
+%require-types
+%strict-args
+%requires ../../qlib/QUnit.qm
+#%include ./_some_module_to_test
+%exec-class QUnitTest
+public class QUnitTest inherits QUnit::Test {
+ constructor() : Test("QUnitTest", "1.0") {
+ addTestCase("What this method is testing", \testMethod(), NOTHING);
+ addTestCase("Skipped test", \testSkipped(), NOTHING);
+ # Return for compatibility with test harness that checks return value.
+ set_return_value(main());
+ }
+ testMethod() {
+ # Test against success
+ testAssertion("success", \equals(), (True, True));
+ # Test against something else
+ testAssertion("failure", \equals(), (True, False), RESULT_FAILURE);
+ }
+ testSkipped() {
+ # Skip this test
+ testSkip("Because of the reason it skipped");
+ }
+}
+```
+
+## Introduction to the QorusInterfaceTest Module
+
+The [QorusInterfaceTest](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1QorusInterfaceTest.html) module provides functionality for automatic testing; it is based on the [QUnit module](https://qoretechnologies.com/manual/qorus/current/qore/modules/QUnit/html/index.html#qunitintro) from Qore. Currently the module provides the following classes:
+
+* [QorusInterfaceTest](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1QorusInterfaceTest.html): extension of the Qore [QUnit module](https://qoretechnologies.com/manual/qorus/current/qore/modules/QUnit/html/index.html#qunitintro) with functions testing Qorus-specific interfaces (abstract class)
+ * [QorusJobTest](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1QorusJobTest.html): base class for testing jobs
+ * [QorusPassiveWorkflowTest](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1QorusPassiveWorkflowTest.html): class for testing workflows without starting execution instances (for example, for testing synchronous workflow orders, etc)
+ * [QorusServiceTest](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1QorusServiceTest.html): base class for testing services
+ * [QorusWorkflowTest](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1QorusWorkflowTest.html): base class for testing workflows, will automatically start execution instances if not already running
+* [Action](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1Action.html): abstract class representing some action
+ * [CreateFile](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CreateFile.html): abstract class for input file
+ * [CreateFileText](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CreateFileText.html): input text file
+ * [CreateFileCsv](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CreateFileCsv.html): input csv file
+ * [InsertDbTableRows](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1InsertDbTableRows.html): inserts one or more rows into DB table
+ * [InsertDbTableRow](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1InsertDbTableRow.html): insert a row into DB table
+ * [DeleteDbTableData](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1DeleteDbTableData.html): delete data from DB table
+ * [TruncateTable](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1TruncateTable.html): truncate a DB table (delete all rows)
+ * [AbstractRemoteDbSqlUtilAction](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1AbstractRemoteDbSqlUtilAction.html): abstract base class for remote sqlutil streaming actions
+ * [AbstractRemoteDbSelectAction](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1AbstractRemoteDbSelectAction.html): abstract base class for streaming data from a remote DB
+ * [RemoteDbSelectAction](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1RemoteDbSelectAction.html): streams data from a remote DB and compares to expected row data
+ * [RemoteDbDeleteAction](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1RemoteDbDeleteAction.html): deletes data in a remote datasource
+ * [RemoteDbRowSqlUtilAction](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1RemoteDbRowSqlUtilAction.html): performs sqlutil-based streaming inserts or upserts of data in remote datasources in one or more tables in a single remote transaction
+ * [InsertRemoteDbTableRows](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1InsertRemoteDbTableRows.html): performs sqlutil-based streaming to insert rows in a single remote table
+ * [UpsertRemoteDbTableRows](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1UpsertRemoteDbTableRows.html): performs sqlutil-based streaming to upsert (or merge) rows in a single remote table
+ * [CallService](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CallService.html): call any Qorus service
+ * [Sleep](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1Sleep.html): wait action
+ * [WaitForWfiid](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1WaitForWfiid.html): wait for WFIID to be finished
+ * [CreateOrder](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CreateOrder.html): creates order
+ * [CheckFile](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CheckFile.html): check whether some file exists
+ * [CheckFileCsv](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CheckFileCsv.html): check csv file content
+ * [CheckFileNonexisting](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CheckFileNonexisting.html): check whether some file doesn't exist
+ * [ExecSynchronousOrder](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1ExecSynchronousOrder.html): creates and executes a synchronous workflow order
+ * [StartLog](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1StartLog.html): starts log stopwatch (i. e. from this time we will check log files)
+ * [CheckLog](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CheckLog.html): check whether log matches specified pattern
+ * [CheckDbTableRows](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CheckDbTableRows.html): check rows in a database
+ * [CheckDbTableRow](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CheckDbTableRow.html): check a row in a database
+ * [CallService](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1CallService.html): invokes given service
+ * [RunJob](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1RunJob.html): invokes the given job and retrieves the result
+ * [RunJobResult](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1RunJobResult.html): runs the given job and checks the result status and optionally the result job info hash To use this module, use "%requires [QorusInterfaceTest](https://qoretechnologies.com/manual/qorus/current/qorus/classQorusInterfaceTest_1_1QorusInterfaceTest.html)" in your code. Example:
+ ```java
+ class MyJobTest inherits QorusJobTest {
+ constructor() : QorusJobTest("my-job", "1.0", \ARGV) {
+ addTestCase("my-test-1", est1());
+ }
+ test1() {
+ t.exec(new CreateFileText(input_filename, file_content));
+ t.exec(new RunJobResult(OMQ::StatComplete));
+ }
+ }
+ ```
+
+### How to run tests
+
+Make sure your scripts have the executable bit set (on UNIX systems) and an appropriate hash-bang as the first line (ex: "#!/usr/bin/env qr" or "#!/usr/bin/env qore". If your script's name is test.qtest, then The following command should execute the script:
+
+test.qtest [OPTIONS]
+
+See [QorusInterfaceTest module](https://qoretechnologies.com/manual/qorus/current/qorus/namespaceQorusInterfaceTest.html) in [Qore](https://qoretechnologies.com/manual/qorus/current/qore/lang/html/namespace_qore.html) for options, output formats and further details.
+
+## Exchange rates application tests
+
+### BASICS-EXCHANGE-CURRENCY-WORKFLOW test
+
+This test can be run using the following command from the command line:
+
+```
+./BASICS-EXCHANGE-CURRENCY-WORKFLOW.qtest -i1 -bCZK -sEUR -a150
+```
+
+This will create a workflow order with the following exchange order data:
+```php
+id : 1
+currency_to_buy : "CZK"
+currency_to_sell : "EUR"
+amount : 150
+```
+
+The [options](https://qoretechnologies.com/manual/qorus/latest/qore/lang/html/class_qore_1_1_get_opt.html#a548ec0b81ec224ccb9498ca4a6e6e831) of the test are defined in the following hash:
+```php
+const OPTIONS = Opts + {
+ "id": "i,id=i",
+ "currency_to_buy": "b,buy=s",
+ "currency_to_sell": "s,sell=s",
+ "amount": "a,amount=f"
+};
+```
+
+The Qorus Visual Code extension currently doesn't support deploying **.qtest** files, so the test has to be run manually, in case your Qorus is running in docker you would need to connect to the container:
+
+```
+docker exec -it qorus bash
+```
+
+And set the environment variables:
+
+```
+. /opt/qorus/bin/env.sh
+```
+
+### BASICS-PROCESS-ORDERS-WORKFLOW test
+
+This test can be run using the following command from the command line:
+
+```
+./BASICS-PROCESS-ORDERS-WORKFLOW.qtest -i1 -eyour@email.com
+```
+
+This will create a workflow order to process the exchange order with id 1 from the database.
+
+### BasicsExchangeCurrencyApiService test
+
+This test can be run the same way the **BASICS-EXCHANGE-CURRENCY-WORKFLOW** is run:
+```
+./BasicsExchangeCurrencyApiService.qtest -i1 -bCZK -sEUR -a150
+```
+
+The test will call the exchange method of the **basics-exchange-currency-api-service** service.
+
+
+### BasicsProcessOrdersJob test
+
+The BasicsProcessOrdersJob test just calls the run function of the **basics-process-orders-job** job.
+
+---
+
+
diff --git a/03_basics_building_blocks/01_exchange_rates_app/16_testing/java/BB_BasicsExchangeCurrencyApiServiceTest.java b/03_basics_building_blocks/01_exchange_rates_app/16_testing/java/BB_BasicsExchangeCurrencyApiServiceTest.java
new file mode 100755
index 0000000000000000000000000000000000000000..af186eacf09eda761781dc6f482eb490e38bf37a
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/16_testing/java/BB_BasicsExchangeCurrencyApiServiceTest.java
@@ -0,0 +1,32 @@
+import com.qoretechnologies.qorus.*;
+import com.qoretechnologies.qorus.client.*;
+import com.qoretechnologies.qorus.test.*;
+
+import org.qore.jni.Hash;
+
+import java.util.List;
+
+public class BB_BasicsExchangeCurrencyApiServiceTest {
+ private static final String ServiceName = "bb-basics-exchange-currency-api-service-java";
+ private static Hash Order = new Hash();
+
+ public static void main(String[] args) throws Throwable {
+ Order.put("id", 12345);
+ Order.put("currency_to_buy", "czk");
+ Order.put("currency_to_sell", "eur");
+ Order.put("amount", 213);
+ Order.put("comment", "test");
+
+ QorusClientCore.init2();
+ QorusServiceTest test = new QorusServiceTest(ServiceName);
+
+ test.addTestCase("svc test", () -> mainTest(test));
+ test.main();
+ }
+
+ private static void mainTest(QorusServiceTest test) throws Throwable {
+ CallService call = new CallService(ServiceName + ".exchange", Order);
+ test.exec(call);
+ System.out.print("Result: " + call.getResult());
+ }
+}
diff --git a/03_basics_building_blocks/01_exchange_rates_app/16_testing/java/BB_BasicsProcessOrdersJobTest.java b/03_basics_building_blocks/01_exchange_rates_app/16_testing/java/BB_BasicsProcessOrdersJobTest.java
new file mode 100755
index 0000000000000000000000000000000000000000..a7ccbd827df2986add5dfc229683c782974b8e39
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/16_testing/java/BB_BasicsProcessOrdersJobTest.java
@@ -0,0 +1,23 @@
+import com.qoretechnologies.qorus.*;
+import com.qoretechnologies.qorus.client.*;
+import com.qoretechnologies.qorus.test.*;
+
+import java.util.HashMap;
+
+public class BB_BasicsProcessOrdersJobTest {
+ public static void main(String[] args) throws Throwable {
+ QorusClientCore.init2();
+ QorusJobTest test = new QorusJobTest("bb-basics-process-orders-job");
+
+ test.addTestCase("job test", () -> testJob(test));
+ test.main();
+ }
+
+ private static void testJob(QorusJobTest test) throws Throwable {
+ RunJob action = new RunJob();
+ test.exec(action);
+
+ action = new RunJobResult(OMQ.StatComplete);
+ test.exec(action);
+ }
+}
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/16_testing/java/BB_ExchangeCurrencyWfTest.java b/03_basics_building_blocks/01_exchange_rates_app/16_testing/java/BB_ExchangeCurrencyWfTest.java
new file mode 100755
index 0000000000000000000000000000000000000000..1d3e56f90d001435c167d1620d6469eb28d9ddbc
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/16_testing/java/BB_ExchangeCurrencyWfTest.java
@@ -0,0 +1,52 @@
+import com.qoretechnologies.qorus.test.*;
+import com.qoretechnologies.qorus.client.*;
+
+import org.qore.jni.Hash;
+import org.qore.lang.qunit.*;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BB_ExchangeCurrencyWfTest {
+ private static final String WorkflowName = "BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW-JAVA";
+
+ private static Hash Orders = new Hash();
+
+ public static void main(String[] args) throws Throwable {
+ Orders.put("id", 212345);
+ Orders.put("currency_to_buy", "czk");
+ Orders.put("currency_to_sell", "eur");
+ Orders.put("amount", 31234);
+
+ QorusClientCore.init2();
+ QorusWorkflowTest test = new QorusWorkflowTest(WorkflowName, "1.0");
+
+ test.addTestCase(WorkflowName, () -> mainTest(test));
+ test.main();
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void mainTest(QorusWorkflowTest test) throws Throwable {
+ Hash staticData = new Hash();
+ staticData.put("orders", Orders);
+
+ int wfiid = test.createOrder(staticData);
+ test.exec(new WaitForWfiid(wfiid));
+
+ Hash select = new Hash();
+
+ Hash where = new Hash();
+ where.put("id", 212345);
+
+ select.put("where", where);
+
+ Orders.put("status", "new");
+
+ Hash[] expected = new Hash[1];
+ expected[0] = Orders;
+
+ test.exec(new CheckDbTableRows("omquser", "exchange_orders", select, expected));
+ }
+}
diff --git a/03_basics_building_blocks/01_exchange_rates_app/16_testing/python/BasicsProcessOrdersJobTest.py b/03_basics_building_blocks/01_exchange_rates_app/16_testing/python/BasicsProcessOrdersJobTest.py
new file mode 100755
index 0000000000000000000000000000000000000000..886ddce8e028642b9d4042a3f98e775c97787184
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/16_testing/python/BasicsProcessOrdersJobTest.py
@@ -0,0 +1,19 @@
+import sys
+import qoreloader
+from qore.QorusInterfaceTest import QorusJobTest, RunJob, RunJobResult
+from qore.__root__ import OMQ
+
+class BasicsProcessOrdersJobTest(QorusJobTest):
+ def __init__(self):
+ super(QorusJobTest, self).__init__("basics-process-orders-job", "1.0")
+ self.addTestCase("job test", self.test1)
+ self.main()
+
+ def test1(self):
+ action = RunJob()
+ self.exec(action)
+
+ action = RunJobResult(OMQ.StatComplete)
+ self.exec(action)
+
+mytest = BasicsProcessOrdersJobTest()
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW.qtest b/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW.qtest
new file mode 100755
index 0000000000000000000000000000000000000000..7fd74f5abda099147dadb364123bedefef4be96d
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW.qtest
@@ -0,0 +1,52 @@
+#! /usr/bin/env qore
+
+# -*- mode: qore; indent-tabs-mode: nil -*-
+# test file: BASICS-EXCHANGE-CURRENCY-WORKFLOW
+# author: Alzhan Turlybekov (Qore Technologies)
+
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusInterfaceTest
+
+%exec-class Test
+
+class Test inherits QorusWorkflowTest {
+ private {
+ const WORKFLOW_NAME = "BB-BASICS-EXCHANGE-CURRENCY-WORKFLOW";
+
+ const OPTIONS = Opts + {
+ "id": "i,id=i",
+ "currency_to_buy": "b,buy=s",
+ "currency_to_sell": "s,sell=s",
+ "amount": "a,amount=f",
+ "comment": "c,comment=s"
+ };
+ }
+
+ constructor() : QorusWorkflowTest(WORKFLOW_NAME, "1.0", \ARGV, OPTIONS) {
+ addTestCase(WORKFLOW_NAME, \mainTest());
+ set_return_value(main());
+ }
+
+ mainTest() {
+ m_options = m_options + {"creation_date": date(format_date("YYYYMMDD", now())), "status": "new"};
+ printf("%N\n", m_options );
+ int wfiid = exec(new CreateOrder(WORKFLOW_NAME, {"orders": {m_options.id: m_options}})).wfiid();
+ exec(new WaitForWfiid(wfiid));
+
+ hash select_hash = {
+ "where": {
+ "id": m_options.id
+ }
+ };
+
+ hash expected = m_options + {
+ "status": "new"
+ };
+
+ exec(new CheckDbTableRows("omquser", "exchange_orders", select_hash, expected));
+ }
+}
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB-BASICS-PROCESS-ORDERS-WORKFLOW.qtest b/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB-BASICS-PROCESS-ORDERS-WORKFLOW.qtest
new file mode 100755
index 0000000000000000000000000000000000000000..0421f6fc5574ad321e35890c4327e6a501568094
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB-BASICS-PROCESS-ORDERS-WORKFLOW.qtest
@@ -0,0 +1,67 @@
+#! /usr/bin/env qore
+
+# -*- mode: qore; indent-tabs-mode: nil -*-
+# test file: BASICS-PROCESS-ORDERS-WORKFLOW
+# author: Alzhan Turlybekov (Qore Technologies)
+
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusInterfaceTest
+%requires SqlUtil
+
+%exec-class Test
+
+class Test inherits QorusWorkflowTest {
+ private {
+ const WORKFLOW_NAME = "BB-BASICS-PROCESS-ORDERS-WORKFLOW";
+ const DATASOURCE_NAME = "omquser";
+ const TABLE_NAME = "exchange_orders";
+
+ const OPTIONS = Opts + {
+ "id": "i,id=i", # id of the exchange order from the database
+ "user_email": "e,email=s"
+ };
+
+ const RATES = {
+ "exchange_rates": {
+ "eur": 1.0,
+ "czk": 25.0,
+ "usd": 22.0
+ }
+ };
+ }
+
+ constructor() : QorusWorkflowTest(WORKFLOW_NAME, "1.0", \ARGV, OPTIONS) {
+ addTestCase(WORKFLOW_NAME, \mainTest());
+ set_return_value(main());
+ }
+
+ mainTest() {
+ printf("%N\n", m_options);
+
+ hash select_hash = {
+ "where": {
+ "id": m_options.id
+ }
+ };
+
+ # check if the order is in the database
+ exec(new CheckDbTableRows(DATASOURCE_NAME, TABLE_NAME, select_hash, {"status": "new"}));
+
+ SqlUtil::AbstractTable exchange_orders_table = get_sql_table(DATASOURCE_NAME, TABLE_NAME);
+ hash result = exchange_orders_table.selectRow(select_hash);
+ result.user_email = m_options.user_email;
+
+ printf("%N\n", m_options + {"orders": list(result,),} + RATES);
+ int wfiid = exec(new CreateOrder(WORKFLOW_NAME,
+ {
+ "orders": list(result,),
+ } + RATES)).wfiid();
+ exec(new WaitForWfiid(wfiid));
+
+ exec(new CheckDbTableRows(DATASOURCE_NAME, TABLE_NAME, select_hash, {"status": "completed"}));
+ }
+}
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB_BasicsExchangeCurrencyApiService.qtest b/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB_BasicsExchangeCurrencyApiService.qtest
new file mode 100755
index 0000000000000000000000000000000000000000..ed7e317d471f4c34e284951a19578b0c1fe9ff13
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB_BasicsExchangeCurrencyApiService.qtest
@@ -0,0 +1,39 @@
+#! /usr/bin/env qore
+
+# -*- mode: qore; indent-tabs-mode: nil -*-
+# test file: BasicsExchangeCurrencyApiService
+# author: Alzhan Turlybekov (Qore Technologies)
+
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusInterfaceTest
+
+%exec-class Test
+
+class Test inherits QorusServiceTest {
+ private {
+ const SERVICE_NAME = "bb-basics-exchange-currency-api-service";
+
+ const OPTIONS = Opts + {
+ "id": "i,id=i",
+ "currency_to_buy": "b,buy=s",
+ "currency_to_sell": "s,sell=s",
+ "amount": "a,amount=f",
+ "comment": "c,comment=s"
+ };
+ }
+
+ constructor() : QorusServiceTest(SERVICE_NAME) {
+ addTestCase("mainTest", \mainTest());
+ set_return_value(main());
+ }
+
+ mainTest() {
+ printf("%N\n", m_options);
+ auto response = exec(new CallService("user." + SERVICE_NAME + ".exchange", m_options)).getResult();
+ printf("worfklow instance id: %y\n", response);
+ }
+}
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB_BasicsProcessOrdersJob.qtest b/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB_BasicsProcessOrdersJob.qtest
new file mode 100755
index 0000000000000000000000000000000000000000..f7537f7ff0edd4a32f3af6a533dca52e0f2f8df7
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/16_testing/qore/BB_BasicsProcessOrdersJob.qtest
@@ -0,0 +1,26 @@
+#! /usr/bin/env qore
+
+# -*- mode: qore; indent-tabs-mode: nil -*-
+# test file: BasicsProcessOrdersJob
+# author: Alzhan Turlybekov (Qore Technologies)
+
+%new-style
+%strict-args
+%require-types
+%enable-all-warnings
+
+%requires QorusInterfaceTest
+
+%exec-class Test
+
+class Test inherits QorusJobTest {
+ constructor() : QorusJobTest("bb-basics-process-orders-job", "1.0") {
+ addTestCase("mainTest", \mainTest());
+ set_return_value(main());
+ }
+
+ mainTest() {
+ auto result = run();
+ printf("Result: %N\n", result);
+ }
+}
\ No newline at end of file
diff --git a/03_basics_building_blocks/01_exchange_rates_app/README.md b/03_basics_building_blocks/01_exchange_rates_app/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..0522133ef2c18111c3a4c144914cea521d4103a9
--- /dev/null
+++ b/03_basics_building_blocks/01_exchange_rates_app/README.md
@@ -0,0 +1,38 @@
+# Exchange rates application
+This application allows user to convert currencies and get information about actual exchange rates.
+It touches upon fundamental Qorus objects such as workflows, services and jobs. Also it covers topics such as mappers, fail-safe processing using validation and error function and it shows how these objects can communicate with each other.
+
+List of tutorials:
+1. [Implementing Qorus Jobs](01_job)
+2. [Implementing Qorus Services](02_service)
+3. [Implementing Qorus Workflows](03_workflow)
+4. [Workflow Serial Steps](04_workflow_serial_steps)
+5. [Workflow Parallel Steps](05_workflow_parallel_steps)
+6. [Exchange Rates API User Connection](06_exchange_api_user_connection)
+7. [Get Exchange Rates using Job](07_job_get_exchange_rates)
+8. [Exchange Rates Datasource Connection](08_exchange_rates_datasource_connection)
+9. [Exchange Currency Workflow](09_exchange_currency_workflow)
+10. [Exchange Currency Workflow with Mapper](10_exchange_currency_workflow_with_mapper)
+11. [Exchange Currency API Service](11_exchange_currency_api_service)
+12. [Process Orders Workflow](12_process_orders_workflow)
+13. [Process Orders Job](13_process_orders_job)
+14. [Workflow Validation Code](14_workflow_validation_code)
+15. [Workflow Error Function](15_workflow_error_function)
+16. [Class-based step configuration items](16_workflow_step_configuration_items)
+17. [Testing](17_testing)
+---
+
+## Design
+
+
+
+
+
+
[Go Back to: Basics of Qorus](../)
+
[Start tutorial: Implementing Qorus jobs](01_job)
+
+
diff --git a/03_basics_building_blocks/01_exchange_rates_app/img/exchange_app_design.pdf b/03_basics_building_blocks/01_exchange_rates_app/img/exchange_app_design.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..4fece8af26ada1e97d55d0762eae8882b228f341
Binary files /dev/null and b/03_basics_building_blocks/01_exchange_rates_app/img/exchange_app_design.pdf differ
diff --git a/03_basics_building_blocks/README.md b/03_basics_building_blocks/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..da1ae225927b1351feb8bfece822cfc16a777c1a
--- /dev/null
+++ b/03_basics_building_blocks/README.md
@@ -0,0 +1,30 @@
+# Basics
+
+## What is Qorus?
+Qorus Integration Engine® is an integration platorm designed for the rapid development and operations of fault-tolerant interfaces. The "fault-tolerant" part is important, as we believe that error handling must be included from the very first release of an interface project, and furthermore that error handling be consistent, configurable, and automatic regarding technical errors. Therefore every solution delivered in Qorus should be failsafe or tolerant of technical errors.
+
+The goal of Qorus is to provide very high business flexibility and low costs, meaning low development and also low operational costs. Qorus is particularly strong with the fault-tolerant execution of stateful integration solutions and is also suitable for solving a wide variety of other integration challenges.
+
+Read more in the documentation: [Introduction to the Qorus Integration Engine](https://qoretechnologies.com/manual/qorus/latest/qorus/sysrefintro.html)
+
+## Features of Qorus Integration Engine®
+
+The following are features of Qorus Integration Engine® supporting the goals mentioned above:
+
+* Dynamic loading and unloading of interface configurations and code
+* Low-code/no-code focus on configuration over coding; coding should be minimized and building blocks should be used instead
+* Focus on simplicity; the platform should handle the complexity, not the developer
+* Complete operational traceability with full visibility of configuration and code in the operational UI resulting in an unrivaled DevOps platform
+* Focus on quality and repeatable results with direct support for Continuous Integration and Continuous Delivery
+* Consistent platform-defined automatic error handling of technical errors and error recovery for stateful interfaces (workflows)
+
+## Tutorials
+1. [Exchange rates application](01_exchange_rates_app)
+
+---
+
+
+
+
[Go Back to: Qorus training program](https://git.qoretechnologies.com/qorus/training/tree/master/03_basics)