Zenaton is a tool designed to greatly simplify execution and management of long running tasks. You can see it as a much simpler but more powerful version of Airflow. To demonstrate how Zenaton works, we will focus on a simple but reasonably realistic use case: a workflow to collect rent from an apartment tenant. It goes like this:

  • Send an email to remind the tenant to pay the rent.
  • Wait for the payment for up to two weeks.
  • If no payment has been received, notify the apartment’s owner.

Introduction

The Zenaton stack revolves around two entities: the workflow and the tasks.

You can write a task with your favorite programming language, like Javascript, as:

const { Task } = require("zenaton");

module.exports = Task("SimpleTask", async function () {
  return 42;
});

This one is very simple, but it could contain much more: calling databases, sending emails, logging… You name it.

Workflows are like scenarios that describe how tasks must be orchestrated: in what order, under which conditions… To define a workflow, Airflow asks you to define a Directed Acyclic Graph. To be easier and more versatile, Zenaton lets you directly code it in your favorite language.

const { Workflow } = require("zenaton");
const SimpleTask = require("./most-simple-zenaton-task");

module.exports = Workflow("SimpleWorkflow", async function () {
  await new SimpleTask().dispatch();
});

Zenaton has many advantages for the developer:

  • Simplified infrastructure. You no longer carry the burden of managing a job scheduler and a message broker. Everything is handled and hosted for you. You just have to code.
  • Out-of-the-box monitoring, with the capability to retry failed tasks with a simple button click, as well as pause, resume or kill your workflows at will.
  • No weird DSL. You can program your business logic using your favorite language: javascript, PHP, Python, Ruby (soon .NET and Java).

curiosity attention

Our use case

For our example, the workflow can be simply written as:

const { Workflow, Wait } = require("zenaton");

Workflow("CollectRent", {
  init(data) {
    this.data = data;
  },
  id() {
    const { tenantId, placeId, month, year } = this.data;
    return `TENANT#${tenantId}-PLACE#${placeId}-MONTH#${month}-YEAR#${year}`;
  },
  async handle() {
    await new AskForRentPayment(this.data).dispatch();

    const event = await new Wait("RentPaid").weeks(2).execute();
    if (event) {
      await new SlackPayment(this.data).dispatch();
      return;
    }

    await new NotifyOwnerOfNonPayment(this.data).dispatch();
  }
});
  • id method lets you define an id for your workflow. Here it is a custom composite built with the tenant id, the place id (our apartment), a month and a year. This will allow us to retrieve it very easily, which will be important later.
  • init method lets you provide data specific to a workflow instance (see below).
  • handle method describes the tasks orchestration. Zenaton provides a Wait function to manage time-sensitive executions. Here, the workflow is waiting for a “RentPaid” event, but for no more than 2 weeks. AskForRentPayment,NotifyOwnerOfNonPayment and SlackPayment are tasks that do what their name describes.

simple as zenaton

To start a new instance of this workflow, you just have to dispatch it:

new CollectRent({
  tenantId: "000000000",
  placeId: "000000000",
  month: 11,
  year: 2018,
}).dispatch();

The interesting part in this workflow is the ability to wait for two weeks (without a scheduler or cron job!) for a RentPaid event that can be emitted at any time and induce the execution of the rest of the workflow. For example in an Express middleware:

const express = require("express");
const CollectRent = require("./workflows/collect-rent");

const app = express();

app.post("/pay-rent/:placeId/:tenantId", (req, res, next) => {
  const { placeId, tenantId } = req.params;
  const { month, year } = req.query;
  
  const workflowInstanceId = `TENANT#${tenantId}-PLACE#${placeId}-MONTH#${month}-YEAR#${year}`;
  
  CollectRent
    .whereId(workflowInstanceId)
    .send("RentPaid")
    .then(() => {
      res.sendStatus(200);    
    }, (err) => {
      next(err);
    });
});

Following an HTTP call informing us that the rent has been paid, we emit a RentPaid event to the corresponding instance of RentWorkflow. The workflow will then resume its execution and end.

blew your mind

Conclusion

The example shown here is very simple, and Zenaton allows for much more convoluted and complex business logic. You can find all the information required to jump aboard in the documentation.

If you want to play with the code and experiment with the Zenaton API, you can download the full implementation here.

You can also download more examples from the NodeJS Zenaton GitHub example repository (available in other languages as well).

If you have any questions, feel free to contact me or the Zenaton team at any time.