Sample Projects

We've created a few sample projects on github that allow you to browse through a real project and which you can deploy yourself to see how Zenaton works. The Workflows respresent some of our basic processes shown in the real world examples and the Tasks are already coded. You can clone the repo from github and launch it locally from your computer. You will need to have a Zenaton account and have installed the agent so that you can run the code and see your results on the dashboard.

New User Activation Emails: Wait for events and trigger emails

View our sample project (available in Node.js ) on Github here

This workflow introduces the wait, wait.event, connector and execute Methods.

We've put together a sample project or recipe with a Zenaton workflow that triggers emails within a web app when the user completes actions. Our example is based on a Zenaton new user journey but can easily be configured to send emails and trigger actions based on any events within an application or external services.

Workflow Logic

When a new user registers, we start a new "NewUserWorkflow instance for that user.
This workflow will wait for events and send emails when the events happen. The workflow can run for days, months or even longer if the events we are waiting for happen later.

In our sample web_app, we send 2 events to our workflow to trigger activities ('installed agent', and 'ran first workflow').

We use several Zenaton functions in our workflow such as 'wait for event' and 'wait for duration' and connectors to manage the authentication for sendgrid and Airtable.
The github repository is split into two directories:

  • web_app: A simple NodeJS web app where we launch the workflow instance per user and send events when the user completes each action
  • worker: Runs our workflow and tasks and receives the events for each workflow instance.

Web_App UI for testing

We've provided a simple UI in the web_app that allows you to simulate the user events (register and install agent or launch workflow) to trigger actions in the workflow and see the corresponding tasks executions on your Zenaton dashboard:

Three workflow versions to send emails

We have provided 3 variants of this workflow depending on how we want to send the emails. We've used the Zenaton Sendgrid API connector but you can also build your own connector to other mail services.

  • Basic Version: send text email using sendgrid
  • Template with Conditional Blocks: send email by triggering the sendgrid dynamic template feature that segments sections of the email based on input variables.
  • Hybrid: Send email via sendgrid and pull dynamic text from airtable database dependent on variables. This would allow non-technical people to easily edit the text content using AirTable. You could change this to pull from any database.

Deploy Workflow on Heroku

To deploy the basic workflow version that send text email using sendgrid, you will need to:

  • Set up your SendGrid api connector on the Zenaton dashboard using our guide
  • Fork the repo so that you can easily change the variables and logic within the workflow and see the impact
  • Deploy it using this button Deploy .We also have other deployment options

Then you can start to use it:

  • Click on 'Register' to simulate signup which launches the workflow instance for a user
  • Click on the 'installation' and 'activation' buttons to simulate sending events to the running workflow for that user

Note: you can also change the 'wait' duration to 5 seconds to speed up your testing experience.

Decide where to lunch with your teammates on Slack

View the sample project in Node.js on Github

This is a fun project that you can quickly deploy and use for your team or make modifications to the workflow to suit your whims.

This workflow introduces the wait, event and execute Methods.

The workflow Where To Lunch is a friendly slack bot to recommend restaurants and organize a daily vote for nearby lunch options.

  • Automatically post a list of nearby restaurants every day
  • Invite coworkers to vote on their preference
  • Change the vote time and window to suite your schedule
  • Go to Lunch!

The Where-to Lunch-project is composed of two workflows. The configureWorkflow.js launches when the location is configured. It will fetch places using Google Place API and store the result in Airtable. The second workflow manages the vote process within the slack channel, reacting to user votes and announces the winner when the voting window ends. For the moment, the code is available in Node.jshere.

  • configureWorkflow.js to set the current location and fetch places
  • proposePlaceAndCollectVotes.js to manage the vote process

Schedule the workflow

Zenaton provides a scheduler. Therefore, it's easy to schedule the vote for the restaurants every day at special time. You have access to your tasks or workflows schedules in your dashboard. You can pause, resume or kill them. More information are in the documentation here.

