-
Notifications
You must be signed in to change notification settings - Fork 21
Basic Concepts
Workflow is a state machine. You can imagine it as a flow chart running inside of your application, and when it reaches a decision point, you can invoke a method to provide an answer that chooses the right path of execution from there. So workflow is a graph of control flow, and activities are its nodes. They can be as simple like an if..then..else branch, or as complex as getting data from a database and doing something complicated depending on the results.
A workflow is an application running inside of your application, with its state and variables, and correlated across Node.js cluster and process instances. So if your Node.js application consists of many instances inside a cluster and many clusters across a server farm, a workflow instance will work like a single instance application within those. A workflow application could outlive Node.js applications, they have out-of-the-box persistence support to make them the ideal platform to do long running business processes, durable services or scheduled background tasks.
There are excellent workflow framework implementations for each major platform, and one of the best is Microsoft Workflow Foundation for the .NET Framework. If you happen to read its introduction article then you will have a picture of what Workflow 4 Node is about, because it was inspired by MS WF.
In WF4Node a workflow is an instance of a running activity.
MS WF has its own markup language which is XML based. Workflow 4 Node has a markup language of course, but that's based on JSON. It took its inspiration from the excellent MongoDB Query language. So if you're familiar with writing MongoDB queries, then you will understand the concept of WF4Node's query language. MongoDB special keys start with $
character, for example $and
, Worflow's keys start with @
character, for example @if
:
{
"@if": {
"condition": "= this.a",
"then": {
"@console": [
"Hello!"
]
}
}
}
We will go in detail in the following chapters.
In Workflow 4 Node an activity is a functor. It has a body for doing some stuff by using a list of arguments. It can have captured variables (or properties) of own, and can access non private variables of outer scopes.
Let's see a very basic but essental activity! Every workflow can invoke arbitrary synchronous or asynchronous JavaScript code.
// Declare:
let engine = new ActivityExecutionEngine({
"@func": {
"args": ["World"]
"code": function (to) {
console.log(`Hello ${to}!`);
}
}
});
// Run:
engine.invoke()
.then(function () {
console.log("Done.");
});
// It prints:
// Hello World!
// Done.
We declare an ActivityExecutionEngine
instance with our workflow markup that describes only a single Func
activity. Func
have a property of code
that defines the method to execute. Every activity has a property of args
, which means an array of arguments must be passed for executing its body. Since Func
's body just executes code
, it takes argument list of args
, so it prints "Hello World!" when the engine gets invoked. Invoke starts the workflow and returns a promise of the result (more on that later).
We declared our workflow with a markup, but it could be done by regular JS constructors of course. So the equivalent of the above is:
// Declare
let func = new Func();
func.code = function (to) {
console.log(`Hello ${to}!`);
};
func.args = ["World"];
let engine = new ActivityExecutionEngine(func);
// Run:
engine.invoke()
.then(function () {
console.log("Done.");
});
// It prints:
// Hello World!
// Done.
External arguments
Workflows can take arguments externally by passing them as arguments of invoke. So the above could be:
// Declare
let func = new Func();
func.code = function (to) {
console.log(`Hello ${to}!`);
};
let engine = new ActivityExecutionEngine(func);
// Run:
engine.invoke("World")
.then(function () {
console.log("Done.");
});
// It prints:
// Hello World!
// Done.
Activities that inherit from the base class Declarator
have the capability of declaring arbitrary variables inside of a workflow. Built-in declarators are:
-
Block
: executes activities of its arguments sequentially (like MSWF'sSequence
). -
Parallel
: executes activities of its arguments in parallel (like MSWF'sParallel
). -
Pick
: executes activities of its arguments in parallel, and if one gets completed cancels the others (like MSWF'sPick
with implicit branches)
// Declare:
let engine = new ActivityExecutionEngine({
"@block": {
a: "Wor",
b: "rld",
args: {
"@func": {
"code": function () {
let to = this.a + this.b;
console.log(`Hello ${to}!`);
}
}
}
}
});
// Run:
engine.invoke()
.then(function () {
console.log("Done.");
});
// It prints:
// Hello World!
// Done.
So, what's that? Variables can be declared on block, there is a
and b
. Block body just executes its arguments sequentially. An activity's arguments are in the property of args
, so there is the func. In case there is only one argument, array can be omitted, so [ { "@func" : .. } ]
is written as { "@func" : .. }
. This time the Func
is not getting arguments, but since an activity is a functor, it can access variables of outer scope, so it can access a
and b
on this
.
That block could be declared by using JS contructors of course:
// Declare:
let block = new Block();
block.a = "Wor";
block.b = "rld";
block.args = new Func();
block.args.code = function () {
let to = this.a + this.b;
console.log(`Hello ${to}!`);
};
let engine = new ActivityExecutionEngine(block);
// Run:
engine.invoke()
.then(function () {
console.log("Done.");
});
// It prints:
// Hello World!
// Done.
Activities are like functors, so they have a result. In Func
simply return a value from the function, to have a result.
// Declare:
let engine = new ActivityExecutionEngine({
"@func": {
"code": function () {
return "Hello World!";
}
}
});
// Run:
engine.invoke()
.then(function (result) {
console.log(result);
});
// It prints:
// Hello World!
If there is an activity in place of a variable or an argument, it gets executed and its result will be used as the value of that variable or argument. It makes activity composition very easy and powerful.
// Declare:
let engine = new ActivityExecutionEngine({
"@block": {
a: {
"@func": {
code: function() {
return "Wo";
}
}
},
args: {
"@func": {
args: [
{
"@func": {
code: function() {
return "rld";
}
}
}
],
"code": function (b) {
let to = this.a + b;
console.log(`Hello ${to}!`);
}
}
}
}
});
// Run:
engine.invoke()
.then(function () {
console.log("Done.");
});
// It prints:
// Hello World!
// Done.
Every activity specifies its result. For example Block
returns its last argument value as its result, like:
// Declare:
let engine = new ActivityExecutionEngine({
"@block": {
a: 1,
args: [
{
"@func": {
code: function() {
this.a++;
}
}
},
{
"@func": {
code: function() {
return this.a;
}
}
}
]
}
});
// Run:
engine.invoke()
.then(function (result) {
console.log(result);
});
// It prints:
// 2
You can see that engine.invoke() is an asynchronous method despite all activities that we've used so far seems synchronous. Activities can be asynchronous, for example Func
is when its function result is a Promise (or something that's "thenable").
// Declare:
let engine = new ActivityExecutionEngine({
"@func": {
"code": function () {
return new Promise(function (resolve) {
setTimeout(
function () {
resolve("Hello World!")
},
1000);
});
}
}
});
// Run:
engine.invoke()
.then(function (result) {
console.log(result);
});
// It waits for a second then prints:
// Hello World!
ES6
If you require bluebird as Promise, then you'll have a Promise.delay(ms) method that returns a promise that resolves after ms milliseconds. This along with ES6 makes the above example far more readable:
let Promise = require("bluebird");
let async = Bluebird.coroutine;
// Declare:
let engine = new ActivityExecutionEngine({
"@func": {
"code": async(function* () {
yield Promise.delay(1000);
return "Hello World!";
})
}
});
// Run:
engine.invoke()
.then(function (result) {
console.log(result);
});
// It waits for a second then prints:
// Hello World!
Expression
is an activity that runs arbitrary JavaScript expressions in its body and returns that result.
let engine = new ActivityExecutionEngine({
"@func": {
args: {
"@expression": {
"expr": "'W' + 'o' + 'r' + 'd'"
}
},
code: function (to) {
return "Hello " + to + "!";
}
}
});
// Run:
engine.invoke()
.then(function (result) {
console.log(result);
});
// It prints:
// Hello World!
That of course doesn't seem too useable, but there is a special shortcut in WF4Node's markup for declaring expressions, it's the =
character. So it makes it easy to reference variables in the markup:
// Declare:
let engine = new ActivityExecutionEngine({
"@block": {
a: "Wo",
b: "rld",
args: {
"@func": {
args: [
"= this.a + this.b" // shortcut for making an Expression
],
code: function (to) {
console.log(`Hello ${to}!`);
}
}
}
}
});
// Run:
engine.invoke()
.then(function () {
console.log("Done.");
});
// It prints:
// Hello World!
// Done.
You can assign values to variables by using the Assign
activity. Variable name should go to its to
, and a value or an activity should go to its value
property. The activity will be evaluated before the assignment happens.
// Declare:
let engine = new ActivityExecutionEngine({
"@block": {
a: "a",
b: null,
args: [
{
"@assign": {
to: "b",
// this following is an Expression activity,
// that gets evaluated before the assignment happens
value: "= this.a"
}
},
"= this.b"
]
}
});
// Run:
engine.invoke()
.then(function (result) {
console.log(result);
});
// It prints:
// a
Shit happens. Exceptions could arise when workflow is executing. To deal with them, there is the Try
activity. It behaves like the Block
activity at first by executing its arguments sequentially. In case of an exception gets thrown, its catch
branch executes with a predefined variable e
containing the error. Its finally
branch gets executed after args if there wasn't an error, or after the catch
branch if there was.
In branches I mean a property by having a value of an activity or an array of activities. In case of an array, they execute sequentially like args.
// Declare:
let engine = new ActivityExecutionEngine({
"@block": {
x: null,
args: [
{
"@try": {
args: [
{
"@func": {
code: function () {
throw new Error("Foo.");
}
}
}
],
catch: {
"@func": {
code: function () {
// e contains the error
this.x = this.e.message;
}
}
},
finally: [
{
"@func": {
code: function () {
this.x += " Bar.";
}
}
}
]
}
},
"= this.x"
]
}
});
// Run:
engine.invoke()
.then(function (result) {
console.log(result);
});
// It prints:
// Foo. Bar.
Catch branch's e
variable's name redefineable by setting Try
's varName
propery.
// Declare:
let engine = new ActivityExecutionEngine({
"@try": {
varName: "err",
args: [
{
"@func": {
code: function () {
throw new Error("Foo.");
}
}
}
],
catch: {
"@func": {
code: function () {
// err contains the error
console.log(this.err.message);
}
}
}
}
});
// Run:
engine.invoke()
.then(function (result) {
console.log(result);
});
// It prints:
// Foo.
Throw
There is an activity for throwing exceptions: Throw
. It has an error
property, and when its value is evaluated to a string then new Error(<error>)
gets thrown, and when it evaluates to an instance of Error
, then that gets thrown.
{
"@throw": {
"error": "foo"
}
}
throws new Error("foo")
, and
{
"@throw": {
"error": "= new TypeError('Fosch Type.')"
}
}
throws new TypeError("Fosch Type.")
.
(Re)Throw
Caught errors could get rethrown by a parameterless Throw
, for example:
// Declare:
let engine = new ActivityExecutionEngine({
"@try": {
varName: "err",
args: [
{
"@func": {
code: function () {
throw new Error("Foo.");
}
}
}
],
catch: [
{
"@func": {
code: function () {
// err contains the error
console.log("1: " + this.err.message);
}
}
},
// means rethrown the caught exception, like in C#
{ "@throw": {} }
]
}
});
// Run:
engine.invoke()
.catch(function (err) {
console.log("2: " + err.message);
});
// It prints:
// 1: Foo.
// 2: Foo.
There are branches in Workflow 4 Node like those you can find in other languages. In this chapter in branches I mean a property by having a value of an activity or an array of activities. In case of an array they execute sequentially like args.
When If
activity's condition
property's value evaluates to true, the then
branch gets executed, to false else
branch executed. Either could be omitted. Activity's result value will be the executed branch's result.
let block = activityMarkup.parse({
"@block": {
v: 5,
args: [
{
"@if": {
condition: "= this.v == 5",
then: {
"@func": {
args: [1],
code: function (a) {
return a + this.v;
}
}
},
else: {
"@func": {
args: [2],
code: function (a) {
return a + this.v;
}
}
}
}
}
]
}
});
let engine = new ActivityExecutionEngine(block);
engine.invoke().then(
function (result) {
console.log(result);
});
.nodeify(done)
// It prints:
// 6
Switch
evaluates its expression
property and it executes Case
activity in its arguments if it holds the evaluated result in its value
property (that must be a constant, it won't get evaluated, so an activity won't work). If no Case
is found it executes Default
activity if any. Either Case
and Default
executes its arguments sequentially like Block
's. Activity's result value will be the executed Case
's or Default
's result.
Example:
let engine = new ActivityExecutionEngine({
"@switch": {
expression: "= 43",
args: [
{
"@case": {
value: 43,
args: function () {
return 55;
}
}
},
{
"@case": {
value: 42,
args: function () {
return "hi";
}
}
},
{
"@default": "= 'boo'"
}
]
}
});
engine.invoke().then(
function (result) {
console.log(result);
});
// It prints:
// 55
and:
let engine = new ActivityExecutionEngine({
"@switch": {
expression: "= 'aaa'",
args: [
{
"@case": {
value: 43,
args: function () {
return 55;
}
}
},
{
"@case": {
value: 42,
args: function () {
return "hi";
}
}
},
{
"@default": "= 'boo'"
}
]
}
});
engine.invoke().then(
function (result) {
console.log(result);
});
// It prints:
// boo
If Switch
doesn't have condition
defined, then it is considered a switch/when branch. It evaluates When
activities in its arguments sequentially until a When
's condition
evaluates as truthy. In this case When
's arguments get executed sequentially like Block
's. If a When
doesn't get executed and there is a Default
then it gets executed instead. Activity's result value will be the executed When
's or Default
's result.
Example:
let engine = new ActivityExecutionEngine({
"@switch": {
args: [
{
"@when": {
condition: 43,
args: function () {
return 55;
}
}
},
{
"@when": {
condition: undefined,
args: function () {
return "hi";
}
}
},
{
"@default": "= 'boo'"
}
]
}
});
engine.invoke().then(
function (result) {
console.log(result);
});
// It prints:
// 55
and:
let engine = new ActivityExecutionEngine({
"@switch": {
args: [
{
"@when": {
condition: "",
args: function () {
return 55;
}
}
},
{
"@when": {
condition: null,
args: function () {
return "hi";
}
}
},
{
"@default": "= 'boo'"
}
]
}
});
engine.invoke().then(
function (result) {
console.log(result);
});
// It prints:
// boo
At this point you already know expression shortcut syntax, but there are other interesting features available to make activity composition efficient a fun.
If any string value of a markup is a valid JavaScript function, then that gets parsed as a function. That feature is essential to have the ability to define activities in plain JSON format.
{
"@func": {
"code": "function() { ... }"
}
}
If there is a function or function string in a place where could be an activity, then that will be considered as a Func
activity containing the function in its code property.
So this:
{
"@block": {
"a": 1,
"args": [
"function() { return this.a++; }",
"function() { return this.a++; }",
"= this.a"
]
}
}
is equivalent to this:
{
"@block": {
"a": 1,
"args": [
{
"@func": {
"code": "function() { return this.a++; }"
}
},
{
"@func": {
"code": "function() { return this.a++; }"
}
},
"= this.a"
]
}
}
If you declare an activity that has nothing else defined other than args, you can use the args shortcut syntax:
{
"@block": [
"function() { console.log(1); }",
"function() { console.log(2); }"
]
}
There is a shortcut for Assign
. If any activity has a string property named @to
in the markup, then activity's result will be assigned in the variable named @to
's value.
So this:
{
"@block": {
"a": 1,
"b": null,
"args": [
{
"@func": {
"code": "function() { return this.a; }",
"@to": "b"
}
},
"= this.b"
]
}
}
is equivalent with this:
{
"@block": {
"a": 1,
"b": null,
"args": [
{
"@assign": {
"to": "b",
"value": {
"@func": {
"code": "function() { return this.a; }"
}
}
}
},
"= this.b"
]
}
}
Templating is a powerful feature. In a place of a value there can be a plain old JS object. But that can have internal activities those get executed to make their result part of the JS object. An example will make that explanation more understandable:
// Declare:
let engine = new ActivityExecutionEngine({
"@block": [
{
a: {
b: function () {
return Promise.delay(100)
.then(function () {
return {
c: 1
}
});
},
d: [
{e: "= 1+1"},
function () {
return Promise.delay(10)
.then(function () {
return {
f: 1
}
});
}
]
}
}
]
});
// Run:
engine.invoke()
.then(function (result) {
console.log(JSON.stringify(result));
});
// It prints:
//{
// "a": {
// "b": {
// "c": 1
// },
// "d": [
// {"e": 2},
// {"f": 1}
// ]
// }
//}
WTF is this, and WTF is this for?
So, there is a block, that executes activities of its args. We're using the args shortcut syntax, so there is one argument, but that's not an activity just an object. Since it has two Func shortcut activities defined, it is considered a template. A template executes its activity parts in parallel, so that two asynchronous functions get executed. After a while they return their values, and if all activities finished inside a template, the object will be constructed by using the results.
But what is this for? Imagine the above, but some MongoDB database query operations instead of those functions, building a JS object that defines a MongoDB query, or update or aggregate operation! You'll get a powerful query and update composition framework, something like SSIS os SQL Agent for Ms SQL Server. Well, that exactly the purpose of my upcoming mongo-crunch module.
If you want that a part of your markup should be considered as is without applying any shortcut or template magic, then use the following syntax:
{
"_": "= this.stringNotAnExpression"
}
means:
"= this.stringNotAnExpression"
and
{
"_": {
"this": "will",
"be": "left",
"as": "this",
"object": "= and this will be a string not an expression"
}
}
means:
{
"this": "will",
"be": "left",
"as": "this",
"object": "= and this will be a string not an expression"
}