Tierceron Dot Com

Myles Skinner's Development Portfolio: Technical Writing

The following is an article that I wrote for PartnerPath's Confluence knowlege base. I present this article as an example of my skills in technical writing and documentation.

Deal Registration—General Approval Workflow

Introduction to the State Machine

Different tenants have different ways of handling their deal registration workflows. Our smaller tenants tend to have simple, straightforward workflows; Enterprise tenants like Majaro often have complicated workflows. In our portal, we need to be able to model our deal registration workflow so that it works equally well for everybody, no matter how complicated their deal registration process is. We use a state machine to define the rules that govern the workflow for each of our tenants. In this document, I will outline the basic state machine configuration for simple workflows. In Figure 1 below, I illustrate a state machine flow chart for deals requiring three levels of administrative approval.

Deal registration workflow state machine diagram depicting admin approvals running vertically and the deal life cycle running horizontally.
Figure 1: The basic Deal state machine

A state machine consists of three components:

  • states that represent the status of an object at any given moment, represented by the bold Ruby symbols in Figure 1. For example, a deal can be in a state where it is "Pending Level One Approval" (:pending_level_one) or "Declined" (:declined),
  • transitions that move an object from one state to another. On Figure 1, the transitions are represented by the arrows, and
  • events that trigger transitions, a few of which are shown in Figure 1 by the verbs associated with the transition arrows (e.g. :approve or :decline). If I approve a deal, that is an event that the state machine will handle.

Two-Dimensional Workflow

One of the questions we faced in our old system was how to model workflows that require multiple levels of approval. An even more complicated question is, "How do we model workflows for different tenants who require different numbers of approval levels?" Our previous solution was to model the workflow as one long line from beginning to end, with special rules determining how a deal should traverse the line, jumping forwards and backwards chaotically. However, this approach got messy, especially when different tenants needed competing and contradictory rules.

In order to make the workflow much easier to configure, and to make the supporting code much more elegant, I have separated the workflow into two distinct but interdependent state machines: the :workflow_status state machine that runs from left to right on Figure 1 above, and the :approval_status state machine that runs from top to bottom. Each has its own set of rules that define the events that trigger transitions from one state to another. The "state machine" defined in our Deal model is actually a combination of these two state machines working together.

Deal registration workflow diagram depicting a progression from 'new' to 'fully approved' to 'closed', with an optional state called 'declined'.
Figure 2: Deal workflow status

The basic workflow lifecycle of a normal deal is very simple: it starts out its life in :new status, progresses to :fully_approved, and eventually becomes :closed, shown in Figure 2 as arrows going from left to right. The basic states defined in the :workflow_status state machine work as follows:

  • :new—all deals automatically start out in a :new state. A deal will remain in the :new state until it hears from the approval state machine that it is time to move forward.
  • :fully_approved—when a deal receives a message from the approval state machine that all approvals are complete, that message will trigger a :complete_approvals event that automatically moves the deal forward from :new status to :fully_approved.
  • :closed—internal users with a high enough level of permission are able to :close a deal when it is complete. Because each tenant might have their own internal requirements for what constitutes a "closed" deal, we do not automatically move deals from :fully_approved to :closed. We close deals when a request comes in from an internal user, either via the deal edit form or an API call. We actually have two separate closed statuses: :closed_won and :closed_lost, with corresponding events :close_win and :close_lose that manage the appropriate transitions. Most of our basic clients only need a generic closed status—in these cases I use :closed_won to handle all closed deals. When a deal is closed, many of its fields become view-only.
  • :declined—the :declined status is special because it sits outside of the regular workflow. If a deal is declined, no matter what state it is in at the time, we immediately trigger a :decline event, setting the deal to :declined status and no further approvals should be possible.
  • client states—We have defined two special states for Snapbo: :info_requested and :resubmitted. These states do not participate in our workflow; we have them defined as placeholder states because Snapbo sends us these state values via our API and expects them to be visible in their portal.
