StateSet Workflow Framework with Temporal

Introduction

StateSet leverages the Temporal framework to build robust and scalable workflows. Temporal’s open-source programming model simplifies complex application logic, enhances reliability, and accelerates feature delivery. By combining serverless API calls with a deterministic execution engine, Temporal provides the backbone for automating critical business processes.

In particular, StateSet’s Return Management (RMA) process is fully automated using Temporal. This includes: generating and emailing return labels, creating return records in StateSet, facilitating record retrieval, updating customer support platforms, and processing instant refunds. This automation significantly streamlines the return process, improves customer satisfaction, and allows businesses to manage returns more efficiently.

Core Components of StateSet Workflows

StateSet Workflows are built using the following components within the Temporal framework:

  • Temporal Client: The entry point for starting workflows and signaling their execution.
  • Temporal Worker: Executes the workflows and activities, polling the task queue for work.
  • Temporal Workflow: Defines the sequence of steps in the business process to be automated.
  • Temporal Activities: Represents atomic units of work that a workflow executes.

Temporal Client: Initiating Workflows

The Temporal Client is used to start workflows, and send signals. In StateSet, the Temporal client is configured with:

  • Namespace: A logical grouping of workflows. (Stateset is using the stateset namespace)
  • Task Queue: A queue that workers poll for workflows and activities. (Stateset is using stateset-returns-automation)
  • Workflow Path: The location of the Workflow definition.

Here’s an example of how Stateset’s Return API uses the Temporal Client to initiate a return workflow.

import { Connection, WorkflowClient } from '@temporalio/client';
import { returnApprovedWorkflow } from './workflows.js'; // Import our return workflow
import { v4 as uuidv4 } from "uuid";

async function run() {
  // Connect to the Temporal service
  const connection = await Connection.connect({
    address: 'https://api.stateset.com/temporal/api/namespaces/default',
  });

  // Create a WorkflowClient instance
  const client = new WorkflowClient({
    connection,
    namespace: 'stateset',
  });

  const return_id = uuidv4();

  // Execute the `returnApprovedWorkflow`
  const return_workflow_result = await client.execute(returnApprovedWorkflow, {
    taskQueue: 'stateset-returns-automation', // the task queue which the worker will monitor
    workflowId: 'workflow-' + return_id, // unique ID of this workflow
    args: [
        {
            "cancel_subscription": true,
            "condition": "A",
            "match": true,
            "country": "US",
            "customer_email": "john.doe@example.com"
        },
        '228476' // ticket id
    ], // Arguments for the workflow
  });

  console.log(return_workflow_result); // Logs the workflow result
}

run().catch((err) => {
  console.error(err);
  process.exit(1);
});

The code shows how we connect to the Temporal cluster and execute the returnApprovedWorkflow workflow by passing some initial arguments.

Temporal Worker: Executing Workflows and Activities

The Temporal Worker is responsible for executing workflows and activities. It polls the specified task queue for pending tasks and executes them.

Here is an example of how a worker is set up to process return workflow:

import { Worker } from '@temporalio/worker';
import { URL } from 'url';
import * as activities from './activities.js'; // Import our activity functions

async function run() {
  // Create a worker instance
  const worker = await Worker.create({
    workflowsPath: new URL('./workflows/return-ticket-approved.js', import.meta.url).pathname, // Path to our workflows
    activities, // Our activity functions
    taskQueue: 'stateset-returns-automation', // Task queue to listen on
    namespace: 'stateset' // The namespace that we're using
  });

  // Start the worker
  await worker.run();
}

run().catch((err) => {
  console.error(err);
  process.exit(1);
});

This worker is configured to pick up workflow tasks on the stateset-returns-automation task queue and use the code in the activities.js file.

Return Approved Workflow: Orchestrating the Process

The returnApprovedWorkflow orchestrates the various steps when a return request is approved. It’s triggered by the Temporal Client, and it executes the following activities:

import { proxyActivities } from '@temporalio/workflow';
import * as wf from '@temporalio/workflow';

