Myles Skinner's Development Portfolio: Technical Writing
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.
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" (
- 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.
:decline). If I approve a deal, that is an event that the state machine will handle.
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
model is actually a combination of these two state machines working together.
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
:newstate. A deal will remain in the
:newstate 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_approvalsevent that automatically moves the deal forward from
:closed—internal users with a high enough level of permission are able to
:closea 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
: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_lost, with corresponding events
:close_losethat manage the appropriate transitions. Most of our basic clients only need a generic closed status—in these cases I use
:closed_wonto handle all closed deals. When a deal is closed, many of its fields become view-only.
:declinedstatus 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
:declineevent, setting the deal to
:declinedstatus and no further approvals should be possible.
- client states—We have defined two special states for Snapbo:
: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 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
:approveevent. The first approval moves the deal from
:pending_level_two; the next approval moves from
: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_statusstate machine that it is time for it to transition from
:fully_approved. Once all approvals are complete, it should no longer be possible to
:declinea 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
:declineevent in the
approvalstable (so that we keep a record of when the
:declineevent 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
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
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
method outlined in Figure 5:
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.