It's also possible to schedule the vote directly from Slack using the command /wheretolunch schedule_vote 15 11 * * MON-FRI using cron expressions.

Deploy and install your own instance

To deploy your own instance, you will need a:

  • Zenaton account (the workflow engine)
  • Airtable account (the database)
  • Google Places API key (to find places near your location)
  • Slack application

Then, you can follow the steps described on the readme in Github. The code can be quickly deployed on Heroku or you can use another cloud provider (see going to production).

Triggering An Email After 3 Days of Cold Weather

View our sample project (only available in Python) on Github here

This workflow introduces the wait, connector and execute Methods.

This example shows how to create a workflow that triggers a promotional email about a tropical vacation after 3 days of cold weather. It can be useful for an agency or anyone who wants to trigger notification based on data from an external API and time factors.

  • Call a weather API to get the temperature of the current day during a specific period (ie. number of days)
  • If the temperature is less than 40°F for three days in a row, then send an email to users using gmail.
  • If not, then keep checking everyday until the end of the time period
  • If during the time period, the temperature hasn't dropped below 40°F for 3 days, then send a different promotional email.

Flowchart of the workflow

This flowchart shows a visual representation of the workflow tasks.

Flowchart of triggering emails weather

Workflow Code

This workflow is the code that orchestrates tasks (through the Zenaton workflow engine) and executes them on your servers. Tasks will be dispatched as soon as three days of cold weather occurs.

trigger_emails_weather.py

<?php
  
use Zenaton\Interfaces\WorkflowInterface;
use Zenaton\Tasks\Wait;
use Zenaton\Traits\Zenatonable;

class TemperatureCampaignWorkflow implements WorkflowInterface
{
    use Zenatonable;

    public function __construct($days, $minTemp, $minRep, $city, $emailRecipients)
    {
        $this->days = $days;
        $this->minTemp = $minTemp;
        $this->minRep = $minRep;
        $this->city = $city;
        $this->emailRecipients = $emailRecipients;
    }

    public function handle()
    {
        $repCount = 0;

        do {
            $currentTemp = (new CheckTemperatureTask($this->city))->execute();
            if ($currentTemp < $this->minTemp) {
                ++$repCount;
            } else {
                $repCount = 0;
            }

            (new Wait())->days(1)->execute();

            --$this->days;
        } while ($repCount < $this->minRep && $this->days > 0);

        if ($repCount === $this->minRep) {
            (new SendEmailCampaign($this->emailRecipients, $this->city))->execute();
        } else {
            (new SendAnotherEmailCampaign($this->emailRecipients, $this->city))->execute();
        }
    }
}
    
const { Workflow, Wait } = require('zenaton')
const CheckTemperature = require('../Tasks/CheckTemperature')
const SendEmailCampaign = require('../Tasks/SendEmailCampaign')
const SendAnotherEmailCampaign = require('../Tasks/SendAnotherEmailCampaign')

module.exports = Workflow('TemperatureCampaignWorkflow', {
  init (days, minTemp, minRep, city, emailRecipients) {
    this.days = days
    this.minTemp = minTemp
    this.minRep = minRep
    this.city = city
    this.emailRecipients = emailRecipients
  },

  async handle () {
    let repCount = 0
    do {
      if ((await new CheckTemperature(this.city).execute()) < this.minTemp) {
        repCount++
      } else {
        repCount = 0
      }
      Wait().days(1).execute()
      this.days--
    } while (repCount < this.minRep && this.days > 0)

    if (repCount === this.minRep) {
      await new SendEmailCampaign(this.emailRecipients, this.city).execute()
    } else {
      await new SendAnotherEmailCampaign(this.emailRecipients,this.city).execute()
    }
  }
})
const { workflow, duration } = require(‘zenaton’)

