line

Tutorial: Abandoned Cart Workflow

This tutorial walks you through creating an ecommerce workflow that sends escalating notifications to a shopper if they have added items to their cart but have not checked out.

View theGithub project. View all of the files, fork the project and deploy to Heroku in a few clicks. Or run it in the online sandbox without needing to install Zenaton.

Play on Zenaton

What you will learn

  • Transform data inside workflow code
  • Receive and react to external events
  • How the onEvent function works alongside the handle function

Description

The workflow launches when a shopper adds their first item to their cart and then, if they do not check out within the wait time, sends a series of communications to re-engage the shopper. The notifications include an in-app notification, an email reminder, and an email with a discount code. The shopper's user profile is updated each time a notification is sent.

Steps in the tutorial (try it on the Sandbox)

Workflow Overview

This is the basic structure of a Zenaton workflow:

The handle function is the main function of the workflow. It contains your workflow's logic.

We will use the onEvent function for this workflow to react to external events.

The someHelperFunction is there to simplify the main handle function and make the whole workflow more readable.

module.exports = { 
  *handle (...input) {
    // describe your workflow steps here
  },
  *onEvent(name, ...data) {
    // describe what to do
    // when receiving an external event
  },
  *someHelperFunction(x,y,z) {
   // ...
  }
});

Note: The functions you see are plain javascript. We've added the * due to the generators

Workflow steps

The workflow instance will launch when the customer adds the first item to their cart.

The workflow waits for the checkout event for a specified amount of time. The checkout event is sent from the application via an HTTP request when the customer checks.

If they don't checkout after a certain amount of time, an in-app notification is sent.

Then the workflow waits again for the checkup event for a specified amount of time.

If they still don't checkout, the workflow will send an email reminder.

Finally, the workflow will send a discount code by email.

There will be 3 waiting phases, with different durations and associated notifications:

  • after duration1, send an in-app reminder notification
  • after duration2, send an email reminder
  • after duration3, send an email with a discount code

At any point in time, once the "checkout" event is received the workflow instance stops.

Waiting for Checkout Event

Here most of the logic is done by the wait function. You simply use it to express that you want to wait for an external event called "checkout" for up to some duration.

(That's all! No more crons to pull your database.)

const duration1 = 30 * 60; // 30 minutes

module.exports = { 
  *handle (cart) {
    // waits for the 'checkout' event, up to duration1 seconds
    let event = yield this.wait.event("checkout").for(duration1);

    // if 'checkout' event was received, then exit
    if (event !== null) return;

   /*
      Escalation phase 1 : in-app notification
    */

    // Send an in-app notification
    yield* this.sendNotification(cart);
  },
  *sendNotification(cart) {
    // TODO: send in-app notification
  }
});

So the workflow will start by waiting for the "checkout" external event for a maximum of 30 minutes.

This means that, if the customer checks out before 30 minutes have passed, the workflow will continue its execution to the next step, which in this case will stop its execution.

But if after 30 minutes the customer still hasn't checked out, the workflow will trigger the "sendNotification" function.

The other phases are done the same way.

onEvent function

The onEvent function is very useful when you want to achieve complex event-based logic.

In this tutorial, while the main handle function is waiting for the "checkout" event, the workflow execution is suspended.

But you can still react to events in the meantime by using the onEvent function.

In this workflow, we will use it to react to the "updateCart" event to keep the cart property updated so that when the customer receives reminder notifications, the workflow will have the latest version of the cart to include relevant details of their cart contents in the email text.

1) Launch the workflow

A workflow instance is launched when a shopper adds the first item to their cart.

The workflow instance will be started using the first item as an input parameter and the customer's email address.

{
  "email": "foo@example.com",
  "cart_id": 123,
  "items": [
    {"sku": 456, "name": "item_1"}
  ]
}

The workflow code starts with:

const duration1 = 5;