Approval workflow diagram depicting a progression through three 'pending approval' levels, ending at 'approvals complete', with an option to decline at any of the pending approval levels.
Figure 3: Three-level approval workflow

Approval Status: Top to Bottom

Depending on the tenant, a deal might need to pass through multiple levels of approval. Figure 3 shows an example of an :approval_workflow with three approval levels. We are not limited to three levels—we can define as many levels as we need. Most basic tenants will only need one or two levels; Majaro currently has six levels.

A new deal starts with an approval status of :pending_level_one, and over the course of its life progresses from top to bottom until it reaches the :approvals_complete state. When approvals are complete, the :approval_status state machine sends a message back to the :workflow_status state machine to let it know that it is time to move forward.

There are two basic events in the normal :approval_status state machine:

  • :approve—when an admin approves one of their pending deals, that action sets off an :approve event. The first approval moves the deal from :pending_level_one downward to :pending_level_two; the next approval moves from :pending_level_two down to :pending_level_three, and so on. When we reach the end of the approval sequence at a state of :approvals_complete, a callback gets fired that notifies the :workflow_status state machine that it is time for it to transition from :new status to :fully_approved. Once all approvals are complete, it should no longer be possible to :approve or :decline a deal, and the approvals fieldset on the deal edit form becomes read-only.
  • :decline—when an admin declines one of their pending deals, what normally happens is that we log the :decline event in the approvals table (so that we keep a record of when the :decline event occurred), and immediately move the :workflow_status into a :declined state. At this point, we should not be able to update the deal any further. As I have described it, this decline behaviour should be correct for all of our tenants except for Majaro, who has their own unique approach to handling declines.

Client Configuration: Defining the Approval Levels

Rather than set up a separate state machine for each tenant, we implement a single state machine with flexible rules. The code fragment in Figure 4 shows how we represent the :approve events diagrammed in Figure 3 in our Deal model:

Ruby code fragment defining the state transitions for the approve event outlined in the text.
Figure 4: Transitions defined for the :approve event

As the comment at the top of Figure 4 indicates, the state machine will examine the transitions we have defined in order, line-by-line, until it finds one that meets the conditions we have defined. When the state machine finds a match, it executes the transition. Because the state machine checks each transition line in order when deciding which line to execute, we can use the ordering to define a precedence order for our rules.

The code in Figure 4 should be easy enough to read—I tried to choose labels that were self-documenting. The main point I'd like to demonstrate is in lines 4-5. Line 4 says we transition from :pending_level_one on to :pending_level_two provided that the level_two_valid? check passes for a particular deal. If level two is not valid, then we fall through to line 5 and transition to :approvals_complete. In other words, if there's no level two, then we must be finished once we get past level one.

The if check on the boolean instance method level_two_valid? in line 4 is important because it allows us to define the approval workflow behaviour not only for individual tenants, but also for individual deals. Consider the sample level_two_valid? method outlined in Figure 5:

Ruby code fragment defining rules for conditionally requiring level two approval.
Figure 5: @deal.level_two_valid? boolean instance method

In Figure 5, level_two_valid? always returns true for BlogFish, so all deals in BlogFish will have two levels of approval. Majaro has much more complicated requirements where some deals have a level two and some don't, based on the evaluation of a number of arbitrary properties of an individual deal. For most other tenants, level_two_valid? returns false, so these tenants will only have a single level of approval on their deal registrations.

By combining the conditional transition rules in the state machine with per-tenant validations for each level of approval, we can allow every tenant's workflow to co-exist peacefully. The vast majority of our tenants will have a simple one-level workflow and the defaults we have defined will "just work" out of the box, but this state machine model can handle any degree of complexity. Majaro is by far our most complex tenant, with six different approval levels—each with its own set of routing rules—that may or may not apply to a particular deal depending on the values of several of its properties. Majaro's state machine is too complex to outline here; I explain the details of Majaro's implementation in a separate document.