// Check the temperature of a given city every day for n days.
// When the temperature drops below minTemp for minRep then send a promotional email via Maichimp
// to visit a warm weather destination. Otherwise send a regular email campaign.
module.exports = workflow('TemperatureCampaignWorkflow', {
 *handle(days, minTemp, minRep, city, emailRecipients) {
 
   // You can get your connectors id here: https://app.zenaton.com/connectors
   const openweathermap = this.connector('openweathermap', 'your-connector-id')
   const mailchimp = this.connector('mailchimp', 'your-connector-id')
   
   let repCount = 0
   do {
     //Check the temperature of Paris via openweathermap API.
     response = yield openweathermap.get(`/weather?q=${city}&units=imperial`)
     //If the temperature is under minTemp degrees (42 degrees fahrenheit) for at least minRep(3 days),
     if (response.main.temp < minTemp) {
       repCount++
     } else {
       repCount = 0
     }
     //wait one day
     yield this.wait.for(duration.days(1));
     days--
   } while (repCount < minRep && days > 0)
   
   //trigger an email campaign on mailchimp based on the weather.
   if (repCount === minRep) {
     yield mailchimp.post(`/campaigns/campaign_${city}_hot/actions/send`)
   } else {
     yield mailchimp.post(`/campaigns/campaign_${city}_cold/actions/send`)
   }
}})
require "./tasks/send_email_campaign"
require "./tasks/check_temperature"
require "./tasks/send_another_email_campaign"

class TemperatureCampaignWorkflow < Zenaton::Interfaces::Workflow
  include Zenaton::Traits::Zenatonable

  def initialize(days, min_temp, min_rep, city, email_recipients)
    @days = days
    @min_temp = min_temp
    @min_rep = min_rep
    @city = city
    @email_recipients = email_recipients
  end

  def handle
    rep_count = 0
    loop do
      if CheckTemperature.new(@city).execute < @min_temp
        rep_count += 1
      else
        rep_count = 0
      end

      Zenaton::Tasks::Wait.new.days(1).execute
      @days -= 1

      break if rep_count == @min_rep || @days == 0
    end

    if rep_count == @min_rep
      SendEmailCampaign.new(@email_recipients, @city).execute
    else
      SendAnotherEmailCampaign.new(@email_recipients, @city).execute
    end
  end
end
    
from tasks.check_temperature import CheckTemperature
from tasks.send_email_campaign import SendEmailCampaign
from tasks.send_another_email_campaign import SendAnotherEmailCampaign

from zenaton.abstracts.workflow import Workflow
from zenaton.traits.zenatonable import Zenatonable
from zenaton.tasks.wait import Wait


class TemperatureCampaignWorkflow(Workflow, Zenatonable):

    def __init__(self, days, min_temp, min_rep, city, email_recipients):
        self.days = days
        self.min_temp = min_temp
        self.min_rep = min_rep
        self.city = city
        self.email_recipients = email_recipients

    def handle(self):
        rep_count = 0
        for _ in range(self.days):
            if CheckTemperature(city=self.city).execute() < self.min_temp:
                rep_count += 1
            else:
                rep_count = 0
            if rep_count == self.min_rep:
                SendEmailCampaign(self.email_recipients, self.city).execute()
                break
            Wait().days(1).execute()
        else:
            SendAnotherEmailCampaign(self.email_recipients, self.city).execute()
            pass
    

Automatic Retry on OpenWeather API

This workflow calls the OpenWeather API to get the temperature of a selected city. However, the api call an fail due to a timeout or rate-limit so we will implement an automatic retry in order to relaunch the task if it fails. If this happens, Zenaton will send an email alert that the task has failed. By automatically retrying the task, the task can be processed on the next try, the states are maintained and the workflow can still be completed.

In the example below, the automatic retry is coded inside the CheckTemperature task. In the example, it will retry the task 3 times spaced one minute apart.

Note that an automatic retry can only be implemented inside of a task and is not applicable to a workflow.

check_temperature.py

<?php

use Zenaton\Interfaces\TaskInterface;
use Zenaton\Traits\Zenatonable;

class CheckTemperature implements TaskInterface
{
    use Zenatonable;