module.exports = {
  *handle(cart) {
    // holds the last version of the cart
    this.cart = cart;

    // waits for the 'checkout' event, up to duration1 seconds
    let event = yield this.wait.event("checkout").for(duration1);

    // if 'checkout' event was received, then exit
    if (event !== null) return;

    // Send an in-app notification
    yield* this.sendNotification(cart);

The workflow will start by waiting for the "checkout" external event for up to a configurable duration.

If the event is received before this duration, the workflow will stop.

But if the event is not received before this duration passes, the sendNotification function will be triggered.

To see it in action, you can launch a workflow instance within your application code using HTTP

curl -X POST https://gateway.zenaton.com/rest-api/v1/instances \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "app-env: dev" \
  -H "app-id: FBKTJXUMLO" \
  -H "api-token: Bt29HVlvrY9atH41LNwnAp5MQYkEut8oCqXKH8NGOjUAtJltB6Sea82FbP2P" \
  -d '{"name":"AbandonedCart","input":[{"email":"foo@example.com","cart_id":123,"items":[{"sku":456,"name":"item_1"}]}],"version":"v1"}'

Or using our SDK:

const { run } = require("../client");

const params = {
  email: "foo@example.com",
  cart_id: 123,
  items: [
    { sku: 456, name: "item_1" }
  ]
};

run.workflow("AbandonedCart", params);

Workflow Executions

Once the workflow is launched, check the real-time executions in your dashboard, click on the "AbandonedCart" card and on the first progress-bar line, you will see a live summary of what's the execution:

workflow executions

Click on the "dispatched" step to view the input parameters, you see the cart data that we use to start the instance.

workflow input zenaton

Click on the first wait row.

workflow details

2) Properties On the dashboard execution, you may have seen the step "Properties". If you click on it, you will see the cart again:

properties dashboard

It's because in the code at the beginning of the code, we did this:

// holds the last version of the cart
this.cart = cart;

From a code standpoint it's a regular object instance attribute.

The benefits here of using properties against a regular local variable is that you can access it from all functions of the workflow of course, but more interesting: all their mutations appear in the dashboard in a step called "Properties".

This is useful to track what happened during the execution, but also to see the history of mutations afterward.

Note: properties rows are added only when they change.

Let's send an updateCart event to see it's property being updated.

3) Send an updateCart event

Our workflow is now running and waiting for the 'checkout' event in the main handle function. So its execution is suspended. But using the onEvent function, the workflow can still react to external events to trigger tasks or change the workflow properties.

As you can only send events to running workflow instances, you may have to start a new instance of the workflow if the previous one is completed.

Start a new instance

curl -X POST https://gateway.zenaton.com/rest-api/v1/instances \
      -H "Content-Type: application/json" \
      -H "Accept: application/json" \
      -H "app-env: dev" \
      -H "app-id: FBKTJXUMLO" \
      -H "api-token: Bt29HVlvrY9atH41LNwnAp5MQYkEut8oCqXKH8NGOjUAtJltB6Sea82FbP2P" \
      -d '{"name":"AbandonedCart","input":\[{"email":"foo@example.com","cart_id":123,"items":[{"sku":456,"name":"item_1"}]}],"version":"v1"}'

Copy the id from the response, here 32766318-c936-43ca-9869-9b4450c29b8f

{
  "workflow": {
    "canonical_name": "AbandonedCart",
    "id": "32766318-c936-43ca-9869-9b4450c29b8f",
    "input": "[{\"cart_id\":123,\"email\":\"foo@example.com\",\"items\":[{\"name\":\"item_1\",\"sku\":456}]}]",
    "name": "AbandonedCart_v1",
    "programming_language": "javascript"
  }
}

Send the "updateCart" event

This will simulate that the customer just added a second item to it's cart.

curl -X POST https://gateway.zenaton.com/rest-api/v1/instances/32766318-c936-43ca-9869-9b4450c29b8f/event \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "app-env: dev" \
  -H "app-id: FBKTJXUMLO" \
  -H "api-token: Bt29HVlvrY9atH41LNwnAp5MQYkEut8oCqXKH8NGOjUAtJltB6Sea82FbP2P" \
  -d '{"name":"updateCart","data":[{"items":[{"sku":456,"name":"item_1"},{"sku":789,"name":"item_2"}]}]}'

