Skip to content
D. Rep edited this page Oct 18, 2017 · 13 revisions

TOC

1. Workflows and Activities

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.

2. Markup

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.

3. Activity concepts

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.

3.1. Func

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.

3.2. Declarators

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's Sequence).
  • Parallel: executes activities of its arguments in parallel (like MSWF's Parallel).
  • Pick: executes activities of its arguments in parallel, and if one gets completed cancels the others (like MSWF's Pick 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.

3.3. Results and Composition

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

3.4. Asynchronity

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!

3.5. Expressions

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.

3.6.Assignments

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

3.7. Errors (Try/catch/finally/Throw)

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.

3.8 Branches (If/then/else/Switch/Case/When/Default)

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.

3.8.1. If/then/else

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

3.8.2. Switch/Case/Default

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

3.8.2. Switch/When/Default

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

4. More on markup

At this point you already know expression shortcut syntax, but there are other interesting features available to make activity composition efficient a fun.

4.1. function string

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() { ... }"
  }
}

4.2. Func shortcut

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"
    ]
  }
}

4.3. args shortcut

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); }"
  ]
}

4.4. @to

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"
    ]
  }
}

4.5. Templates

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.

4.6. Escaping

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"
}