// Proxy Activities (configure activity timeouts)
const { generateResponse, createZendeskComment, createReturnRecord, generateUSLabel, generateCALabel, updateWorkflowId, updateMatch, cancelSubscription } = proxyActivities({
    startToCloseTimeout: '1 minute',
});

/** A workflow that orchestrates a return process*/
export async function returnApprovedWorkflow(body, ticket_id_int) {

    let workflow_state = []; // Example to track workflow execution.

    // Extract data from the input body.
    var cancel_subscription = body.cancel_subscription;
    var condition = body.condition;
    var match = body.match;
    var country = body.country;
    var customer_email = body.customer_email;

    // 1. Generate a Customer Response
    await generateResponse(body);

    // 2. Generate a Return Label based on Country
    if (country == "US") {
        await generateUSLabel(ticket_id_int);
    } else {
        await generateCALabel(ticket_id_int);
    }

    // 3. Create a Return Record in our system
    var return_id = await createReturnRecord(ticket_id_int);

    // 4. Update the Return Record with the Temporal Workflow ID
    await updateWorkflowId(return_id, wf.workflowInfo().workflowId);

    // 5. Cancels the customer subscription if cancel_subscription is set to true
    if (cancel_subscription) {
        await cancelSubscription(customer_email, ticket_id_int);
    };

    // 6. Wait 3 Days
    await wf.sleep('3 days');

    // 7. Process Instant Refund based on Condition and Match
    if (condition == "A" && match == true) {
        const matched_return = await updateMatch(return_id, wf.workflowInfo().workflowId);

        // refundOrder is not an activity, so it runs synchronously inside of the workflow
        await refundOrder(return_id);

        // Creates a comment in zendesk for auditing purposes
        await createZendeskComment(ticket_id_int, condition);

        return 'return_and_refund_processed';
    }
    return 'return_processed';
}

This workflow defines the sequence of steps to execute when a return is approved. The workflow is composed of activities which are executed by the temporal worker.

Activities: The Atomic Units of Work

Activities encapsulate the atomic units of work that a workflow executes. Here is an example of a few of the activities that we use in the workflow above. These are the actual calls that are executed by the Temporal Worker.

import axios from 'axios';

// Activity to Generate a Customer Response
export async function generateResponse(body) {
    console.log("generating response");
    return true;
}

// Activity to generate a return label in the US
export async function generateUSLabel(ticket_id_int) {
    console.log("generating US label");
    return true;
}

// Activity to generate a return label in Canada
export async function generateCALabel(ticket_id_int) {
    console.log("generating CA label");
    return true;
}

// Activity to create a record in our system
export async function createReturnRecord(ticket_id_int) {
    console.log("creating return record");
    return 'return_' + ticket_id_int;
}

// Activity to update the record with the workflow id
export async function updateWorkflowId(return_id, workflow_id) {
    console.log("updating workflow id");
    return true;
}

// Activity to update if it was a match for instant refunds
export async function updateMatch(return_id, workflow_id) {
    console.log("updating return match");
    return true;
}

// Activity to create a comment in Zendesk
export async function createZendeskComment(ticket_id_int, condition) {
    console.log("creating zendesk comment");
    return true;
}

// Activity to cancel the customer subscription
export async function cancelSubscription(customer_email, ticket_id_int) {
    console.log("cancelling customer subscription");
    return true;
}

These activities are functions that do the actual work for the workflow. In this case they’re just logging the activity for the sake of example. In a production system, these functions would call our APIs to perform business logic

Stateset Cloud: Hosted Temporal

StateSet provides a hosted Temporal workflow service with deterministic execution and state-of-the-art infrastructure. The hosted service can be accessed at https://cloud.stateset.com.

This diagram shows the different parts of the temporal framework and how they’re organized.

Conclusion

By leveraging the Temporal framework, StateSet implements complex workflow orchestration in a safe, reliable, and scalable way. The separation of concerns between workflows and activities enables StateSet to develop complex features quickly while relying on the powerful Temporal platform.