    public function __construct($city)
    {
        $this->city = $city;
    }

    public function handle()
    {
        // [...] OpenWeather API call
    }

    public function onErrorRetryDelay($exception)
    {
        // The retry index starts at 1 and increases by one for every retry.
        // This can be used to to increase the time between each attempt.
        $n = $this->getContext()->getRetryIndex();
        if ($n > 3) {
            return false;
        }

        return $n * 60;
    }
}
              
const { Task } = require("zenaton");

module.exports = Task("CheckTemperature", {

    init(city) {
        this.city = city;
    },

    async handle() {
        // [...] OpenWeather API call
    },

    onErrorRetryDelay(exception) {
        // The retry index starts at 1 and increases by one for every retry.
        // This can be used to to increase the time between each attempt.
        const n = this.context.retryIndex;
        if (n > 3) {
            return false;
        }

        return n * 60;
    }
});
              
const { task } = require("zenaton");

module.exports = task("CheckTemperature", {

    async handle(city) {
        // [...] OpenWeather API call
    },

    onErrorRetryDelay(exception) {
        // The retry index starts at 1 and increases by one for every retry.
        // This can be used to to increase the time between each attempt.
        const n = this.context.retryIndex;
        if (n > 3) {
            return false;
        }

        return n * 60;
    }
});
              
class CheckTemperature < Zenaton::Interfaces::Task
    include Zenaton::Traits::Zenatonable

    def initialize(city)
      @city = city
    end

    def handle
        # [...] OpenWeather API call
    end

    def on_error_retry_delay(exception)
        # The retry index starts at 1 and increases by one for every retry.
        # This can be used to to increase the time between each attempt.
        n = @context.retry_index
        if n > 3
            false
        else
            n * 60
        end
    end
end
              
from zenaton.abstracts.task import Task
from zenaton.traits.zenatonable import Zenatonable

class CheckTemperature(Task, Zenatonable):

    def __init__(self, city):
        self.city = city

    def handle(self):
        # [...] OpenWeather API call

    def on_error_retry_delay(self, exception):
        # The retry index starts at 1 and increases by one for every retry.
        # This can be used to to increase the time between each attempt.
        n = self._context.retry_index
        if n > 3:
            return False

        return n * 60
              

For more information about automatic retry, you check the documentation here.

Amazon Dash Button Workflow

View the sample project (only available in PHP) on Github here

This workflow introduces the wait, event, connector and execute Methods.

This example shows how to create a workflow similar to an Amazon Dash Button. The customer orders an item and their credit card on file is charged. Or, if their payment method on file is not current, they are prompted to update their payment information and if they do not, the order is cancelled.

  • Charge customer for the order processed.
  • If the payment information is not up to date, then ask the customer to update their payment details.
  • If payment details are not updated within two weeks, then cancel the order
  • Send customer an invoice when their payment is processed and send the order to shipping

Flowchart of the workflow

This flowchart helps visualize different tasks within the workflow.

Workflow Code

This workflow is the code that orchestrates tasks (through the Zenaton workflow engine) and executes them on your servers. Tasks will be dispatched as soon as the user presses the button to order something.

amazon_dash_button.py

<?php
  
use App\Events\OrderPaid;
use App\Order;
use App\Tasks\AskForNewPaymentDetails;
use App\Tasks\CancelOrder;
use App\Tasks\ChargeCustomerForOrder;
use App\Tasks\SendOrderInvoice;
use App\Tasks\SendOrderToShipping;
use Zenaton\Interfaces\WorkflowInterface;
use Zenaton\Tasks\Wait;
use Zenaton\Traits\Zenatonable;
final class OrderFromDashButton implements WorkflowInterface
{
    use Zenatonable;
    private $order;
    public function __construct(Order $order)
    {
        $this->order = $order;
    }
    public function handle()
    {
        $charged = (new ChargeCustomerForOrder($this->order))->execute();
        $event = null;
        if (!$charged) {
            (new AskForNewPaymentDetails($this->order))->dispatch();
            $event = (new Wait(OrderPaid::class))->days(14)->execute();
        }
        if ($charged || $event) {
            (new SendOrderInvoice($this->order))->dispatch();
            (new SendOrderToShipping($this->order))->dispatch();
        } else {
            (new CancelOrder($this->order))->dispatch();
        }
    }
    public function getId()
    {
        return $this->order->id;
    }
}
const { Wait, Workflow } = require("zenaton");
const AskForNewPaymentDetails = require("../Tasks/AskForNewPaymentDetails");
const ChargeCustomerForOrder = require("../Tasks/ChargeCustomerForOrder");
const SendOrderInvoice = require("../Tasks/SendOrderInvoice");
const SendOrderToShipping = require("../Tasks/SendOrderToShipping");