Here is the event payload in details. It's composed of a name "updateCart", and some data, here the latest cart:

{
  "name": "updateCart",
  "data": [
    {
      "items": [
        { "sku": 456, "name": "item_1" },
        { "sku": 789, "name": "item_2" }
      ]
    }
  ]
}

This event wil trigger the onEvent function of the workflow that will update the cart property

  *onEvent(name, data) {
    if (name === "updateCart") {
      this.cart = { ...this.cart, items: data.items };
    }
  },

In the dashboard, you will the step dedicated to this event "updateCart", you will also see it's associated data: the latest cart

abandoned cart steps

As the cart property has changed, there is also a new "Properties" step to show our cart property updated with the latest cart.

These events would normally be sent from our application code using the Zenaton SDK or via HTTP.

4) Send a checkout event

You can now send the checkout event whenever you want, and then see the outcomes on the workflow executions.

curl -X POST https://gateway.zenaton.com/rest-api/v1/instances/32766318-c936-43ca-9869-9b4450c29b8f/event \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "app-env: dev" \
  -H "app-id: FBKTJXUMLO" \
  -H "api-token: Bt29HVlvrY9atH41LNwnAp5MQYkEut8oCqXKH8NGOjUAtJltB6Sea82FbP2P" \
  -d '{"name":"checkout","data":[]}'

5) Explore the different logic paths

Launch the workflow again and wait before sending the checkout event to test different paths.

6) Setup the connectors (optional)

To better see the different workflow logic paths, you may want to setup the Slack and Sendgrid connectors:

Setup those connectors from your dashboard : search for them, then click add.

connectors slack zenaton

You will then have them in your connectors list

connectors list

We've written the workflow code for it so just uncomment the body of the sendNotificationsendReminder, and sendDiscount functions into the workflow/AbandonedCart.js file

Then set the slackConnectorId and sendgridConnectorId variables at the top of the code.

For sending Slack notification for example the sendNotification function will use the connector with the id you got from your dashboard: 7a290130-1b51-11ea-9b7a-c17d2986804f.

Then you don't manage authentication anymore, you just use the Slack API.

*sendNotification(cart) {
    const slack = this.connector("slack", slackConnectorId);

    slack.post("chat.postMessage", {
      body: {
        text: `A user has an issue with cart ${cart.cart_id}`,
        as_user: false,
        channel: slackChannelId
      }
    });
  },

The same applies to sendReminder and sendDiscount functions that uses Sendgrid API:

*sendReminder(cart) {
    const sendgrid = this.connector("sendgrid", sendgridConnectorId);

    const payload = {
      body: {
        personalizations: [{ to: [{ email: cart.email }] }],
        content: [{
            type: "text/plain",
            value: "Hey, we've noticed you did not complete your purchase\n"
        }],
        subject: "You have a cart still open!",
        from: { email: emailFrom }
      }
    };

    sendgrid.post("/mail/send", payload);
  },

Finally, don't forget to change the workflow input to change the email to yours when launching a new workflow instance:

curl -X POST https://gateway.zenaton.com/rest-api/v1/instances \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "app-env: dev" \
  -H "app-id: FBKTJXUMLO" \
  -H "api-token: Bt29HVlvrY9atH41LNwnAp5MQYkEut8oCqXKH8NGOjUAtJltB6Sea82FbP2P" \
  -d '{"name":"AbandonedCart","input":[{"email":"YOUR_EMAIL","cart_id":123,"items":[{"sku":456,"name":"item_1"}]}],"version":"v1"}'

Then you can start it again and see the different logic paths.

Ideas for adding logic

You can add a threshold on the total amount of the cart to handle only valuable cart, and notify Customer success team to provide assistance to the customer or propose different payment methods, ...

You can send a last email with some similar products, propose them to be alerted on price changes, or when items go back in stock ...

View theGithub project. View all of the files, fork the project and deploy to Heroku in a few clicks. Or run it in the online sandbox without needing to install Zenaton.

Play on Zenaton