Seam
Simple workflows in Ruby.
Usage
Seam is meant for situations where you want to take some entity (user, order, ec.) through a long-running process that is comprised of multiple steps.
For example, if you want every new user a "hello" email after signup, then wait a few days, and then send a "gone so soon?" email if they haven't signed in again.
This gem provides some simple tools for building and executing this process. It provides a way to define the process, break it up into separate components, and then send entities through the process.
Define a workflow
To start, define a workflow.
flow = Seam::Flow.new
flow.send_order_to_warehouse
flow.wait_for_order_to_be_shipped wait_up_to: 7.days
flow.send_shipping_email email_template: 'shipping_7'
flow.mark_order_as_fulfilled
A flow will convert any method call you make into a step that has to be completed.
You can also pass a hash to the method, which will be saved for later.
flow.wait_for_order_to_be_shipped wait_up_to: 7.days
Starting an instance of the flow
Starting an instance of the flow is done with "start":
flow.start order_id: '1234'
An instance of this effort was created and saved in whatever persistence is being used (in-memory by default).
This effort will start at the first step (send_order_to_warehouse) and then progress through the steps as they are completed.
"start" also returns the effort that was created, and it will look like this:
<Seam::Effort
@completed_steps=[],
@history=[],
@complete=false,
@id="1ecc4cbe-16af-45f6-8532-7f37493ec11c",
@created_at=2013-08-20 22:58:07 -0500,
@next_execute_at=2013-08-20 22:58:07 -0500,
@next_step="send_order_to_warehouse",
@flow={"steps"=>[{"name"=>"send_order_to_warehouse", "type"=>"do", "arguments"=>{}}, {"name"=>"wait_for_order_to_be_shipped", "type"=>"do", "arguments"=>{}}, {"name"=>"send_shipping_email", "type"=>"do", "arguments"=>{}}, {"name"=>"mark_order_as_fulfilled", "type"=>"do", "arguments"=>{}}]},
@data={"order_id"=>"1234"}>
So we have a unique instance of this flow and the instance has been saved in the database. The first step to be executed for this instance is "send_order_to_warehouse", so let's create a worker for this step.
Defining workers for each step
A workflow is comprised of steps, and each step needs a worker. Each worker will execute whatever it was meant to do, and then either:
- Pass the workflow instance to the next step on the process, or
- Delay the step execution for a later date, or
- End the entire workflow process for the instance.
Since send_order_to_warehouse is the first step in this workflow, let's build the worker for it first:
class SendOrderToWarehouseWorker < Seam::Worker
def process
# the current workflow instance is available as "effort"
order = Order.find effort.data['order_id']
warehouse_service.send order
# by default, if this worker completes with no error the workflow instance will be sent to the next step
end
end
If you name your class as a camel-case version of the step, Seam will automatically bind up the worker to the step.
To execute the worker, use:
SendOrderToWarehouse.execute_all
This method will look for all workflow instances that are currently ready for the step in question.
Progressing through the workflow
By default, steps are considered as being completed when the worker completes successfully. There might be times where you don't want to go quickly, like the next step in this example:
class WaitForOrderToBeShippedWorker < Seam::Worker
def process
effort.data["shipping_status"] = # some method that returns the shipping status
unless effort.data["shipping_status"] == "shipped"
try_again_in 4.hours
end
end
end
"try_again_in" can be used to signal that the step has not been completed and should be retried later.
"eject" can also be used to signify that the entire workflow should be stopped, like so:
class WaitForOrderToBeShippedWorker < Seam::Worker
def process
effort.data["shipping_status"] = # some method that returns the shipping status
if effort.data["shipping_status"] == "cancelled"
eject # no need to continue!
end
end
end
History
As workflow instances progress through each step, the history of every operation will be stored. A history of the "data" block before and after each step run is also stored.
The history is available through:
effort.history
Waiting
Seam comes with a default worker for waiting. It can be defined by calling "wait" on a flow, like this.
flow = Seam::Flow.new
flow.send_order_to_warehouse
flow.wait 2.days
flow.check_if_the_order_has_been_fulfilled
Installation
Add this line to your application's Gemfile:
gem 'seam'
And then execute:
$ bundle
Or install it yourself as:
$ gem install seam
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request