module.exports = Workflow("OrderFromDashButton", {
  init (order) {
    this.order = order;
  },
  id () {
    return this.order.id;
  },
  async handle () {
    const order = this.order
    const charged = await new ChargeCustomerForOrder(this.order).execute();
    let event = null;

    if (!charged) {
      await new AskForNewPaymentDetails(this.order).dispatch();
      event = await new Wait("OrderPaid").seconds(14).execute();
    }

    if (charged || event) {
      await new SendOrderInvoice(this.order).dispatch();
      await new SendOrderToShipping(this.order).dispatch();
    } else {
      await new CancelOrder(this.order).dispatch();
    }
  }
});
    
const { workflow, duration } = require("zenaton");

module.exports = workflow("OrderFromDashButton", {
  *handle(order) {
    const stripe = this.connector(
      'stripe',
      'your-stripe-connector-id-from-zenaton-dashboard'
    );
    const quickbooks = this.connector(
      'quickbooks',
      'your-quickbooks-connector-id-from-zenaton-dashboard'
    );

    const charge = yield stripe.post(`/v1/charges`, {amount: order.total});
    const capture_response = yield stripe.post(`/v1/charges/${charge.id}/capture`);
    const charged = capture_response.captured

    let event = null;

    if (!charged) {
      this.run.task("AskForNewPaymentDetails", order);
      event = yield this.wait.event("OrderPaid").for(duration.weeks(2));
    }

    if (charged || event) {
      const realmID = order.realmID
      const email = order.email

      // Create an invoice
      const invoice_lines = this.invoice_lines(order);
      const invoice = yield quickbooks.post(`/v3/company/${realmID}/invoice`, invoice_lines);
      
      // Send the invoice
      yield quickbooks.post(`/v3/company/${realmID}/invoice/${invoice.Invoice.Id}/send?sendTo=${email}`);

      this.run.task("SendOrderToShipping", order);
    } else {
      this.run.task("CancelOrder", order);
    }
  },
  invoice_lines: function(order) {
    // ...
  }
});
require "./events/order_paid"
require "./order"
require "./tasks/ask_for_new_payment_details"
require "./tasks/cancel_order"
require "./tasks/charge_customer_for_order"
require "./tasks/send_order_invoice"
require "./tasks/send_order_to_shipping"
require "zenaton"

class OrderFromDashButton < Zenaton::Interfaces::Workflow
  include Zenaton::Traits::Zenatonable

  def initialize(order)
    @order = order
  end

  def id
    @order.id
  end

  def handle
    charged = ChargeCustomerForOrder.new(@order).execute
    event = nil

    unless charged
      AskForNewPaymentDetails.new(@order).dispatch
      event = Zenaton::Tasks::Wait.new(OrderPaid).days(14).execute
    end

    if charged || event
      SendOrderInvoice.new(@order).dispatch
      SendOrderToShipping.new(@order).dispatch
    else
      CancelOrder.new(@order).dispatch
    end
  end
