Workflow basics


The only requirements to write a workflow are:

  • Implementing Zenaton\Interfaces\WorkflowInterface that requires only a handle method that will be called to run the workflow;
  • Using Zenaton\Traits\Zenatonable trait, that defines dispatch and execute methods
  • Using the provided Workflow function;
  • Inheriting from Zenaton::Interfaces::Workflow that requires only a handle method that will be called to run the workflow;
  • Including Zenaton::Traits::Zenatonable module, that defines dispatch and execute methods
  • Inheriting from Zenaton.abstracts.workflow.Workflow class that requires only a handle method that will be called to run the workflow;
  • Inheriting from zenaton.traits.zenatonable.Zenatonable class, that defines dispatch and execute methods
  • Using either of the provided functions workflow.New or workflow.NewCustom;
  • Being idempotent. In more practical terms, it means it must implement a logical flow and NOT the tasks themselves.

Idempotence implies that any actions (such as requesting a database, writing/reading a file, using current time, sending an email, echoing in console, etc.) that have side effects or that need access to potentially changing information MUST be done within tasks (not from within workflows).

As Zenaton engine triggers the execution of the class describing a workflow each time it has to decide what to do next, failing to follow the idempotence requirement will lead to multiple executions of actions wrongly present in it.

The provided methods execute and dispatch are internally implemented to ensure idempotency.


use Zenaton\Interfaces\WorkflowInterface;
use Zenaton\Traits\Zenatonable;

class WelcomeFlow implements WorkflowInterface
    use Zenatonable;

    protected $user

    public function __construct(User $user)
        $this->email = $user->email;
        $this->slackId = $user->slackId;

    public function handle()
        (new SendWelcomeEmail($this->email))->execute();
        (new IntroduceUserThroughSlack($this->slackId))->execute();

There are two ways to create a workflow. Both of them require implementing the Handler interface. The Handler interface has one required method: func Handle() (interface{}, error).

  • The simpler way to define a workflow is to call the workflow.New() function. You must provide a name and a handle function of the form: func () (interface{}, error). Under the hood we create a Handler Interface for you using the provided function. For example:
import ""

var SimpleWorkflow = workflow.New("SimpleWorkflow",
func() (interface{}, error) {
... // business logic of the workflow
  • If you want to implement the Handler Interface on your own, you must call the workflow.NewCustom() function. This function takes a name and an instance of your type that has a Handle method. You can also optionally provide an Init method that takes any number and type of arguments and initializes the workflow with data. For example:
import ""

var WelcomeWorkflow = workflow.NewCustom("WelcomeWorkflow", &Welcome{})

type Welcome struct {
    //Fields must be exported, as they will need to be serialized
    Email string
    SlackID string

func (w *Welcome) Init(user User) {
    w.Email = user.Email
    w.SlackID = user.SlackID

func (w *Welcome) Handle() (interface{}, error) {


You need to setup Zenaton with your credentials:

Zenaton\Client::init($app_id, $api_token, $app_env);
const { Client } = require("zenaton");

Client.init(app_id, api_token, app_env);
Zenaton::Client(app_id, api_token, app_env);
Zenaton.client.Client(app_id, api_token, app_env)
zenaton.InitClient(appID, apiToken, appEnv)

then launching a workflow is as easy as:

(new WelcomeFlow($user))->dispatch();
await new WelcomeFlow(user).dispatch();;

dispatch returns a Promise which will resolve when the agent confirms that your workflow has been properly scheduled.

We use await here for simplicity, but of course if your Javascript stack does not support async/await (or you are in a module scope) you can use the more traditional then()/catch() syntax.
new WelcomeFlow(user).dispatch().catch((err) => {

If you want to reference this workflow later, just implement a getId public method in your workflow implementation that provides the id Zenaton should use to retrieve this workflow instance, eg in WelcomeFlow.phpWelcomeFlow.jsWelcomeFlow.rbWelcomeFlow.pyWelcomeFlow.go

// [...]
public function getId()
    return $this->email;
id() {
def id
def id(self)
    func (w *Welcome) ID() string {
To be valid, this getId method MUST be unique (meaning in the same environment, you can not have two running instances of the same workflow with the same id). This getId method must have the form func ID() string

Pause, Resume, Kill

You can pause a workflow’s instance

await WelcomeFlow.whereId(email).pause();

and later resume it

await WelcomeFlow.whereId(email).resume();

or even kill it

await WelcomeFlow.whereId(email).kill();


At any time, you can inspect current properties by retrieving an instance by id:

$instance = WelcomeFlow::whereId($email)->find();
const instance = await WelcomeFlow.whereId(email).find();


If an error occurred during a task execution, then this workflow instance will automatically be paused and you will have to resume it manually here.

If an error is returned from one of your tasks, you can handle it normally in your Workflow. Eg:

var a int
err := tasks.A.New().Execute().Output(&a)
if err != nil {
    ... //handle error

Note: If you have a custom error type, the information will be lost. Here we just return a standard go error where err.Error() matches the output of the err.Error() that was returned from the task.

For parallel tasks, you will receive a slice of errors. This slice will be nil if no error occurred. If there was an error in one of the parallel tasks, you will receive a slice of the same length as the input tasks, and the index of the task that produced an error will be the same index as the non-nil err in the slice of errors. Eg:

var a int
var b int

errs := task.Parallel{
}.Execute().Output(&a, &b)

if errs != nil {
    if errs[0] != nil {
        // tasks.A error
    if errs[1] != nil {
        // tasks.B error

If your task panics, then this workflow instance will automatically be paused and you will have to resume it manually here.