The Process API stands as a fundamental component in intent-driven development, but its exact role may not be immediately clear. While the UI schema is responsible for creating the user interface, the Process API is tasked with executing logic across the UI, data, and custom processes. Essentially, the Process API enables the definition of processes through a sequence of process steps. It represents the ‘execution of intent’ aspect in the intent-driven development framework, forming the backbone of custom intent execution.
Example libraries
There are two process api implementations we can use as a reference.
- crs-process-api – A JavaScript-based process API tailored for web applications, featuring a rich set of modules and undergoing constant development to meet emerging needs.
- py-processes – A Python-based process API designed for Python applications, primarily utilized for Selenium applications. It is being expanded to incorporate modules for data mining and artificial intelligence.
Process api workflows
Processes may range from straightforward single-step actions to intricate systems that operate based on logic and conditions within a network of complex scenarios. The Process API can be seen as an executable flow diagram defined in a JSON structure. Moreover, process steps have the capability to initiate other processes. These processes aren’t restricted to a single file; as long as their schemas are registered with the schema registry, they remain accessible for execution by any process. This feature facilitates the reusability of processes, allowing process schemas to function as libraries of intent.
There are two main parts on the process api that one should take note of.
- Specifying Process API capabilities via modules and actions.
- Implementing Process API functionalities through coding or process schemas.
Modules
From an architectural standpoint, the Process API is notably straightforward. It provides a set of core features to the user, but its true potential lies in its modules. The capabilities available in your schemas are determined by the modules registered with the Process API. Its modular nature allows for the addition of custom modules capable of performing a variety of tasks. These modules are lazy-loaded, meaning they only load their execution logic when it is needed for the first time.
Modules are static classes that reveal actions that can be executed. These actions are always asynchronous and consistently accept the same parameters:
- step – the JSON definition of the test step that is being executed.
- context – the context object is provided during the execution of a process. This context, which can be any object or class instance, is passed to the process API when invoked. It serves as a means for the process to access and interact with relevant data or components. For instance, if there’s a user CRUD component with associated processes, the component itself could be passed as the context to these processes. This allows the processes to directly interact with the CRUD component, treating it as the “context” for their operations. The context is not a required parameter. Add it if your process requires it.
- process – the ongoing process that is currently being executed.
- item – in case of a loop, the current item that is being iterated over. For more details on this please see the loop documentation.
In the Python version, the first parameter is consistently the ‘api’ – an instance of the process API that provides utility functions frequently used in process actions.
To understand how this works, lets create a simple math module, in this case we will use JavaScript.
export class MathModule {
static async add(step, context, process, item) {
const value1 = await getValue(step.args.value1, context, process, item);
const value2 = await getValue(step.args.value2, context, process, item);
const result = value1 + value2;
if (step.args.target != null) {
await setValue(step.args.target, result)
}
return result;
}
}
crs.intent.math = MathActions;
In the given example, we are constructing a static class named “MathModule
” which reveals a static method known as “add”. In the realm of intent-driven development (IDD), we refer to the ‘add’ method as an “action”. Within this action, there’s usage of a utility function named “getValue
“. We’ll delve into this function in greater detail later, but it’s important to note that in the JavaScript version of IDD, this method is incorporated within the process parameter.
await crs.process.getValue(step.args.value1, context, process, item);
I shortened it above so that the code would not overflow the code block.
The same applies to the “setValue
” function.
To understand the parameters we need to first look at how this action will be executed.
JavaScript
const result = await crs.call(
"math", // type
"add", // action
{ value1: 10, value2: 20 }, // args
context, // execution context
process, // current process
item // item
);
JSON
{
"type": "math",
"action": "add",
"args": {
"value1": 10,
"value2": 20,
"target": "$data.sum"
}
}
There’s are distinct differences in how the JavaScript and JSON versions are executed. In the JSON version, a ‘target’ is defined. In this context, the target serves as a keyword, indicating where the action’s result will be stored, making it accessible for subsequent steps during their execution. It’s noteworthy that the target utilizes an expression syntax. While we will explore this in greater depth later, it’s important to understand for now that the process includes a data object, and the result is being saved to this data object under a property named ‘sum’.
In both scenarios, we specify the type of intent, referring to the name under which the module was registered on the intent object. Additionally, we determine the action to be performed on the intent type and outline the arguments that provide the step-related context needed by the action to execute its task.
The arguments you define are attached to the step object that is passed to the action. We consistently utilize the ‘getValue’ function to extract information from the arguments, as they might reference variables rather than direct values. For instance, if there’s another action intended to use the value stored in “$data.sum”, ‘getValue’ will assess and determine whether the input at “step.args.value1” is an actual value or a property path.
{
"type": "math",
"action": "add",
"args": {
"value1": "$data.sum",
"value2": 1,
"target": "$data.newValue"
}
}
In the JSON version, we invoke the action by specifying the “type”, “action”, and “args”. However, in the JavaScript version, we include additional parameters like context, process, and item. The execution in JSON is always part of a larger process, which means these values are automatically incorporated into the execution process. On the other hand, in JavaScript, we often use it as a manual, library-like step where there might not be an overarching context, process, or item. This depends on the scenario and whether the action requires those elements to be in place.
For instance, if you define the value using an expression, as done in the JSON, you need to ensure that the requirements are met. For example, if the action is called in JavaScript and the value is set to “$data.value1”, you are obliged to pass an object as the process parameter and make sure it has a “data” property defined. This ensures that the action can operate correctly within the given context.
const process = {
"data": {
"value1": 10
}
}
const result = await crs.call("math", "add", {
"value1": "$data.value1",
"value2": 10,
"target": "$data.sum"
}, null, process, null);
assert(process.data.sum === result)
In the provided example, we also add a ‘target’ property to the arguments, directing it to the ‘data’ object and a property named “sum”. Since the execution pipeline for both the JavaScript and JSON versions is identical, they function in exactly the same way. The sole distinction lies in the fact that the JSON version undergoes parsing first, and the intent is executed following this parsing process.
“getValue” and Value expressions
From the examples discussed earlier, it’s evident that an argument value can be either a direct value or a path. This differs slightly from binding expressions, as we are indicating to the step where to source the value from. The function ‘getValue’ focuses solely on the location of the actual value, rather than what is done with it.
There are a number of expression prefixes to take note of as they are also used in conditional expressions.
- $context – A property path that uses the context object as its root.
- $process – A property path with the process object as the starting point.
- $item – A property path rooted in the current loop item.
- $text – A quick reference to “process.text”.
- $data – A concise way to refer to “process.data”.
- $parameters – A shorthand for “process.parameters”.
- $bid – A brief notation for “process.parameters.bId”.
When using getValue you must pass in all the parameters.
- The desired value as specified in the arguments.
- The context object for use in expressions starting with “$context”.
- The process object applicable in “$process” expressions or similar shorthand notations.
- The item object utilized in “$item” expressions.
const value1 = await crs.process.getValue(step.args.value1, context, process, item);
“setValue”
The ‘setValue’ method follows the same guidelines as the ‘getValue’ method. The key difference is that you also provide the value to be set, in addition to all the other parameters.
const target = "$context.result";
const value = "test";
await crs.process.setValue(target, value, context, process, item);
In this instance, ‘step.args.target’ must conform to a value expression, as outlined in the “getValue” section. In this example save the value “test” to the “result” property on the context object.
Dynamic module loader
I can register a process api module by attaching the class to the intent object.
class MyModuleActions = {
...
}
crs.intent.module_name = MyModuleActions;
The issue here lies in the assumption that the system will automatically reference a particular module, which isn’t always the case in large-scale applications. The preferable approach is to initially register the module file with the module loader. Then, if a process references this module through an intent type, the system should verify if the module is already loaded. If it’s not, the system should load and register the module on the intent stack, but only at the point of necessity – effectively loading and registering it as needed.
// template
await crs.modules.add("name", `... path to file .../name-actions.js`);
// example
await crs.modules.add("action", `${root}/action-systems/action-actions.js`);
Schema registry
When schemas function as libraries for other schemas, they must be discoverable by the system. If you intend to use a schema through a schema intent type, the referred schema must be registered in the schema registry. Without this registration, the system will not recognize it as a callable schema.
The Process API features an object named “processSchemaRegistry,” where you can register your process schemas. When a schema is added, the system checks its ID and includes it in a lookup table. If you trigger an action on a different schema using the “process” intent type, this lookup table is consulted to locate the schema where that particular process is defined.
const schemaJSON = {
"id": "my_schema",
"utility_process": { ... process details ... }
}
crs.processSchemaRegistry.add(schemaJSON);
The ‘add’ method is indifferent to the origin or format of the dictionary it receives. Whether it’s directly a dictionary or a JSON obtained from a remote source, the method remains functional. After acquiring the JSON representation of the schema, simply use the ‘add’ method to register it within the registry.
When you are certain that a schema is no longer needed, you can remove it from the registry using the ‘remove’ method. This method requires a schema object as its parameter, where the object’s ID is used to identify the schema to be removed. If you have direct access to the schema instance, you can pass it directly to the method. However, if you only possess the schema’s ID, you must pass an object that includes the ID property.
const schemaJSON = {
"id": "my_schema",
"utility_process": { ... process details ... }
}
crs.processSchemaRegistry.remove(schemaJSON);
// or
crs.processSchemaRegistry.remove({ id: "my_schema" });
Running a process
I want to execute a process schema in my current context using JavaScript, and the process runner employs event aggregation for invocation. Given that the process API is one of the three foundational pillars, and one of these pillars is the binding engine, which possesses an event aggregation feature, we utilize this capability. By leveraging event aggregation, we instruct the process API to execute a schema, specify its parameters, and await its results.
crs.processSchemaRegistry.add(schema);
await crsbinding.events.emitter.emit("run-process", {
context: this,
step: {
action: "main",
args: {
schema: "my_schema"
}
},
parameters: {
bId : this._dataId
}
});
crs.processSchemaRegistry.remove(schema);
In this example you can see three distinct steps:
- Add the Schema(s) to the Registry: You should add the schema to the registry, which can be done as part of the same function call or at any point before executing the process during the application’s lifecycle. It’s important to remember that modules could be used for schema registry if they better suit your scenario.
- Request Schema Execution Using Event Aggregation: The next step is to initiate the execution of the schema. This is accomplished through event aggregation, which triggers the process associated with the schema.
- Remove the Schema(s) When Done: After the schema has served its purpose, consider removing it from the registry to conserve memory. This removal is context-dependent; if the schema is likely to be reused, it may be beneficial to keep it in the registry until it’s certain that it’s no longer needed, or until the end of the application lifecycle.
The aggregation definition object defines:
- Which context object is appropriate as the ‘context’ parameter in the actions?
- The schema and process to be executed are determined here, where we adopt the step structure for standardization. The action refers to the name of the process, and akin to the setup in the process intent type, the schema is specified as an argument property.
- The parameters that should be passed to the process are dictated by its “parameters_def”. In this situation, the required parameter is the binding context ID.