end
from zenaton.abstracts.workflow import Workflow
from zenaton.traits.zenatonable import Zenatonable
from events.order_paid import OrderPaid
from tasks.ask_for_new_payment_details import AskForNewPaymentDetails
from tasks.cancel_order import CancelOrder
from tasks.charge_customer_for_order import ChargeCustomerForOrder
from tasks.send_order_invoice import SendOrderInvoice
from tasks.send_order_to_shipping import SendOrderToShipping

class OrderFromDashButton(Workflow, Zenatonable):
    def __init__(order):
        self.order = order
    def handle(self):
    
      charged = ChargeCustomerForOrder(self.order).execute()
      
      event = None

      if not charged:
          AskForNewPaymentDetails(self.order).dispatch()
          event = Wait(OrderPaid).days(14).execute()
      if charged or event:
          SendOrderInvoice(self.orderh).dispatch()
          SendOrderInvoice(self.orderh).dispatch()
      else:
          CancelOrder(self.order).dispatch()   
    

Automatic Retry on payment API

This workflow calls a Payment Service Provider (psp). This API call can fail and an automatic retry could be used to automatically relaunch the task. In this case, an alerting email would be sent letting us know that the task failed and the workflow will be completed.

We have included a sample task below showing what it would look like to code the automatic retry inside the ChargeCustomerForOrder task. In the example, if the task fails, it will retry the task 3 times spaced one minute apart.

Note that automatic retry can be implemented inside a task but is not applicable for a workflow.

charge_customer_for_order.py

<?php

use Zenaton\Interfaces\TaskInterface;
use Zenaton\Traits\Zenatonable;

class ChargeCustomerForOrder implements TaskInterface
{
    use Zenatonable;

    private $order;

    public function __construct($order)
    {
        $this->order = $order;
    }

    public function handle()
    {
        // [...] Payment Service Provider API call
    }

    public function onErrorRetryDelay($exception)
    {
        // The retry index starts at 1 and increases by one for every retry.
        // This can be used to to increase the time between each attempt.
        $n = $this->getContext()->getRetryIndex();
        if ($n > 3) {
            return false;
        }

        return $n * 60;
    }
}
              
const { Task } = require("zenaton");

module.exports = Task("ChargeCustomerForOrder", {

    async handle() {
        // [...] Payment Service Provider API call
    },

    onErrorRetryDelay(exception) {
        // The retry index starts at 1 and increases by one for every retry.
        // This can be used to to increase the time between each attempt.
        const n = this.context.retryIndex;
        if (n > 3) {
            return false;
        }

        return n * 60;
    }
});
              
const { task } = require("zenaton");

module.exports = task("ChargeCustomerForOrder", {

    async handle() {
        // [...] Payment Service Provider API call
    },

    onErrorRetryDelay(exception) {
        // The retry index starts at 1 and increases by one for every retry.
        // This can be used to to increase the time between each attempt.
        const n = this.context.retryIndex;
        if (n > 3) {
            return false;
        }

        return n * 60;
    }
});
              
class ChargeCustomerForOrder < Zenaton::Interfaces::Task
    include Zenaton::Traits::Zenatonable

    def handle
        # [...] Payment Service Provider API call
    end

    def on_error_retry_delay(exception)
        # The retry index starts at 1 and increases by one for every retry.
        # This can be used to to increase the time between each attempt.
        n = @context.retry_index
        if n > 3
            false
        else
            n * 60
        end
    end
end
              
from zenaton.abstracts.task import Task
from zenaton.traits.zenatonable import Zenatonable

class ChargeCustomerForOrder(Task, Zenatonable):

    def handle(self):
        # [...] Payment Service Provider API call

    def on_error_retry_delay(self, exception):
        # The retry index starts at 1 and increases by one for every retry.
        # This can be used to to increase the time between each attempt.
        n = self._context.retry_index
        if n > 3:
            return False

        return n * 60
              

Automatic Retry on Zenaton Dashboard

View the Zenaton dashboard to see historical data for each task:

  • When does it retry ?
  • When does it fail ?
  • How long does it take to be processed ?

automatic retry payment API call

For more information about automatic retry, you check the documentation here.