make documentation more developer-friendly

This commit is contained in:
Elizabeth Esswein
2024-02-07 12:57:40 -05:00
parent 14d081bfea
commit 0457022cd0
68 changed files with 1841 additions and 2384 deletions

View File

@@ -1,25 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
.PHONY: apidoc
apidoc:
sphinx-apidoc -d5 -Mefo . ../venv/lib/python3.7/site-packages/SpiffWorkflow
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -1,492 +0,0 @@
A More In-Depth Look at Some of SpiffWorkflow's Features
========================================================
BPMN Task Specs
---------------
BPMN Tasks inherit quite a few attributes from :code:`SpiffWorkflow.specs.base.TaskSpec`, but only a few are used.
* `name`: the unique id of the TaskSpec, and it will correspond to the BPMN ID if that is present
* `description`: we use this attribute to provide a description of the BPMN type (the text that appears here can be overridden in the parser)
* `inputs`: a list of TaskSpec `names` that are parents of this TaskSpec
* `outputs`: a list of TaskSpec `names` that are children of this TaskSpec
* `manual`: :code:`True` if human input is required to complete tasks associated with this TaskSpec
BPMN Tasks have the following additional attributes.
* `bpmn_id`: the ID of the BPMN Task (this will be :code:`None` if the task is not visible on the diagram)
* `bpmn_name`: the BPMN name of the Task
* `lane`: the lane of the BPMN Task
* `documentation`: the contents of the BPMN `documentation` element for the Task
* `data_input_associations`: a list of incoming data object references
* `data_output_associtions`: a list of outgoing data object references
* `io_specification`: the BPMN IO specification of the Task
Filtering Tasks
---------------
Tasks by Lane
^^^^^^^^^^^^^
The :code:`workflow.get_ready_user_tasks` method optionally takes the argument `lane`, which can be used to
restrict the tasks returned to only tasks in that lane.
.. code:: python
ready_tasks = workflow.get_ready_user_tasks(lane='Customer')
will return only tasks in the 'Customer' lane in our example workflow.
Tasks by Spec Name
^^^^^^^^^^^^^^^^^^
To retrieve a list of tasks associated with a particular task spec, use :code:`workflow.get_tasks_from_spec_name`
.. code:: python
tasks = workflow.get_tasks_from_spec_name('customize_product')
will return a list containing the Call Actitivities for the customization of a product in our example workflow.
.. note::
The `name` paramter here refers to the task spec name, not the BPMN name (for visible tasks, this will
be the same as the `bpmn_id`)
Tasks by State
^^^^^^^^^^^^^^
We need to import the :code:`TaskState` object (unless you want to memorize which numbers correspond to which states).
.. code:: python
from SpiffWorkflow.util.task import TaskState
tasks = workflow.get_tasks(TaskState.COMPLETED)
will return a list of completed tasks.
See :doc:`../concepts` for more information about task states.
Tasks in a Subprocess or Call Activity
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The :code:`BpmnWorkflow` class maintains a dictionary of subprocesses (the key is the `id` of the Call Activity or
Subprocess Task). :code:`workflow.get_tasks` will start at the top level workflow and recurse through the subprocesses
to create a list of all tasks. It is also possible to start from a particular subprocess:
.. code:: python
tasks = workflow.get_tasks_from_spec_name('customize_product')
subprocess = workflow.get_subprocess(tasks[0])
subprocess_tasks = workflow.get_tasks(workflow=subprocess)
will limit the list of returned tasks to only those in the first product customization.
.. note::
Each :code:`Task` object has a reference to its workflow; so with a Task inside a subprocess, we can call
:code:`workflow.get_tasks(workflow=task.workflow)` to start from our current workflow.
Logging
-------
Spiff provides several loggers:
- the :code:`spiff` logger, which emits messages when a workflow is initialized and when tasks change state
- the :code:`spiff.metrics` logger, which emits messages containing the elapsed duration of tasks
- the :code:`spiff.data` logger, which emits a message when :code:`task.update_data` is called or workflow data is retrieved or set.
Log level :code:`INFO` will provide reasonably detailed information about state changes.
As usual, log level :code:`DEBUG` will probably provide more logs than you really want
to see, but the logs will contain the task and task internal data.
Data can be included at any level less than :code:`INFO`. In our example application,
we define a custom log level
.. code:: python
logging.addLevelName(15, 'DATA')
so that we can see the task data in the logs without fully enabling debugging.
The workflow runners take an `-l` argument that can be used to specify the logging level used when running the example workflows.
We'll write the logs to a file called `data.log` instead of the console to avoid printing very long messages during the workflow.
Our logging configuration code can be found in `runner/shared.py`. Most of the code is about logging
configuration in Python rather than anything specific to SpiffWorkflow, so we won't go over it in depth.
Parsing
-------
Each of the BPMN pacakges (:code:`bpmn`, :code:`spiff`, or :code:`camunda`) has a parser that is preconfigured with
the specs in that package (if a particular TaskSpec is not implemented in the package, :code:`bpmn` TaskSpec is used).
See the example in :doc:`synthesis` for the basics of creating a parser. The parser can optionally be initialized with
- a set of namespaces (useful if you have custom extensions)
- a BPMN Validator (the one in the :code:`bpmn` package validates against the BPMN 2.0 spec)
- a mapping of XML tag to Task Spec Descriptions. The default set of descriptions can be found in
:code:`SpiffWorkflow.bpmn.parser.spec_descriptions`. These values will be added to the Task Spec in the `description` attribute
and are intended as a user-friendly description of what the task is.
The :code:`BpmnValidator` can be used and extended independently of the parser as well; call :code:`validate` with
an :code:`lxml` parsed tree.
Loading BPMN Files
^^^^^^^^^^^^^^^^^^
In addition to :code:`load_bpmn_file`, there are similar functions :code:`load_bpmn_str` which can load the XML from a string, and
:code:`load_bpmn_io`, which can load XML from any object implementing the IO interface, and :code:`add_bpmn_xml`, which can load
BPMN specs from an :code:`lxml` parsed tree.
Dependencies
^^^^^^^^^^^^
The following methods are available for discovering the names of processes and DMN files that may be defined externally:
- :code:`get_subprocess_specs`: Returns a mapping of name -> :code:`BpmnWorkflowSpec` for any Call Activities referenced by the
provided spec (searches recursively)
- :code:`find_all_spec`: Returns a mapping of name -> :code:`BpmnWorkflowSpec` for all processes used in all files that have been
provided to the parser at that point.
- :code:`get_process_dependences`: Returns a list of process IDs referenced by the provided process ID
- :code:`get_dmn_dependencies`: Returns a list of DMN IDs referenced by the provided process ID
Serialization
-------------
The :code:`BpmnWorkflowSerializer` has two components
* the `workflow_spec_converter` (which handles serialization of objects that SpiffWorkflow knows about)
* the `data_converter` (which handles serialization of custom objects)
Unless you have overriden any of TaskSpecs with custom specs, you should be able to use the serializer
configuration from the package you are importing the parser from (:code:`bpmn`, :code:`spiff`, or :code:`camunda`).
See :doc:`synthesis` for an example.
Serializing Custom Objects
^^^^^^^^^^^^^^^^^^^^^^^^^^
In `Custom Script Engines`_ , we add some custom methods and objects to our scripting environment. We create a simple
class (a :code:`namedtuple`) that holds the product information for each product.
We'd like to be able to save and restore our custom object.
.. code:: python
ProductInfo = namedtuple('ProductInfo', ['color', 'size', 'style', 'price'])
def product_info_to_dict(obj):
return {
'color': obj.color,
'size': obj.size,
'style': obj.style,
'price': obj.price,
}
def product_info_from_dict(dct):
return ProductInfo(**dct)
registry = DictionaryConverter()
registry.register(ProductInfo, product_info_to_dict, product_info_from_dict)
Here we define two functions, one for turning our object into a dictionary of serializable objects, and one for recreating
the object from the dictionary representation we created.
We initialize a :code:`DictionaryConverter` and `register` the class and methods.
Registering an object sets up relationships between the class and the serialization and deserialization methods. We go
over how this works in a little more detail in `Custom Serialization in More Depth`_.
It is also possible to bypass using a :code:`DictionaryConverter` at all for the data serialization process (but not for
the spec serialization process). The only requirement for the the `data_converter` is that it implement the methods
- `convert`, which takes an object and returns something JSON-serializable
- `restore`, which takes a serialized version and returns an object
Serialization Versions
^^^^^^^^^^^^^^^^^^^^^^
As we make changes to Spiff, we may change the serialization format. For example, in 1.2.1, we changed
how subprocesses were handled interally in BPMN workflows and updated how they are serialized and we upraded the
serializer version to 1.1.
As we release SpiffWorkflow 2.0, there are several more substantial changes, and we'll upgrade the serializer version to 1.2.
Since workflows can contain arbitrary data, and even SpiffWorkflow's internal classes are designed to be customized in ways
that might require special serialization and deserialization, it is possible to override the default version number, to
provide users with a way of tracking their own changes. This can be accomplished by setting the `VERSION` attribute on
the :code:`BpmnWorkflowSerializer` class.
If you have not provided a custom version number, SpiffWorkflow wil attempt to migrate your workflows from one version
to the next if they were serialized in an earlier format.
If you've overridden the serializer version, you may need to incorporate our serialization changes with
your own. You can find our conversions in
`SpiffWorkflow/bpmn/serilaizer/migrations <https://github.com/sartography/SpiffWorkflow/tree/main/SpiffWorkflow/bpmn/serializer/migration>`_
These are broken up into functions that handle each individual change, which will hopefully make it easier to incoporate them
into your upgrade process, and also provides some documentation on what has changed.
Custom Serialization in More Depth
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Both of the serializer components mentioned in `Serialization`_ are based on the :code:`DictionaryConverter`. Let's import
it and create one and register a type:
.. code:: python
from datetime import datetime
from SpiffWorkflow.bpmn.serializer.helpers.dictionary import DictionaryConverter
registry = DictionaryConverter()
registry.register(
datetime.
lambda dt: {'value': dt.isoformat() },
lambda dct: datetime.fromisoformat(dct['value'])
)
The arguemnts to :code:`register` are:
* `cls`: the class to be converted
* `to_dict`: a function that returns a dictionary containing JSON-serializable objects
* `from_dict`: a function that take the output of `to_dict` and restores the original object
When the :code:`register` method is called, a `typename` is created and maps are set up between `cls` and `to_dict`
function, `cls` and `typename`, and `typename` and `from_dict`.
When :code:`registry.convert` is called on an object, the `cls` is use to retrieve the `to_dict` function and the
`typename`. The `to_dict` funciton is called on the object and the `typename` is added to the resulting dictionary.
When :code:`registry.restore` is called with a dictionary, it is checked for a `typename` key, and if one exists, it
is used to retrieve the `from_dict` function and the dictionary is passed to it.
If an object is not recognized, it will be passed on as-is.
Custom Script Engines
---------------------
You may need to modify the default script engine, whether because you need to make additional
functionality available to it, or because you might want to restrict its capabilities for
security reasons.
.. warning::
By default, the scripting environment passes input directly to :code:`eval` and :code:`exec`! In most
cases, you'll want to replace the default scripting environment with one of your own.
Files referenced in this section:
* `script_engine.py <https://github.com/sartography/spiff-example-cli/blob/main/runner/script_engine.py>`_
* `product_info.py <https://github.com/sartography/spiff-example-cli/blob/main/runner/product_info.py>`_
* `subprocess.py <https://github.com/sartography/spiff-example-cli/blob/main/runner/subprocess.py>`_
* `spiff-bpmn-runner.py <https://github.com/sartography/spiff-example-cli/blob/main/spiff-bpmn-runner.py>`_
The following example replaces the default global enviroment with the one provided by
`RestrictedPython <https://restrictedpython.readthedocs.io/en/latest/>`_
.. code:: python
from RestrictedPython import safe_globals
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from SpiffWorkflow.bpmn.PythonScriptEngineEnvironment import TaskDataEnvironment
restricted_env = TaskDataEnvironment(safe_globals)
restricted_script_engine = PythonScriptEngine(environment=restricted_env)
Another reason you might want to customize the scripting environment is to provide access to custom
classes or functions.
In our example models so far, we've been using DMN tables to obtain product information. DMN
tables have a **lot** of uses so we wanted to feature them prominently, but in a simple way.
If a customer was selecting a product, we would surely have information about how the product
could be customized in a database somewhere. We would not hard code product information in
our diagram (although it is much easier to modify the BPMN diagram than to change the code
itself!). Our shipping costs would not be static, but would depend on the size of the order and
where it was being shipped -- maybe we'd query an API provided by our shipper.
SpiffWorkflow is obviously **not** going to know how to query **your** database or make API calls to
**your** vendors. However, one way of making this functionality available inside your diagram is to
implement the calls in functions and add those functions to the scripting environment, where they
can be called by Script Tasks.
We are not going to actually include a database or API and write code for connecting to and querying
it, but since we only have 7 products we can model our database with a simple dictionary lookup
and just return the same static info for shipping for the purposes of the tutorial.
We'll define some resources in `product_info.py`
.. code:: python
from collections import namedtuple
ProductInfo = namedtuple('ProductInfo', ['color', 'size', 'style', 'price'])
INVENTORY = {
'product_a': ProductInfo(False, False, False, 15.00),
'product_b': ProductInfo(False, False, False, 15.00),
'product_c': ProductInfo(True, False, False, 25.00),
'product_d': ProductInfo(True, True, False, 20.00),
'product_e': ProductInfo(True, True, True, 25.00),
'product_f': ProductInfo(True, True, True, 30.00),
'product_g': ProductInfo(False, False, True, 25.00),
}
def lookup_product_info(product_name):
return INVENTORY[product_name]
def lookup_shipping_cost(shipping_method):
return 25.00 if shipping_method == 'Overnight' else 5.00
We'll add these functions to our scripting environment in `script_engine.py`
.. code:: python
env_globals = {
'lookup_product_info': lookup_product_info,
'lookup_shipping_cost': lookup_shipping_cost,
'datetime': datetime,
}
custom_env = TaskDataEnvironment(env_globals)
custom_script_engine = PythonScriptEngine(environment=custom_env)
.. note::
We're also adding :code:`datetime`, because we added the timestamp to the payload of our message when we
set up the Message Event (see :doc:`events`)
When we initialize the runner in `spiff-bpmn-runner.py`, we'll import and use `custom_script_engine` as our
script engine.
We can use the custom functions in script tasks like any normal function. We've replaced the Business Rule
Task that determines product price with a script that simply checks the `price` field on our product.
.. figure:: figures/script_engine/top_level.png
:scale: 30%
:align: center
Top Level Workflow with Custom Script Engine
And we can simplify the gateways in our 'Call Activity' flows as well now too:
.. figure:: figures/script_engine/call_activity.png
:scale: 30%
:align: center
Call Activity with Custom Script Engine
To run this workflow (you'll have to manually change which script engine you import):
.. code-block:: console
./spiff-bpmn-runner.py -p order_product -b bpmn/tutorial/top_level_script.bpmn bpmn/tutorial/call_activity_script.bpmn
Another reason to customize the scripting enviroment is to allow it to run completely separately from
SpiffWorkflow. You might wish to do this for performance or security reasons.
In our example repo, we've created a simple command line script in `runner/subprocess.py` that takes serialized global
and local environments and a script or expression to execute or evaluate. In `runner/script_engine.py`, we create
a scripting environment that runs the current :code:`execute` or :code:`evaluate` request in a subprocess with this
script. We've imported our custom methods into `subprocess.py` so they are automatically available when it is used.
This example is needlessly complex for the work we're doing in this case, but the point of the example is to demonstrate
that this could be a Docker container with a complex environment, an HTTP API running somewhere else entirely.
.. note::
Note that our execute method returns :code:`True`. We could check the status of our process here and return
:code:`False` to force our task into an `ERROR` state if the task failed to execute.
We could also return :code:`None`
if the task is not finished; this will cause the task to go into the `STARTED` state. You would have to manually
complete a task that has been `STARTED`. The purpose of the state is to tell SpiffWorkflow your application will
handle monitoring and updating this task and other branches that do not depend on this task may proceed. It is
intended to be used with potentially long-running tasks.
See :doc:`../concepts` for more information about Task States and Workflow execution.
Service Tasks
-------------
Service Tasks are also executed by the workflow's script engine, but through a different method, with the help of some
custom extensions in the :code:`spiff` package:
- `operation_name`, the name assigned to the service being called
- `operation_params`, the parameters the operation requires
This is our script engine and scripting environment:
.. code:: python
service_task_env = TaskDataEnvironment({
'product_info_from_dict': product_info_from_dict,
'datetime': datetime,
})
class ServiceTaskEngine(PythonScriptEngine):
def __init__(self):
super().__init__(environment=service_task_env)
def call_service(self, operation_name, operation_params, task_data):
if operation_name == 'lookup_product_info':
product_info = lookup_product_info(operation_params['product_name']['value'])
result = product_info_to_dict(product_info)
elif operation_name == 'lookup_shipping_cost':
result = lookup_shipping_cost(operation_params['shipping_method']['value'])
else:
raise Exception("Unknown Service!")
return json.dumps(result)
service_task_engine = ServiceTaskEngine()
Instead of adding our custom functions to the enviroment, we'll override :code:`call_service` and call them directly
according to the `operation_name` that was given. The :code:`spiff` Service Task also evaluates the parameters
against the task data for us, so we can pass those in directly. The Service Task will also store our result in
a user-specified variable.
We need to send the result back as json, so we'll reuse the functions we wrote for the serializer.
The Service Task will assign the dictionary as the operation result, so we'll add a `postScript` to the Service Task
that retrieves the product information that creates a :code:`ProductInfo` instance from the dictionary, so we need to
import that too.
The XML for the Service Task looks like this:
.. code:: xml
<bpmn:serviceTask id="Activity_1ln3xkw" name="Lookup Product Info">
<bpmn:extensionElements>
<spiffworkflow:serviceTaskOperator id="lookup_product_info" resultVariable="product_info">
<spiffworkflow:parameters>
<spiffworkflow:parameter id="product_name" type="str" value="product_name"/>
</spiffworkflow:parameters>
</spiffworkflow:serviceTaskOperator>
<spiffworkflow:postScript>product_info = product_info_from_dict(product_info)</spiffworkflow:postScript>
</bpmn:extensionElements>
<bpmn:incoming>Flow_104dmrv</bpmn:incoming>
<bpmn:outgoing>Flow_06k811b</bpmn:outgoing>
</bpmn:serviceTask>
Getting this information into the XML is a little bit beyond the scope of this tutorial, as it involves more than
just SpiffWorkflow. I hand edited it for this case, but you can hardly ask your BPMN authors to do that!
Our `modeler <https://github.com/sartography/bpmn-js-spiffworkflow>`_ has a means of providing a list of services and
their parameters that can be displayed to a BPMN author in the Service Task configuration panel. There is an example of
hard-coding a list of services in
`app.js <https://github.com/sartography/bpmn-js-spiffworkflow/blob/0a9db509a0e85aa7adecc8301d8fbca9db75ac7c/app/app.js#L47>`_
and as suggested, it would be reasonably straightforward to replace this with a API call. `SpiffArena <https://www.spiffworkflow.org/posts/articles/get_started/>`_
has robust mechanisms for handling this that might serve as a model for you.
How this all works is obviously heavily dependent on your application, so we won't go into further detail here, except
to give you a bare bones starting point for implementing something yourself that meets your own needs.
To run this workflow (you'll have to manually change which script engine you import):
.. code-block:: console
./spiff-bpmn-runner.py -p order_product -b bpmn/tutorial/top_level_service_task.bpmn bpmn/tutorial/call_activity_service_task.bpmn

163
doc/bpmn/application.rst Normal file
View File

@@ -0,0 +1,163 @@
Overview
========
This section focuses on the example application rather than the library itself; it is intended to orient people
attempting to use this documentation, so we won't devote that much space to it (much of it is necessary for a
functioning app, but not directly relevant to library use); nonetheless, there is quite a bit of code and a general
idea of what's here will be helpful.
The application has several parts:
- an engine, which uses SpiffWorkflow to run, parse, and serialize workflows
- a curses UI for running and examining Workflows, which uses the engine
- a command line UI with some limited functionality, which also uses the engine
We'll mainly focus on the engine, as it contains the interface with the library, though a few examples will come from
the other components. The engine is quite small and simple compared to the code required to handle user input and
display information in a terminal.
Configuration is set up in a python module and passed into the application with the `-e` argument, which loads the
configured engine from this file. This setup should make it relatively to change the behavior of engine. The
following configurations are included:
- :code:`spiff_example.spiff.file`: uses spiff BPMN extensions and serializes to JSON files
- :code:`spiff_example.spiff.sqlite`: uses spiff BPMN extensions and serializes to SQLite
- :code:`spiff_example.camunda.default`: uses Camunda extensions and serializes to SQLite
.. _quickstart:
Quickstart
==========
There are several versions of a product ordering process of variying complexity located in the
:example:`bpmn/tutorial` directory of the repo which contain most of the elements that SpiffWorkflow supports. These
diagrams can be viewed in any BPMN editor, but many of them have custom extensions created with
`bpmn-js-spiffworflow <https://github.com/sartography/bpmn-js-spiffworkflow>`_.
To add a workflow via the command line and store serialized specs in JSON files:
.. code-block:: console
./runner.py -e spiff_example.spiff.file add \
-p order_product \
-b bpmn/tutorial/{top_level,call_activity}.bpmn \
-d bpmn/tutorial/{product_prices,shipping_costs}.dmn
To run the curses application using serialized JSON files:
.. code-block:: console
./runner.py -e spiff_example.spiff.file
Select the 'Start Workflow' screen and start the process.
The Application in Slightly More Depth
======================================
The application requires the name of a module to load that contains a configuration such as one of those defined above.
To start the curses UI using the JSON file serializer:
.. code-block:: console
./runner.py -e spiff_example.spiff.file
If the application is run with no other arguments, the curses UI will be loaded.
It is possible to add a workflow spec through the curses UI, but it is going to be somewhat painful to do so unless
you are a better typist and proofreader than I; therefore, there are also a few command line utilities for handling
some of the functionality, including adding workflow specs.
Command line options are
- :code:`add` to add a workflow spec (while taking advantage of your shell's file completion functionality)
- :code:`list` to list the available specs
- :code:`run` to run a workflow non-interactively
Each of these options has a help menu that describes how to use them.
Configuration Modules
=====================
The three main ways that users can customize the library are:
- the parser
- the script engine
- the serializer
We use the configuration module to allow these components to be defined outside the workflow engine itself and passed
in as parameters to make it easier to experiment. I am somewhat regularly asked questions about why a diagram doesn't
executed as expected, or how to get the script engine to work a particular way; this is a first pass at setting
something up that works better for me than configuring the library's test loader and running that in a debugger; I hope
other people will find it useful as well.
We'll go through the configuration in greater detail in later sections, but we'll take a brief look at the simplest
configuration, :app:`spiff/file.py` here.
In this file, we'll initialize our parser:
.. code-block:: python
parser = SpiffBpmnParser()
We don't need to further customize this parser -- this is a builtin parser that can handle DMN files as well as Spiff
BPMN extensions.
We also need to initialize a serializer:
.. code-block:: python
dirname = 'wfdata'
FileSerializer.initialize(dirname)
registry = FileSerializer.configure(SPIFF_CONFIG)
serializer = FileSerializer(dirname, registry=registry)
JSON specs and workflows will be stored in :code:`wfdata`. The :code:`registry` is the place where information about
converting Python objects to and from JSON-serializable dictionary form is maintained. :code:`SPIFF_CONFIG` tells the
serializer how to handle objects used internally by Spiff. Workflows can also contain arbitrary data, so this registry
can also tell the serializer how to handle any non-serializable data in your workflow. We'll go over this in more
detail in :ref:`serializing_custom_objects`.
We initialize a scripting enviroment:
.. code-block:: python
script_env = TaskDataEnvironment({'datetime': datetime })
>script_engine = PythonScriptEngine(script_env)
The :code:`PythonScriptEngine` handles execution of script tasks and evaluation of gateway and DMN conditions.
We'll create the script engine based on it; execution and evaluation will occur in the context of this enviroment.
SpiffWorkflow provides a default scripting environment that is suitable for simple applications, but a serious
application will probably need to extend (or restrict) it in some way. See :doc:`script_engine` for a few examples.
Therefore, we have the ability to optionally pass one in.
In this case, we'll include access to the :code:`datetime` module, because we'll use it in several of our script tasks.
We also specify some handlers:
.. code-block:: python
handlers = {
UserTask: UserTaskHandler,
ManualTask: ManualTaskHandler,
NoneTask: ManualTaskHandler,
}
This is a mapping of task spec to task handler and lets our application know how to handle these tasks.
.. note::
In our application, we're also passing in handlers, but this is not a typical use case. The library knows how to
handle all task types except for human (User and Manual) tasks, and those handlers would typically be built into
your application. However, this application needs to be able to deal with more than one set of human task specs,
and this is a convenient way to do this. The library treats None tasks (tasks with no specific type assigned)
like Manual Tasks by default.
We then create our BPMN engine (:app:`engine/engine.py`) using each of these components:
.. code-block:: python
from ..engine import BpmnEngine
engine = BpmnEngine(parser, serializer, handlers, script_env)

View File

@@ -1,45 +0,0 @@
Events
======
Message Events
--------------
Configuring Message Events
^^^^^^^^^^^^^^^^^^^^^^^^^^
.. figure:: figures/throw_message_event.png
:scale: 60%
:align: center
Throw Message Event configuration
.. figure:: figures/message_start_event.png
:scale: 60%
:align: center
Message Catch Event configuration
The Throw Message Event Implementation should be 'Expression' and the Expression should
be a Python statement that can be evaluated. In this example, we'll just send the contents
of the :code:`reason_delayed` variable, which contains the response from the 'Investigate Delay'
Task.
We can provide a name for the result variable, but I have not done that here, as it does not
make sense to me for the generator of the event to tell the handler what to call the value.
If you *do* specify a result variable, the message payload (the expression evaluated in the
context of the Throwing task) will be added to the handling task's data in a variable of that
name; if you leave it blank, SpiffWorkflow will create a variable of the form <Handling
Task Name>_Response.
Running the Model
^^^^^^^^^^^^^^^^^
If you have set up our example repository, this model can be run with the
following command:
.. code-block:: console
./camunda-bpmn-runner.py -c order_collaboration \
-d bpmn/tutorial/product_prices.dmn bpmn/tutorial/shipping_costs.dmn \
-b bpmn/camunda/events.bpmn bpmn/camunda/call_activity.bpmn

131
doc/bpmn/camunda/index.rst Normal file
View File

@@ -0,0 +1,131 @@
Using the Camunda Configuration Module
======================================
.. warning:: There is a better way ...
SpiffWorkflow does not aim to support all of Camunda's proprietary extensions.
Many of of the items in the Camunda Properties Panel do not work. And
major features of SpiffWorkflow (Messages, Data Objects, Service Tasks, Pre-Scripts, etc...)
can not be configured in the Camunda editor. Use `SpiffArena <https://www.spiffworkflow.org/posts/articles/get_started/>`_
to build and test your BPMN models instead!
Earlier users of SpiffWorkflow relied heavily on Camunda's modeler and several of our task spec
implementations were based on Camunda's extensions. Support for these extensions has been moved
to the :code:`camunda` package. We are not actively maintaining this package (though we will
accept contributions from Camunda users!). Please be aware that many of the Camunda extensions
that will appear in the Camunda editor do not work with SpiffWorkflow.
In this repo, we provide the following configuration:
.. code-block:: console
./runner.py -e spiff_example.camunda.sqlite
Tasks
=====
User Tasks
----------
Creating a User Task
^^^^^^^^^^^^^^^^^^^^
When you click on a user task in the BPMN modeler, the Properties Panel includes a form tab. Use this
tab to build your questions.
The following example shows how a form might be set up in Camumda.
.. figure:: figures/user_task.png
:scale: 30%
:align: center
User Task configuration
Manual Tasks
------------
Creating a Manual Task
^^^^^^^^^^^^^^^^^^^^^^
We can use the BPMN element Documentation field to display more information about the context of the item.
Spiff is set up in a way that you could use any templating library you want, but we have used
`Jinja <https://jinja.palletsprojects.com/en/3.0.x/>`_.
In this example, we'll present an order summary to our customer.
.. figure:: figures/documentation.png
:scale: 30%
:align: center
Element Documentation
Example Code
------------
Example Human task handlers can be found in :app:`camunda/curses_handlers.py`.
Events
======
Message Events
--------------
Configuring Message Events
^^^^^^^^^^^^^^^^^^^^^^^^^^
.. figure:: figures/throw_message_event.png
:scale: 60%
:align: center
Throw Message Event configuration
.. figure:: figures/message_start_event.png
:scale: 60%
:align: center
Message Catch Event configuration
The Throw Message Event Implementation should be 'Expression' and the Expression should
be a Python statement that can be evaluated. In this example, we'll just send the contents
of the :code:`reason_delayed` variable, which contains the response from the 'Investigate Delay'
Task.
We can provide a name for the result variable, but I have not done that here, as it does not
make sense to me for the generator of the event to tell the handler what to call the value.
If you *do* specify a result variable, the message payload (the expression evaluated in the
context of the Throwing task) will be added to the handling task's data in a variable of that
name; if you leave it blank, SpiffWorkflow will create a variable of the form <Handling
Task Name>_Response.
MultiInstance Tasks
===================
Earlier versions of SpiffWorkflow relied on the properties available in the Camunda MultiInstance Panel.
.. figure:: figures/multiinstance_task_configuration.png
:scale: 60%
:align: center
MultiInstance Task configuration
SpiffWorkflow has a MultiInstance Task spec in the :code:`camunda` package that interprets these fields
in the following way:
* Loop Cardinality:
- If this is an integer, or a variable that evaluates to an integer, this number would be
used to determine the number of instances
- If this is a collection, the size of the collection would be used to determine the number of
instances
* Collection: the output collection (input collections have to be specified in the "Cardinality" field).
* Element variable: the name of the varible to copy the item into for each instance.
.. warning::
The spec in this package is based on an old version of Camunda, so the panel may have changed. The
properties might or might not have been the way Camunda used these fields, and may or may not be similar
to newer or current versions. *Use at your own risk!*

View File

@@ -1,30 +0,0 @@
MultiInstance Tasks
===================
Earlier versions of SpiffWorkflow relied on the properties available in the Camunda MultiInstance Panel.
.. figure:: figures/multiinstance_task_configuration.png
:scale: 60%
:align: center
MultiInstance Task configuration
SpiffWorkflow has a MultiInstance Task spec in the :code:`camunda` package that interprets these fields
in the following way:
* Loop Cardinality:
- If this is an integer, or a variable that evaluates to an integer, this number would be
used to determine the number of instances
- If this is a collection, the size of the collection would be used to determine the number of
instances
* Collection: the output collection (input collections have to be specified in the "Cardinality" field).
* Element variable: the name of the varible to copy the item into for each instance.
.. warning::
The spec in this package is based on an old version of Camunda, which might or might not have been the
way Camunda uses these fields, and may or may not be similar to newer or current versions.
*Use at your own risk!*

View File

@@ -1,23 +0,0 @@
Camunda Editor Support
======================
.. warning:: There is a better way ...
SpiffWorkflow does not aim to support all of Camunda's proprietary extensions.
Many of of the items in the Camunda Properties Panel do not work. And
major features of SpiffWorkflow (Messages, Data Objects, Service Tasks, Pre-Scripts, etc...)
can not be configured in the Camunda editor. Use `SpiffArena <https://www.spiffworkflow.org/posts/articles/get_started/>`_
to build and test your BPMN models instead!
Earlier users of SpiffWorkflow relied heavily on Camunda's modeler and several of our task spec
implementations were based on Camunda's extensions. Support for these extensions has been moved
to the :code:`camunda` package. We are not actively maintaining this package (though we will
accept contributions from Camunda users!). Please be aware that many of the Camunda extensions
that will appear in the Camunda editor do not work with SpiffWorkflow.
.. toctree::
:maxdepth: 3
tasks
events
multiinstance

View File

@@ -1,104 +0,0 @@
Tasks
=====
User Tasks
----------
Creating a User Task
^^^^^^^^^^^^^^^^^^^^
When you click on a user task in the BPMN modeler, the Properties Panel includes a form tab. Use this
tab to build your questions.
The following example shows how a form might be set up in Camumda.
.. figure:: figures/user_task.png
:scale: 30%
:align: center
User Task configuration
Manual Tasks
------------
Creating a Manual Task
^^^^^^^^^^^^^^^^^^^^^^
We can use the BPMN element Documentation field to display more information about the context of the item.
Spiff is set up in a way that you could use any templating library you want, but we have used
`Jinja <https://jinja.palletsprojects.com/en/3.0.x/>`_.
In this example, we'll present an order summary to our customer.
.. figure:: figures/documentation.png
:scale: 30%
:align: center
Element Documentation
Running The Model
-----------------
If you have set up our example repository, this model can be run with the
following command:
.. code-block:: console
./camunda-bpmn-runner.py -p order_product -d bpmn/tutorial/product_prices.dmn -b bpmn/camunda/task_types.bpmn
Example Application Code
------------------------
Handling the User Task
^^^^^^^^^^^^^^^^^^^^^^
.. code:: python
dct = {}
for field in task.task_spec.form.fields:
if isinstance(field, EnumFormField):
option_map = dict([ (opt.name, opt.id) for opt in field.options ])
options = "(" + ', '.join(option_map) + ")"
prompt = f"{field.label} {options} "
option = input(prompt)
while option not in option_map:
print(f'Invalid selection!')
option = input(prompt)
response = option_map[option]
else:
response = input(f"{field.label} ")
if field.type == "long":
response = int(response)
update_data(dct, field.id, response)
DeepMerge.merge(task.data, dct)
The list of form fields for a task is stored in :code:`task.task_spec.form_fields`.
For Enumerated fields, we want to get the possible options and present them to the
user. The variable names of the fields were stored in :code:`field.id`, but since
we set labels for each of the fields, we'd like to display those instead, and map
the user's selection back to the variable name.
For other fields, we'll just store whatever the user enters, although in the case
where the data type was specified to be a :code:`long`, we'll convert it to a
number.
Finally, we need to explicitly store the user-provided response in a variable
with the expected name with :code:`update_data(dct, field.id, response)` and merge
the newly collected data into our task data with :code:`DeepMerge.merge(task.data, dct)`.
Our :code:`update_data` function handles "dot notation" in field names, which creates
nested dictionaries based on the path components.
.. code:: python
def update_data(dct, name, value):
path = name.split('.')
current = dct
for component in path[:-1]:
if component not in current:
current[component] = {}
current = current[component]
current[path[-1]] = value

View File

@@ -1,60 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
numfig = True
# -- Project information -----------------------------------------------------
project = 'SpiffWorkflow-BPMN Documentation'
copyright = '2020, Sartography'
author = 'Sartography'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc', # 'sphinx.ext.coverage',
'sphinx.ext.viewcode',
'sphinx.ext.autosummary',
#'sphinx.ext.intersphinx',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'default'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
intersphinx_mapping = {'http://docs.python.org/': None}

View File

@@ -11,11 +11,12 @@ First we'll create a new class
.. code:: python
from SpiffWorkflow.bpmn.specs.event_definitions import TimerEventDefinition, NoneEventDefinition
from SpiffWorkflow.bpmn.specs.mixins.events.start_event import StartEvent
from SpiffWorkflow.spiff.specs.spiff_task import SpiffBpmnTask
from SpiffWorkflow.bpmn.specs.event_definitions import NoneEventDefinition
from SpiffWorkflow.bpmn.specs.event_definitions.timer import TimerEventDefinition
from SpiffWorkflow.bpmn.specs.mixins import StartEventMixin
from SpiffWorkflow.spiff.specs import SpiffBpmnTask
class CustomStartEvent(StartEvent, SpiffBpmnTask):
class CustomStartEvent(StartEventMixin, SpiffBpmnTask):
def __init__(self, wf_spec, bpmn_id, event_definition, **kwargs):
@@ -26,13 +27,14 @@ First we'll create a new class
super().__init__(wf_spec, bpmn_id, event_definition, **kwargs)
self.timer_event = None
When we create our custom event, we'll check to see if we're creating a Start Event with a TimerEventDefinition, and if so,
we'll replace it with a NoneEventDefinition.
When we create our custom event, we'll check to see if we're creating a Start Event with a :code:`TimerEventDefinition`, and
if so, we'll replace it with a :code:`NoneEventDefinition`. There are three different types of Timer Events, so we'll use
the base class for all three to make sure we account for TimeDate, Duration, and Cycle.
.. note::
Our class inherits from two classes. We import a mixin class that defines generic BPMN Start Event behavior from
:code:`StartEvent` in the :code:`bpmn` package and the :code:`SpiffBpmnTask` from the :code:`spiff` package, which
:code:`StartEventMixin` in the :code:`bpmn` package and the :code:`SpiffBpmnTask` from the :code:`spiff` package, which
extends the default :code:`BpmnSpecMixin`.
We've split the basic behavior for specific BPMN tasks from the :code:`BpmnSpecMixin` to make it easier to extend
@@ -44,10 +46,10 @@ Whenever we create a custom task spec, we'll need to create a converter for it s
.. code:: python
from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer
from SpiffWorkflow.bpmn.serializer.task_spec import StartEventConverter
from SpiffWorkflow.bpmn.serializer import BpmnWorkflowSerializer
from SpiffWorkflow.bpmn.serializer.default import EventConverter
from SpiffWorkflow.spiff.serializer.task_spec import SpiffBpmnTaskConverter
from SpiffWorkflow.spiff.serializer.config import SPIFF_SPEC_CONFIG
from SpiffWorkflow.spiff.serializer import DEFAULT_CONFIG
class CustomStartEventConverter(SpiffBpmnTaskConverter):
@@ -63,21 +65,20 @@ Whenever we create a custom task spec, we'll need to create a converter for it s
return dct
SPIFF_SPEC_CONFIG['task_specs'].remove(StartEventConverter)
SPIFF_SPEC_CONFIG['task_specs'].append(CustomStartEventConverter)
wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter(SPIFF_SPEC_CONFIG)
serializer = BpmnWorkflowSerializer(wf_spec_converter)
DEFAULT_CONFIG['task_specs'].remove(StartEventConverter)
DEFAULT_CONFIG['task_specs'].append(CustomStartEventConverter)
registry = BpmnWorkflowSerializer.configure(DEFAULT_CONFIG)
serializer = BpmnWorkflowSerializer(registry)
Our converter will inherit from the :code:`SpiffBpmnTaskConverter`, since that's our base generic BPMN mixin class.
The :code:`SpiffBpmnTaskConverter` ultimately inherits from
The :code:`SpiffBpmnTaskConverter` itself inherits from
:code:`SpiffWorkflow.bpmn.serializer.helpers.task_spec.BpmnTaskSpecConverter`. which provides some helper methods for
extracting standard attributes from tasks; the :code:`SpiffBpmnTaskConverter` does the same for extensions from the
:code:`spiff` package.
We don't have to do much -- all we do is replace the event definition with the original. The timer event will be
moved when the task is restored.
moved when the task is restored, and this saves us from having to write a custom parser.
.. note::
@@ -86,31 +87,22 @@ moved when the task is restored.
way to make this a little easier to follow.
When we create our serializer, we need to tell it about this task. We'll remove the converter for the standard Start
Event and add the one we created to the confiuration and create the :code:`workflow_spec_converter` from the updated
config.
Event and add the one we created to the configuration. We then get a registry of classes that the serializer knows
about that includes our custom spec, as well as all the other specs and initialize the serializer with it.
.. note::
We have not instantiated our converter class. When we call :code:`configure_workflow_spec_converter` with a
configuration (which is essentially a list of classes, split up into sections for organizational purposes),
*it* instantiates the classes for us, using the same `registry` for every class. At the end of the configuration
if returns this registry, which now knows about all of the classes that will be used for SpiffWorkflow
specifications. It is possible to pass a separately created :code:`DictionaryConverter` preconfigured with
other converters; in that case, it will be used as the base `registry`, to which specification conversions will
be added.
Because we've built up the `registry` in such a way, we can make use of the :code:`registry.convert` and
:code:`registry.restore` methods rather than figuring out how to serialize them. We can use these methods on any
objects that SpiffWorkflow knows about.
See :doc:`advanced` for more information about the serializer.
The reason there are two steps involved (regurning a registry and *then* passing it to the serializer) rather
that using the configuration directly is to allow further customization of the :code:`registry`. Workflows
can contain arbtrary data, we want to provide developers the ability to serialization code for any object. See
:ref:`serializing_custom_objects` for more information about how this works.
Finally, we have to update our parser:
.. code:: python
from SpiffWorkflow.spiff.parser import SpiffBpmnParser
from SpiffWorkflow.spiff.parser.event_parsers import StartEventParser
from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser
from SpiffWorkflow.bpmn.parser.util import full_tag
parser = SpiffBpmnParser()
@@ -122,12 +114,10 @@ will use. This is a bit unintuitive, but that's how it works.
Fortunately, we were able to reuse an existing Task Spec parser, which simplifies the process quite a bit.
Having created a parser and serializer, we could replace the ones we pass in the the :code:`SimpleBpmnRunner` with these.
Having created a parser and serializer, we could create a configuration module and instantiate an engine with these
components.
I am going to leave creating a script that makes use of them to readers of this document, as it should be clear enough
how to do.
There is a very simple diagram `bpmn/tutorial/timer_start.bpmn` with the process ID `timer_start` with a Start Event
There is a very simple diagram :bpmn:`timer_start.bpmn` with the process ID `timer_start` with a Start Event
with a Duration Timer of one day that can be used to illustrate how the custom task works. If you run this workflow
with `spiff-bpmn-runner.py`, you'll see a `WAITING` Start Event; if you use the parser and serializer we just created,
you'll be propmted to complete the User Task immediately.
with any of the configurations provided, you'll see a `WAITING` Start Event; if you use the parser and serializer we
just created, you'll be propmted to complete the User Task immediately.

View File

@@ -1,65 +1,26 @@
Data
====
BPMN Model
----------
We'll be using the following files from `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_.
- `bpmn-spiff/events <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/events.bpmn>`_ workflow
- `bpmn-spiff/call_activity <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/call_activity.bpmn>`_ workflow
- `bpmn-spiff/data_output <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/data_output.bpmn>`_ workflow
- `product_prices <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/product_prices.dmn>`_ DMN table
- `shipping_costs <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/shipping_costs.dmn>`_ DMN table
Data Objects
^^^^^^^^^^^^
------------
Data Objects exist at the process level and are not visible in the diagram, but when you create a Data Object
Reference, you can choose what Data Object it points to.
Data Objects exist at the process level and are not visible to individual Tasks unless explicitly indicated (ie there
is a line from the Data Object to or from a Task).
.. figure:: figures/data/data_object_configuration.png
:scale: 50%
:align: center
All Workflows have a :code:`data` attribute; like :code:`Task.data`, it is just a dictionary, and this is where the
values for Data Objects are stored.
Configuring a Data Object Reference
If a Data Object is to be made available to a Task, the value is copied from the Workflow data into the Task data
immediately before the Task becomes :code:`READY`; when the Task completes, the Data Object value is removed from the
Task data.
When a Data Output association (a line) is drawn from a task to a Data Object Reference, the value is copied
from the task data to the workflow data and removed from the task. If a Data Input Association is created from
a Data Object Reference, the value is temporarily copied into the task data while the task is being executed,
and immediately removed afterwards.
This allows sensitive data to be removed from individual tasks (in our example, the customer's credit card
number). It can also be used to prevent large objects from being repeatedly copied from task to task.
Multiple Data Object References can point to the same underlying data. In our example, we use two references
to the same Data Object to pass the credit card info to both tasks that require it. On the right panel, we can
see that only one data object exists in the process.
.. figure:: figures/data/data_objects.png
:scale: 30%
:align: center
Data objects in a process
If you step through this workflow, you'll see that the card number is not contained in the task data after
the 'Enter Payment Info' has been completed but is available to the 'Charge Customer' task later on.
Running The Model
*****************
If you have set up our example repository, this model can be run with the following command:
.. code-block:: console
./spiff-bpmn-runner.py -c order_collaboration \
-d bpmn/tutorial/product_prices.dmn bpmn/tutorial/shipping_costs.dmn \
-b bpmn/tutorial/events.bpmn bpmn/tutorial/call_activity.bpmn
If value is to be written to a Data Object, it is stored in the Workflow data and removed from the Task data when the
Task completes.
Data Objects are always available to Gateways (even though you cannot draw a line on the diagram).
Data Inputs and Outputs
^^^^^^^^^^^^^^^^^^^^^^^
-----------------------
In complex workflows, it is useful to be able to specify required Data Inputs and Outputs, especially for Call Activities
given that they are external and might be shared across many different processes.
@@ -69,30 +30,23 @@ be copied into the activity and copy *only* the variables you've specified as in
SpiffWorkflow will copy *only* the variables you've specified from the Call Activity at the end of the process. If any
of the variables are missing, SpiffWorkflow will raise an error.
Our product customization Call Activity does not require any input, but the output of the process is the product
name and quantity. We can add corresponding Data Outputs for those.
.. figure:: figures/data/data_output.png
:scale: 30%
:align: center
Our product customization Call Activity (:bpmn:`call_activity.bpmn`) does not require any input, but the output of the
process is the product name and quantity. We can add corresponding Data Outputs for those.
Data Outputs in a Call Activity
If you use the alternate version of this Call Activity (:bpmn:`data_output.bpmn`) and choose a product that has
customizations, when you inspect the data after the Call Activity completes, you'll see that the customizations have been
removed.
If you use this version of the Call Activity and choose a product that has customizations, when you inspect the data
after the Call Activity completes, you'll see that the customizations have been removed. We won't continue to use this
version of the Call Activity, because we want to preserve all the data.
To load this example (product D has two customizations):
.. code-block:: console
./runner.py -e spiff_example.spiff.file add -p order_product \
-b bpmn/tutorial/{top_level,data_output}.bpmn \
-d bpmn/tutorial/{shipping_costs,product_prices}.dmn
.. note::
The BPMN spec allows *any* task to have Data Inputs and Outputs. Our modeler does not provide a way to add them to
arbitrary tasks, but SpiffWorkflow will recognize them on any task if they are present in the BPMN XML.
Running The Model
*****************
If you have set up our example repository, this model can be run with the following command:
.. code-block:: console
./spiff-bpmn-runner.py -p order_product \
-d bpmn/tutorial/product_prices.dmn bpmn/tutorial/shipping_costs.dmn \
-b bpmn/tutorial/top_level.bpmn bpmn/tutorial/data_output.bpmn

View File

@@ -1,279 +0,0 @@
Events
======
BPMN Model
----------
We'll be using the following files from `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_.
- `transaction <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/transaction.bpmn>`_ workflow
- `signal_event <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/signal_event.bpmn>`_ workflow
- `events <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/events.bpmn>`_ workflow
- `call activity <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/call_activity.bpmn>`_ workflow
- `product_prices <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/product_prices.dmn>`_ DMN table
- `shipping_costs <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/shipping_costs.dmn>`_ DMN table
A general overview of events in BPMN can be found in the :doc:`overview`
section of the documentation.
SpiffWorkflow supports the following Event Definitions:
- `Cancel Events`_
- `Signal Events`_
- `Terminate Events`_
- `Error Events`_
- `Escalation Events`_
- `Timer Events`_
- `Message Events`_
We'll include examples of all of these types in this section.
.. note::
SpiffWorflow can also support Multiple Event definitions, but our modeler does not allow you to create them,
so we will not delve into them further here.
Transactions
^^^^^^^^^^^^
We also need to introduce the concept of a Transaction because certain events
can only be used in that context. A Transaction is essentially a Subprocess, but
it must fully complete before it affects its outer workflow.
We'll make our customer's ordering process through the point they review their order
into a Transaction. If they do not complete their order, then product selections and
customizations will be discarded; if they place the order, the workflow will proceed
as before.
We'll also introduce our first event type, the Cancel Event. Cancel Events can
only be used in Transactions.
Cancel Events
^^^^^^^^^^^^^
.. figure:: figures/events/transaction.png
:scale: 30%
:align: center
Workflow with a Transaction and Cancel Event
We changed our 'Review Order' Task to be a User Task and have added a form, so
that we can give the customer the option of cancelling the order. If the customer
answers 'Y', then the workflow ends normally and we proceed to collecting
payment information.
However, if the user elects to cancel their order, we use a Cancel End Event
instead, which generates a Cancel Event. We can then attach a Cancel Boundary
Event to the Transaction, and execute that path if the event occurs. Instead of
asking the customer for their payment info, we'll direct them to a form and ask
them why they cancelled their order.
If the order is placed, the workflow will contain the order data; if it is
cancelled, it will contain the reason for cancellation instead.
To run this workflow
.. code-block:: console
./spiff-bpmn-runner.py -p order_product \
-d bpmn/tutorial/product_prices.dmn bpmn/tutorial/shipping_costs.dmn \
-b bpmn/tutorial/transaction.bpmn bpmn/tutorial/call_activity.bpmn
Signal Events
^^^^^^^^^^^^^
.. figure:: figures/events/signal_event.png
:scale: 30%
:align: center
Workflow with Signal Events
Suppose we also want to give our customer the ability to cancel their order at
any time up until they are charged. We need to throw an event after the charge
is placed and catch this event before the user completes the 'Cancel Order' task.
Once the charge is placed, the task that provides the option to cancel will
itself be cancelled when the charge event is received.
We'll also need to detect the case that the customer cancels their order and
cancel the charge task if it occurs; we'll use a separate Signal for that.
Multiple tasks can catch the same Signal Event. Suppose we add a Manager role
to our Process, and allow the Employee to refer unsuccessful charges to the
Manager for resolution. The Manager's task will also need to catch the 'Order
Cancelled' Signal Event.
Signals are referred to by name.
.. figure:: figures/events/throw_signal_event.png
:scale: 60%
:align: center
Signal Event configuration
Terminate Events
^^^^^^^^^^^^^^^^
We also added a Terminate Event to the Manager Workflow. A regular End Event
simply marks the end of a path. A Terminate Event will indicate that the
entire Process is complete and any remaining tasks should be cancelled. Our
customer cannot cancel an order that has already been cancelled, and we won't ask
them for feedback about it (we know that is was because we were unable to charge
them for it), so we do not want to execute either of those tasks.
To run this workflow
.. code-block:: console
./spiff-bpmn-runner.py -p order_product \
-d bpmn/tutorial/product_prices.dmn bpmn/tutorial/shipping_costs.dmn \
-b bpmn/tutorial/signal_event.bpmn bpmn/tutorial/call_activity.bpmn
We'll now modify our workflow to add an example of each of the other types of
events that SpiffWorkflow supports.
Error Events
^^^^^^^^^^^^
Let's turn to our order fulfillment subprocess. Either of these steps could
potentially fail, and we may want to handle each case differently.
.. figure:: figures/events/events.png
:scale: 30%
:align: center
Workflow with multiple event types
One potential failure is that our product is unavailable. This actually might be
a temporary problem, but we'll assume that it is a show stopper for the sake of
this tutorial.
We ask the Employee to verify that they were able to retrieve the product; if they
were unable to do so, then we generate an Error End Event, which we will handle
with an Interrupting Error Boundary Event (Error events are *always* interrupting).
If the product is unavailable, our Manager will notify the customer, issue a refund,
and cancel the order.
Escalation Events
^^^^^^^^^^^^^^^^^
Escalation events are a lot like Error Events and as far as I can tell, which one
to use comes down to preference, with the caveat that if you want to use an Intermediate
Event, you'll have to use Escalation, because BPMN does not allow Intermediate Error Events,
and that Error Events cannot be Non-Interrupting.
In our example, we'll assume that if we failed to ship the product, we can try again later,
so, we will not end the Subprocess (Escalation events can be either Interrupting or
Non-Interrupting).
However, we still want to notify our customer of a delay, so we use a Non-Interrupting
Escalation Boundary Event.
Both Error and Escalation Events can be optionally associated with a code. Here is
Throw Event for our `product_not_shipped` Escalation.
.. figure:: figures/events/throw_escalation_event.png
:scale: 60%
:align: center
Throw Escalation Event configuration
Error Event configuration is similar.
If no code is provided in a Catch event, it can be caught by any Escalation with the same
name.
Timer Events
^^^^^^^^^^^^
In the previous section, we mentioned that that we would try again later if we were unable
to ship the order. We can use a Duration Timer Event to force our workflow to wait a certain
amount of time before continuing. We can use this as a regular Intermediate Event (in
'Try Again Later') or a Boundary Event. Timer Boundary Events can be Interrupting, but in
this case, we simply want to notify the customer of the delay while continuing to process
their order, so we use a Non-Interrupting Event.
.. figure:: figures/events/timer_event.png
:scale: 60%
:align: center
Duration Timer Event configuration
We express the duration as an ISO8601 duration.
.. note::
We enclosed the string in quotes, because it is possible to use a variable to determine
how long the timer should wait.
It is also possible to use a static date and time to trigger an event. It will also need to be
specified in ISO8601 format.
Timer events can only be caught, that is waited on. The timer begins implicitly when we
reach the event.
Message Events
^^^^^^^^^^^^^^
In BPMN, Messages are used to communicate across processes. Technically, Messages are not
intended to be used inside a single Process, but Spiff does support this use.
Messages are similar to Signals, in that they are referenced by name, but they have the
additional property that they may contain a payload. The payload is a bit of python code that will be
evaluated against the task data and sent along with the Message. In the corresponding Message Catch
Event or Receive Task, we define a variable name where we'll store the result.
We've added a QA process to our model, which will be initiated whenever an order takes too long
to fulfill. We'll send the reason for the delay in the Message.
Spiff Messages can also optionally use Correlation Keys. The Correlation Key is an expression or set of
expressions that are evaluated against a Message payload to create an additional identifier for associating
messages with Processes.
In our example, it is possible that multiple QA processes could be started (the timer event will fire every
two minutes until the order fulfillment process is complete, or more realistically, they could be
investigating many entirely different orders, even if our simple runner does not handle that case).
In this case, the Message name is insufficient, as there will be multiple Processes that can accept
Messages based on the name.
.. figure:: figures/events/correlation.png
:scale: 50%
:align: center
Defining a correlation key
We use the timestamp of the Message creation as a unique key that can be used to distinguish between multiple
QA Processes.
.. figure:: figures/events/throw_message_event.png
:scale: 50%
:align: center
Configuring a message throw event
When we receive the event, we assign the payload to :code:`order_info`.
.. figure:: figures/events/catch_message_event.png
:scale: 50%
:align: center
Configuring a message catch event
The correlation is visible on both the Throw and Catch Events, but it is associated with the message rather
than the tasks themselves; if you update the expression on either event, the changes will appear in both places.
Running The Model
^^^^^^^^^^^^^^^^^
.. code-block:: console
./spiff-bpmn-runner.py -c order_collaboration \
-d bpmn/tutorial/product_prices.dmn bpmn/tutorial/shipping_costs.dmn \
-b bpmn/tutorial/events.bpmn bpmn/tutorial/call_activity.bpmn
.. note::
We're specifying a collaboration rather than a process so that SpiffWorkflow knows that there is more than
one top-level process.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 242 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,86 +0,0 @@
Gateways
========
BPMN Model
----------
In this section, we'll expand our model by creating alternate paths through the
workflow depending on the current workflow state, in this case, answers provided
by the user through forms.
We've also added a second DMN table to find the cost of the selected shipping
method, and we updated our order total calculations to incorporate that cost.
We'll be using the following files from `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_.
- `gateway_types <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/gateway_types.bpmn>`_ workflow
- `product_prices <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/product_prices.dmn>`_ DMN table
- `shipping_costs <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/shipping_costs.dmn>`_ DMN table
Exclusive Gateway
^^^^^^^^^^^^^^^^^
Exclusive Gateways are used when exactly one alternative can be selected.
Suppose our products are T-shirts and we offer product C in several colors. After
the user selects a product, we check to see it if is customizable. Our default
branch will be 'Not Customizable', but we'll direct the user to a second form
if they select 'C'; our condition for choosing this branch is a simple python
expression.
.. figure:: figures/gateways/exclusive_gateway.png
:scale: 30%
:align: center
Flow configuration
Parallel Gateway
^^^^^^^^^^^^^^^^
.. sidebar:: IDs vs Names
We've assigned descriptive names to all our tasks so far. Text added to
the Name field will appear in the diagram, so sometimes it's better to
leave it blank to avoid visual clutter. I've put a description of the
gateway into the ID field instead.
Parallel Gateways are used when the subsequent tasks do not need to be completed
in any particular order. The user can complete them in any sequence and the
workflow will wait for all tasks to be finished before advancing.
We do not care whether the user chooses a shipping method or enters their
address first, but they'll need to complete both tasks before continuing.
We don't need to do any particular configuration for this gateway type.
.. figure:: figures/gateways/parallel_gateway.png
:scale: 30%
:align: center
Parallel Gateway example
Inclusive Gateway
^^^^^^^^^^^^^^^^^
SpiffWorkflow also supports Inclusive Gateways, though we do not have an example of this gateway
type in this tutorial. Inclusive Gateways have conditions on outgoing flows like Exclusive Gateways,
but unlike Exclusive Gateways, multiple paths may be taken if more than one conition is met.
Event-Based Gateway
^^^^^^^^^^^^^^^^^^^
SpiffWorkflow supports Event-Based Gateways, though we do not use them in this tutorial. Event-Based
gateways select an outgoing flow based on an event. We'll discuss events in the next section.
Running The Model
^^^^^^^^^^^^^^^^^
If you have set up our example repository, this model can be run with the
following command:
.. code-block:: console
./spiff-bpmn-runner.py -p order_product \
-d bpmn/tutorial/product_prices.dmn bpmn/tutorial/shipping_costs.dmn \
-b bpmn/tutorial/gateway_types.bpmn

155
doc/bpmn/imports.rst Normal file
View File

@@ -0,0 +1,155 @@
Creating and Running Workflows
==============================
.. code-block:: python
from SpiffWorkflow.bpmn import BpmnWorkflow, BpmnEvent
from SpiffWorfkflow import TaskState
Parsing
=======
Basic Parsing
-------------
.. code-block:: python
from SpiffWorkflow.bpmn.parser import BpmnParser, BpmnValidator
Customized Parsing
------------------
.. code-block:: python
from SpiffWorkflow.bpmn.parser import TaskParser, EventDefinitionParser
Examples
--------
- :doc:`parsing`
- :doc:`custom_task_spec`
Script Engine
=============
To modify the default execution environment
-------------------------------------------
.. code-block:: python
from SpiffWorkflow.bpmn.script_engine import TaskDataEnvironment
To control how the engine interacts with the workflow
-----------------------------------------------------
.. code-block:: python
from SpiffWorkflow.bpmn.script_engine import PythonScriptEngine
To implement custom exec/eval
-----------------------------
.. code-block:: python
from SpiffWorkflow.bpmn.script_engine import BasePythonScriptEngineEnvironment
Examples
--------
- :doc:`script_engine`
Specs
=====
Using a Spec
------------
.. code-block:: python
from SpiffWorkflow.bpmn.specs import <TaskSpec>
from SpiffWorkflow.bpmn.specs.event_definition import <EventDefinition>
Extending a Spec
----------------
.. code-block:: python
from SpiffWorkflow.bpmn.specs import BpmnTaskSpec # Implements generic BPMN behavior
from SpiffWorkflow.bpmn.specs.mixins import <TaskSpecMixin> # Implements specific BPMN behavior
Implement a Datastore
---------------------
.. code-block:: python
from SpiffWorkflow.bpmn.spec import BpmnDataStoreSpecification
Examples
--------
- :doc:`workflows`
- :doc:`custom_task_spec`
Serializer
==========
Basic Usage
-----------
.. code-block:: python
from SpiffWorkflow.bpmn.serializer import BpmnWorkflowSerializer
Custom Data
-----------
.. code-block:: python
from SpiffWorkflow.bpmn.serializer import DefaultRegistry
Spec Customizations
-------------------
.. code-block:: python
from SpiffWorkflow.bpmn.serializer import DEFAULT_CONFIG
from SpiffWorkflow.bpmn.serializer.default import <TaskSpecConverter>
from SpiffWorkflow.bpmn.serializer.helpers import (
TaskSpecConverter,
EventDefinitionConverter,
BpmnDataSpecificationConverter,
)
Examples
--------
- :doc:`serialization`
- :doc:`custom_task_specs`
DMN
===
.. code-block:: python
from SpiffWorkflow.dmn.parser import BpmnDmnParser
from SpiffWorkflow.dmn.specs import BusinessRuleTaskMixin
from SpiffWorkflow.dmn.serializer import BaseBusinessRuleTaskConverter
Spiff
=====
.. code-block:: python
from SpiffWorkflow.spiff.parser import SpiffBpmnParser, VALIDATOR
from SpiffWorkflow.spiff.specs import <TaskSpec>
from SpiffWorkflow.spiff.serializer import DEFAULT_CONFIG
Camunda
=======
.. code-block:: python
from SpiffWorkflow.camunda.parser import CamundaParser
from SpiffWorkflow.camunda.specs import <TaskSpec>
from SpiffWorkfllw.camunda.serializer import DEFAULT_CONFIG

107
doc/bpmn/index.rst Normal file
View File

@@ -0,0 +1,107 @@
SpiffWorkflow and BPMN
======================
All the Python code and BPMN models used here are available in an example
project `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_
This example application serves two purposes:
- to illustate what a developer will need to do to use this library
- to let library users experiment without fully building out their own system
The Example Application
-----------------------
.. toctree::
:maxdepth: 2
application
Parsing
-------
.. toctree::
:maxdepth: 2
parsing
Workflows and Tasks
-------------------
.. toctree::
:maxdepth: 2
workflows
Scripting Environment
---------------------
.. toctree::
:maxdepth: 2
script_engine
Data
----
.. toctree::
:maxdepth: 2
data
Serialization
-------------
.. toctree::
:maxdepth: 2
serialization
Custom Tasks
------------
.. toctree::
:maxdepth: 2
custom_task_spec
Logging
-------
.. toctree::
:maxdepth: 2
logging
Exceptions
----------
.. toctree::
:maxdepth: 2
errors
What's in the Module and Where to Find It
-----------------------------------------
.. toctree::
:maxdepth: 1
imports
Supported BPMN Elements
-----------------------
.. toctree::
:maxdepth: 2
supported
Camunda Editor Support
----------------------
.. toctree::
:maxdepth: 2
camunda/index

View File

@@ -1,103 +0,0 @@
BPMN Workflows
==============
The basic idea of SpiffWorkflow is that you can use it to write an interpreter
in Python that creates business applications from BPMN models. In this section,
we'll develop a model of a reasonably complex process and show how to run it.
We expect that readers will fall into two general categories:
- People with a background in BPMN who might not be very familiar Python
- Python developers who might not know much about BPMN
This section of the documentation provides an example that (hopefully) serves
the needs of both groups. We will introduce some of the more common BPMN
elements and show how to build a simple workflow runner around them.
SpiffWorkflow does heavy-lifting such as keeping track of task dependencies and
states and providing the ability to serialize or deserialize a workflow that
has not been completed. The developer will write code for displaying workflow
state and presenting tasks to users of their application.
All the Python code and BPMN models used here are available in an example
project called `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_.
Quickstart
----------
Check out the code in `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_
and follow the instructions to set up an environment to run it in.
Run the sample workflow we built up using our example application with the following
command:
.. code-block:: console
./spiff-bpmn-runner.py -c order_collaboration \
-d bpmn/tutorial/{product_prices,shipping_costs}.dmn \
-b bpmn/tutorial/{top_level_multi,call_activity_multi}.bpmn
.. sidebar:: BPMN Runner
The example app provides a utility for running BPMN Diagrams from the command
line that will allow you to introspect a bit on a running process. You
can see the options available by running:
./spiff-bpmn-runner.py --help
The code in the workflow runner and the models in the bpmn directory of the
repository will be discussed in the remainder of this tutorial.
Supported BPMN Elements
-----------------------
.. toctree::
:maxdepth: 3
tasks
gateways
organization
events
data
multiinstance
Putting it All Together
-----------------------
.. toctree::
:maxdepth: 2
synthesis
Features in More Depth
----------------------
.. toctree::
:maxdepth: 2
advanced
Custom Task Specs
-----------------
.. toctree::
:maxdepth: 2
custom_task_spec
Exceptions
----------
.. toctree::
:maxdepth: 2
errors
Camunda Editor Support
----------------------
.. toctree::
:maxdepth: 2
camunda/support

45
doc/bpmn/logging.rst Normal file
View File

@@ -0,0 +1,45 @@
Logging
=======
Spiff provides several loggers:
- the :code:`spiff` logger, which emits messages when a workflow is initialized and when tasks change state
- the :code:`spiff.metrics` logger, which emits messages containing the elapsed duration of tasks
All log entries created during the course of running a workflow contain the following extra attributes:
- :code:`workflow_spec`: the name of the current workflow spec
- :code:`task_spec`: the name of the task spec
- :code:`task_id`: the ID of the task)
- :code:`task_type`: the name of the task's spec's class
If the log level is less than 20:
- :code:`data` the task data (this can be quite large and is only made available for debugging purposes)
If the log level is less than or equal to 10:
- :code:`internal_data`: the task internal data (only available at DEBUG or below because it is not typically useful)
The metrics logger additionally provides and only emits messages at the DEBUG level:
- :code:`elapsed`: the time it took the task to run after (ie, the duration of the :code:`task.run` method)
In our command line UI (:app:`cli/subcommands.py`), we've added a few of these extra attributes to the log records:
.. code-block:: python
spiff_logger = logging.getLogger('spiff')
spiff_handler = logging.StreamHandler()
spiff_handler.setFormatter('%(asctime)s [%(name)s:%(levelname)s] (%(workflow_spec)s:%(task_spec)s) %(message)s')
spiff_logger.addHandler(spiff_handler)
metrics_logger = logging.getLogger('spiff.metrics')
metrics_handler = logging.StreamHandler()
metrics_handler.setFormatter('%(asctime)s [%(name)s:%(levelname)s] (%(workflow_spec)s:%(task_spec)s) %(elasped)s')
metrics_logger.addHandler(metrics_handler)
In the configuration module :app:`spiff/file.py` that appears in many examples, we set the level of the :code:`spiff`
logger to :code:`INFO`, so that we'll see messages about task state changes, but we ignore the metrics log; however,
the configuration could easily be changed to include it (it can, however generate a high volume of very large records,
so consider yourself warned!).

View File

@@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@@ -1,143 +0,0 @@
MultiInstance Tasks
===================
BPMN Model
----------
We'll be using the following files from `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_.
- `multiinstance <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/top_level_multi.bpmn>`_ workflow
- `call activity multi <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/call_activity_multi.bpmn>`_ workflow
- `product_prices <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/product_prices.dmn>`_ DMN table
- `shipping_costs <https://github.com/sartography/spiff-example-cli/blob/main/bpmntutorial//shipping_costs.dmn>`_ DMN table
Loop Task
^^^^^^^^^
Suppose we want our customer to be able to select more than one product.
We'll run our 'Select and Customize Product' Call Activity as a Loop Task.
First we'll update the Call Activity's model to ask the customer if they would like to continue shopping.
.. figure:: figures/multiinstance/call_activity_multi.png
:scale: 30%
:align: center
Selecting more than one product
We've also added a *postScript* to the user task. Spiffworkflow provides extensions that allow scripts to be
run before and after tasks. It is often the case that data needs to be manipulated before and after a task.
We could add regular Script Tasks before and after, but diagrams quickly become cluttered with scripts, and
these extensions are intended to alleviate that.
We use a *postScript* to add the current product to a list of products.
.. code:: python
products.append({
'product_name': product_name,
'product_quantity': product_quantity,
'product_color': product_color,
'product_size': product_size,
'product_style': product_style,
'product_price': product_price,
})
We'll use a *preScript* on the first User Task (Select Product and Quantity) to initialize these variables to
:code:`None` each time we execute the task.
Loop Tasks run either a specified number of times or until a completion condition is met. Since we can't
know in advance how many products the customer will select, we'll add :code:`continue_shopping == 'Y'` as a
completion condition. We'll re-run this Call Activity as long as the customer indicates they want to choose
another product. We'll also set up the list of products that we plan on appending to.
We also added a postscript to this activity to delete the customization values so that we won't have to
look at them for the remainder of the workflow.
.. figure:: figures/multiinstance/loop_task.png
:scale: 30%
:align: center
Call Activity with Loop
We also needed to update our Script Task and the Instructions of the Review Order Task to handle an array
of products rather than a single product.
Here is our new script
.. code:: python
order_total = sum([ p['product_quantity'] * p['product_price'] for p in products ]) + shipping_cost
And our order summary
.. code:: python
Order Summary
{% for product in products %}
{{ product['product_name'] }}
Quantity: {{ product['product_quantity'] }}
Price: {{ product['product_price'] }}
{% endfor %}
Shipping Cost: {{ shipping_cost }}
Order Total: {{ order_total }}
Parallel MultiInstance
^^^^^^^^^^^^^^^^^^^^^^
We'll also update our 'Retrieve Product' Task and 'Product Not Available' flows to
accommodate multiple products. We can use a Parallel MultiInstance for this, since
it does not matter what order our Employee retrieves the products in.
.. figure:: figures/multiinstance/multiinstance_task_configuration.png
:scale: 30%
:align: center
MultiInstance Task configuration
We've specified :code:`products` as our Input Collection and :code:`product` as our Input Item. The
Input Collection should be an existing collection. We'll create a task instance for each element of
the collection, and copy the value into the Input Item; this is how we'll access the data of the
element.
.. :code::
Item: {{product['product_quantity']}} of {{product['product_name']}}
We also specified :code:`availability` as our Output Collection. Since this variable does not exist,
SpiffWorkflow will automatically create it. You can use an existing variable as an Output Collection;
in this case, its contents will be updated with new values. The Output Item will be copied out of the
child task into the Output Collection.
The 'Retrieve Product' task creates :code:`product_available` from the form input.
Since our input is a list, our output will also be a list. It is possible to generate different output
types if you create the output collections before referring to them.
We have to update our gateway condition to handle the list:
.. figure:: figures/multiinstance/availability_flow.png
:scale: 60%
:align: center
Gateway Condition
Sequential MultiInstance
^^^^^^^^^^^^^^^^^^^^^^^^
SpiffWorkflow also supports Sequential MultiInstance Tasks for collections, or if the loopCardinality
is known in advance, although we have not added an example of this to our workflow. Their configuraiton
is almost idenitcal to the configuration for Parallel MultiInstance Tasks.
Running The Model
^^^^^^^^^^^^^^^^^
If you have set up our example repository, this model can be run with the following command:
.. code-block:: console
./spiff-bpmn-runner.py -p order_product \
-d bpmn/tutorial/product_prices.dmn bpmn/tutorial/shipping_costs.dmn \
-b bpmn/tutorial/top_level_multi.bpmn bpmn/tutorial/call_activity_multi.bpmn

View File

@@ -1,125 +0,0 @@
Organizing More Complex Workflows
=================================
BPMN Model
----------
We'll be using the following files from `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_.
- `lanes <https://github.com/sartography/spiff-example-cli/blob/main/tutorial/bpmn/lanes.bpmn>`_ workflow
- `top_level <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/top_level.bpmn>`_ workflow
- `call_activity <https://github.com/sartography/spiff-example-cli/blob/main/tutorial/bpmn/call_activity.bpmn>`_ workflow
- `product_prices <https://github.com/sartography/spiff-example-cli/blob/main/tutorial/bpmn/product_prices.dmn>`_ DMN table
- `shipping_costs <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/shipping_costs.dmn>`_ DMN table
Lanes
^^^^^
Lanes are a method in BPMN to distinguish roles for the workflow and who is
responsible for which actions. In some cases this will be different business
units, and in some cases this will be different individuals - it really depends
on the nature of the workflow. Within the BPMN editor, this is done by choosing the
'Create pool/participant' option from the toolbar on the left hand side.
We'll modify our workflow to get the customer's payment information and send it
to an employee who will charge the customer and fulfill the order.
.. figure:: figures/organization/lanes.png
:scale: 30%
:align: center
Workflow with lanes
To run this workflow
.. code-block:: console
./spiff-bpmn-runner.py -p order_product \
-d bpmn/tutorial/product_prices.dmn bpmn/tutorial/shipping_costs.dmn \
-b bpmn/tutorial/lanes.bpmn
For a simple code example of displaying a tasks lane, see `Handling Lanes`_
Subprocesses
^^^^^^^^^^^^
Subprocesses allow us to conceptualize a group of tasks as a unit by creating a
mini-workflow inside a task. Subprocess Tasks come in two different flavors: expanded
or collapsed. The difference between the two types is visual rather than functional.
It also possible to refer to external processes via a Call Activity Task. This
allows us to 'call' a separate Process (which might be stored independently of the
Process we're implementing) by referencing the ID of the called Process, which can simplify
business logic and make it re-usable.
We'll expand 'Fulfill Order' into sub tasks -- retrieving the product and shipping
the order -- and create an Expanded Subprocess.
We'll also expand our selection of products, adding several new products and the ability
to customize certain products by size and style in addition to color.
.. figure:: figures/organization/dmn_table_updated.png
:scale: 60%
:align: center
Updated product list
.. note::
I've added what customizations are available for each product in the 'Annotations'
column of the DMN table. This is not actually used by Spiff; it simply provides
the option of documenting the decisions contained in the table.
Since adding gateways for navigating the new options will add a certain amount of
clutter to our diagram, we'll create a separate workflow for selecting and customizing
products and refer to that in our main workflow.
.. figure:: figures/organization/call_activity.png
:scale: 30%
:align: center
Subworkflow for product selection
We need to make sure the 'Called Element' matches the ID we assigned in the called Process.
.. figure:: figures/organization/top_level.png
:scale: 30%
:align: center
Parent workflow
Running the Model
^^^^^^^^^^^^^^^^^
.. code-block:: console
./spiff-bpmn-runner.py -p order_product \
-d bpmn/tutorial/product_prices.dmn bpmn/tutorial/shipping_costs.dmn \
-b bpmn/tutorial/top_level.bpmn bpmn/tutorial/call_activity.bpmn
Example Application Code
------------------------
Handling Lanes
^^^^^^^^^^^^^^
We are not required to handle lanes in our application, but most of the time we'll
probably want a way of filtering on lanes and selectively displaying tasks. In
our sample application, we'll simply display which lane a task belongs to.
.. code:: python
def get_task_description(self, task, include_state=True):
task_spec = task.task_spec
lane = f'{task_spec.lane}' if task_spec.lane is not None else '-'
name = task_spec.bpmn_name if task_spec.bpmn_name is not None else '-'
description = task_spec.description if task_spec.description is not None else 'Task'
state = f'{task.get_state_name()}' if include_state else ''
return f'[{lane}] {name} ({description}: {task_spec.bpmn_id}) {state}'
The tasks lane can be obtained from :code:`task.task_spec.lane`, which will be :code:`None`
if the task is not part of a lane.
See the Filtering Tasks Section of :doc:`advanced` more information about working with lanes in Spiff.

View File

@@ -1,135 +0,0 @@
Overview
========
BPMN and SpiffWorkflow
----------------------
.. sidebar:: BPMN Resources
This guide is a mere introduction to BPMN.
For more serious modeling, we recommend looking for more comprehensive
resources. We have used the `books by Bruce Silver <https://www.amazon.com/Bruce-Silver/e/B0062AXUFY/ref=dp_byline_cont_pop_book_1>`_
as a guide for our BPMN modeling.
.. image:: figures/overview/bpmnbook.jpg
:align: center
Business Process Model and Notation (BPMN) is a diagramming language for specifying business
processes. BPMN links the realms of business and IT, and creates a common process language that
can be shared between the two.
BPMN describes details of process behaviors efficiently in a diagram. The meaning is precise enough
to describe the technical details that control process execution in an automation engine.
SpiffWorkflow allows you to create code to directly execute a BPMN diagram.
When using SpiffWorkflow, a client can manipulate the BPMN diagram and still have their product work
without a need for you to edit the Python code, improving response and turnaround time.
.. sidebar:: BPMN Modelers
Currently the best way to build BPMN diagrams is through our SpiffArena project
which provides a custom BPMN Modeler, along with ways to test and run BPMN diagrams
from within a web browser. Please see our `getting started guide <https://www.spiffworkflow.org/posts/articles/get_started/>`_
for more information.
It is also possible to use version 7 of the Camunda Modeler to create BPMN diagrams.
However, be cautious of the properies panel settings, as many of these settings are
not a part of the BPMN Standard, and are not handled in the same way within SpiffWorkflow.
You can download the Camunda Modeler from `Camunda <https://camunda.com/download/modeler/>`_.
Today, nearly every process modeling tool supports BPMN in some fashion making it a great tool to
learn and use. This page provides a brief overview, and the following section provides a more
in-depth look. There are many resources for additional information about BPMN.
Most of the examples in this guide have been created with
`our modeler <https://github.com/sartography/bpmn-js-spiffworkflow>`_, which is based on
`bpmn.js <https://bpmn.io/toolkit/bpmn-js/>`_.
A Simple Workflow
-----------------
All BPMN models have a start event and at least one end event. The start event
is represented with a single thin border circle. An end event is represented
by a single thick border circle.
The following example also has one task, represented by the rectangle with curved corners.
.. figure:: figures/overview/simplestworkflow.png
:scale: 25%
:align: center
A simple workflow.
The sequence flow is represented with a solid line connector. When the node at
the tail of a sequence flow completes, the node at the arrowhead is enabled to start.
A More Complicated Workflow
---------------------------
.. figure:: figures/overview/ExclusiveGateway.png
:scale: 25%
:align: center
A workflow with a gateway
In this example, the diamond shape is called a gateway. It represents a branch
point in our flow. This gateway is an exclusive data-based gateway (also
called an XOR gateway). With an exclusive gateway, you must take one path or
the other based on some data condition. BPMN has other gateway types.
The important point is that we can use a gateway to add a branch in the
workflow **without** creating an explicit branch in our Python code.
An Even More Complicated Workflow
------
BPMN is a rich language that can describe many different types of processes. In
the following pages we'll cover lanes (a way to distribute work across different
roles) events (a way to handle asynchronous events), multi-instance tasks (that
can be executed many times in parallel or in sequence) and decomposition (the
many ways you can interconnect diagrams to build larger more complex processes)
We are just scratching the surface. For now let's take one more step and look
at what Events make possible.
Events
^^^^^^^
In the above simple workflows, all of the transitions are deterministic and we
have direct connections between tasks. We need to handle the cases where an event
may or may not happen, and link these events in different parts of the workflow or
across different workflows.
BPMN has a comprehensive suite of event elements. SpiffWorkflow does not support
every single BPMN event type, but it can handle many of them.
.. figure:: figures/overview/events.png
:scale: 25%
:align: center
A workflow containing events
We've already seen plain Start and End Events. BPMN also includes the concept
of Intermediate Events (standalone events that may be Throwing or Catching) as well
as Boundary Events (which are exclusively Catching).
All Start Events are inherently Catching Events (a workflow can be initiated if a
particular event occurs) and all End Events are Throwing Events (they can convey
the final state of a workflow or path to other tasks and workflows).
If an Intermediate Throwing Event is added to a flow, the event it represents
will be generated and the flow will continue immediately. If an Intermediate
Catching Event is added to a flow, the workflow will wait to catch the event it
represents before advancing.
A Boundary Event represents an event that may be caught only while a particular task
is being executed and comes in two types: Interrupting (in which case the task it is
attached to will be cancelled if the event is received) or Non-Interrupting (in
which case the task will continue). In both cases, flows may emanate from the
Boundary Event, which will trigger those paths if the events occur while the task
is being executed.

141
doc/bpmn/parsing.rst Normal file
View File

@@ -0,0 +1,141 @@
Parsing BPMN
============
The example application assumes that a :code:`BpmnProcessSpec` will be generated for each process independently of
starting a workflow and that these will be immediately serialized and provided with a ID. We'll discuss serialization
in greater detail later; for now we'll simply note that the file serializer simply writes a JSON representation of the
spec to a file and uses the filename as the ID.
.. note::
This is design choice -- it would be possible to re-parse the specs each time a process was run.
Default Parsers
===============
Importing
---------
Each of the BPMN modules (:code:`bpmn`, :code:`spiff`, or :code:`camunda`) has a parser that is preconfigured with
the specs in that module (if a particular TaskSpec is not implemented in the module, :code:`bpmn` TaskSpec is used).
- :code:`bpmn`: :code:`from SpiffWorkflow.bpmn.parser import BpmnParser`
- :code:`dmn`: :code:`from SpiffWorkflow.dmn.parser import BpmnDmnParser`
- :code:`spiff`: :code:`from SpiffWorkflow.spiff.parser import SpiffBpmnParser`
- :code:`camunda`: :code:`from SpiffWorkflow.camunda.parser import CamundaParser`
.. note::
The default parser cannot parse DMN files. The :code:`BpmnDmnParser` extends the default parser to add that
capability. Both the :code:`spiff` and :code:`camunda` parsers inherit from :code:`BpmnDmnParser`.
Instantiation of a parser has no required arguments, but there are several optional parameters.
Validation
----------
The :code:`SpiffWorkflow.bpmn.parser` module also contains a :code:`BpmnValidator`.
The default validator validates against the BPMN 2.0 spec. It is possible to import additional specifications (e.g.
for custom extensions) as well.
By default the parser does not validate, but if a validator is passed in, it will be used on any files added to the parser.
.. code-block:: python
from SpiffWorkflow.bpmn.parser import BpmnParser, BpmnValidator
parser = BpmnParser(validator=BpmnValidator())
Spec Descriptions
-----------------
A default set of :code:`decription` attributes for each Task Spec. The description is intended to be a user-friendly
representation of the task type. It is a mapping of XML tag to string.
The default set of descriptions can be found in :code:`SpiffWorkflow.bpmn.parser.spec_descriptions`.
Creating a BpmnProcessSpec from BPMN Process
--------------------------------------------
From the :code:`add_spec` method of our BPMN engine (:app:`engine/engine.py`):
.. code-block:: python
def add_spec(self, process_id, bpmn_files, dmn_files):
self.add_files(bpmn_files, dmn_files)
try:
spec = self.parser.get_spec(process_id)
dependencies = self.parser.get_subprocess_specs(process_id)
except ValidationException as exc:
self.parser.process_parsers = {}
raise exc
spec_id = self.serializer.create_workflow_spec(spec, dependencies)
logger.info(f'Added {process_id} with id {spec_id}')
return spec_id
def add_files(self, bpmn_files, dmn_files):
self.parser.add_bpmn_files(bpmn_files)
if dmn_files is not None:
self.parser.add_dmn_files(dmn_files)
The first step is adding BPMN and DMN files to the parser using the :code:`add_bpmn_files` and
:code:`add_dmn_files` methods.
We use the :code:`get_spec` to parse the BPMN process with the provided :code:`process_id` (*not* the process name).
.. note::
Ths parser was designed to load one set of files and parse a process and will raise a :code:`ValidationException`
if any duplicate iDs are present. The available processes are immediately added to :code:`process_parsers`, so
re-adding a file will generate an exception. Therefore, if we run into a problem (the specific case here) or wish
to reuse the same parser, we need to clear this attribute.
Other Methods for Adding Files
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- :code:`add_bpmn_files_by_glob`: Loads files from a glob instead of a list.
- :code:`add_bpmn_file`: Adds one file rather than a list.
- :code:`load_bpmn_str`: Loads and parses XML from a string.
- :code:`load_bpmn_io`: Loads and parses XML from an object implementing the IO interface.
- :code:`load_bpmn_xml`: Parses BPMN from an :code:`lxml` parsed tree.
.. _parsing_subprocesses:
Handling Subprocesses and Call Activities
-----------------------------------------
Internally, Call Activities and Subprocesses (as well as Transactional Subprocesses) are all treated as separate
specifications. This is to prevent a single specification from becoming too large, especially in the case where the
same process spec will be called more than once.
The :code:`get_subprocess_specs` method takes a process ID and recursively searches for Call Activities, Subprocesses,
etc used by or defined in the provided BPMN files. It returns a mapping of process ID to parsed specification.
Other Methods for Finding Dependencies
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- :code:`find_all_specs`: Returns a mapping of name -> :code:`BpmnWorkflowSpec` for all processes in all files that have been
provided to the parser at that point.
- :code:`get_process_dependencies`: Returns a list of process IDs referenced by the provided process ID
- :code:`get_dmn_dependencies`: Returns a list of DMN IDs referenced by the provided process ID
Creating a BpmnProcessSpec from a BPMN Collaboration
----------------------------------------------------
The parser can also generate a workflow spec based on a collaboration:
.. code-block:: python
def add_collaboration(self, collaboration_id, bpmn_files, dmn_files=None):
self.add_files(bpmn_files, dmn_files)
try:
spec, dependencies = self.parser.get_collaboration(collaboration_id)
except ValidationException as exc:
self.parser.process_parsers = {}
raise exc
A spec is created for each of the processes in the collaboration, and each of these processes is wrapped inside a
subworkflow. This means that a spec created this way will *always* require subprocess specs, and this method
returns the generated spec (which doesn't directly correspond to anything in the BPMN file) as well as the processes
present in the file, and theit dependencies.

209
doc/bpmn/script_engine.rst Normal file
View File

@@ -0,0 +1,209 @@
Script Engine Overview
======================
You may need to modify the default script engine, whether because you need to make additional
functionality available to it, or because you might want to restrict its capabilities for
security reasons.
.. warning::
By default, the scripting environment passes input directly to :code:`eval` and :code:`exec`! In most
cases, you'll want to replace the default scripting environment with one of your own.
Restricting the Script Environment
==================================
The following example replaces the default global enviroment with the one provided by
`RestrictedPython <https://restrictedpython.readthedocs.io/en/latest/>`_
We've modified our engine configuration to use the restricted environment in :app:`spiff/restricted.py`
.. code:: python
from RestrictedPython import safe_globals
from SpiffWorkflow.bpmn.PythonScriptEngineEnvironment import TaskDataEnvironment
restricted_env = TaskDataEnvironment(safe_globals)
restricted_script_engine = PythonScriptEngine(environment=restricted_env)
We've also included a dangerous process in :bpmn:`dangerous.bpmn`
If you run this process using the regular script enviromment, the BPMN process will get the OS process ID and
prompt you to kill it; if you answer 'Y', it will do so (it won't actually do anything more dangerous than screw
up your terminal settings, but hopefully proves the point).
.. code-block:: console
./runner.py -e spiff_example.spiff.file add -p end_it_all -b bpmn/tutorial/dangerous.bpmn
./runner.py -e spiff_example.spiff.file
If you load the restricted engine:
.. code-block:: console
./runner.py -e spiff_example.spiff.restricted
You'll get an error, because imports have been restricted.
.. note::
Since we used exactly the same parser and serializer, we can simply switch back and forth between these
two script engines (that is the only difference between the two configurations).
Making Custom Classes and Functions Available
=============================================
Another reason you might want to customize the scripting environment is to provide access to custom
classes or functions.
In many of our example models, we use DMN tables to obtain product information. DMN is a convenient
way of making table data available to our processes.
However, in a slightly more realistic scenario, we would surely have information about how the product
could be customized in a database somewhere. We would not hard code product information in our diagram
(although it is much easier to modify the BPMN and DMN models than to change the code itself!). Our
shipping costs would not be static, but would depend on the size of the order and where it was being
shipped -- maybe we'd query an API provided by our shipper.
SpiffWorkflow is obviously **not** going to know how to query **your** database or make API calls to
**your** vendors. However, one way of making this functionality available inside your diagram is to
implement the calls in functions and add those functions to the scripting environment, where they
can be called by Script Tasks.
We are not going to actually include a database or API and write code for connecting to and querying
it, but since we only have 7 products we can model our database with a simple dictionary lookup
and just return the same static info for shipping for the purposes of the tutorial.
We'll customize our scripting environment in :app:`spiff/custom_object.py`:
.. code:: python
from collections import namedtuple
ProductInfo = namedtuple('ProductInfo', ['color', 'size', 'style', 'price'])
INVENTORY = {
'product_a': ProductInfo(False, False, False, 15.00),
'product_b': ProductInfo(False, False, False, 15.00),
'product_c': ProductInfo(True, False, False, 25.00),
'product_d': ProductInfo(True, True, False, 20.00),
'product_e': ProductInfo(True, True, True, 25.00),
'product_f': ProductInfo(True, True, True, 30.00),
'product_g': ProductInfo(False, False, True, 25.00),
}
def lookup_product_info(product_name):
return INVENTORY[product_name]
def lookup_shipping_cost(shipping_method):
return 25.00 if shipping_method == 'Overnight' else 5.00
script_env = TaskDataEnvironment({
'datetime': datetime,
'lookup_product_info': lookup_product_info,
'lookup_shipping_cost': lookup_shipping_cost,
})
script_engine = PythonScriptEngine(script_env)
.. note::
We're also adding :code:`datetime`, because other parts of the process require it.
We can use the custom functions in script tasks like any normal function. To load the example diagrams that use the
custom script engine:
.. code-block:: console
./runner.py -e spiff_example.spiff.custom_object add -p order_product \
-b bpmn/tutorial/{top_level_script,call_activity_script}.bpmn
If you start the application in interactive mode and choose a product, you'll see tuple info reflected in the task data
after selecting a product.
Service Tasks
=============
We can also use Service Tasks to accomplish the same goal. Service Tasks are also executed by the workflow's script
engine, but through a different method, with the help of some custom extensions in the :code:`spiff` module:
- `operation_name`, the name assigned to the service being called
- `operation_params`, the parameters the operation requires
The advantage of a Service Task is that it is a bit more transparent what is happening (at least at a conceptual level)
than function calls embedded in a Script Task.
We implement the :code:`PythonScriptEngine.call_service` method in :app:`spiff/service_task.py`:
.. code:: python
service_task_env = TaskDataEnvironment({
'product_info_from_dict': product_info_from_dict,
'datetime': datetime,
})
class ServiceTaskEngine(PythonScriptEngine):
def __init__(self):
super().__init__(environment=service_task_env)
def call_service(self, operation_name, operation_params, task_data):
if operation_name == 'lookup_product_info':
product_info = lookup_product_info(operation_params['product_name']['value'])
result = product_info_to_dict(product_info)
elif operation_name == 'lookup_shipping_cost':
result = lookup_shipping_cost(operation_params['shipping_method']['value'])
else:
raise Exception("Unknown Service!")
return json.dumps(result)
service_task_engine = ServiceTaskEngine()
Instead of adding our custom functions to the environment, we'll override :code:`call_service` and call them directly
according to the `operation_name` that was given. The :code:`spiff` Service Task also evaluates the parameters
against the task data for us, so we can pass those in directly. The Service Task will also store our result in
a user-specified variable.
We need to send the result back as json, so we'll reuse the functions we wrote for the serializer (see
:ref:`serializing_custom_objects`).
The Service Task will assign the dictionary as the operation result, so we'll add a `postScript` to the Service Task
that retrieves the product information that creates a :code:`ProductInfo` instance from the dictionary, so we need to
add that to the scripting enviroment too.
The XML for the Service Task looks like this:
.. code:: xml
<bpmn:serviceTask id="Activity_1ln3xkw" name="Lookup Product Info">
<bpmn:extensionElements>
<spiffworkflow:serviceTaskOperator id="lookup_product_info" resultVariable="product_info">
<spiffworkflow:parameters>
<spiffworkflow:parameter id="product_name" type="str" value="product_name"/>
</spiffworkflow:parameters>
</spiffworkflow:serviceTaskOperator>
<spiffworkflow:postScript>product_info = product_info_from_dict(product_info)</spiffworkflow:postScript>
</bpmn:extensionElements>
<bpmn:incoming>Flow_104dmrv</bpmn:incoming>
<bpmn:outgoing>Flow_06k811b</bpmn:outgoing>
</bpmn:serviceTask>
Getting this information into the XML is a little bit beyond the scope of this tutorial, as it involves more than
just SpiffWorkflow. I hand edited it for this case, but you can hardly ask your BPMN authors to do that!
Our `modeler <https://github.com/sartography/bpmn-js-spiffworkflow>`_ has a means of providing a list of services and
their parameters that can be displayed to a BPMN author in the Service Task configuration panel. There is an example of
hard-coding a list of services in
`app.js <https://github.com/sartography/bpmn-js-spiffworkflow/blob/0a9db509a0e85aa7adecc8301d8fbca9db75ac7c/app/app.js#L47>`_
and as suggested, it would be reasonably straightforward to replace this with a API call.
`SpiffArena <https://www.spiffworkflow.org/posts/articles/get_started/>`_ has robust mechanisms for handling this that
might serve as a model for you.
How this all works is obviously heavily dependent on your application, so we won't go into further detail here, except
to give you a bare bones starting point for implementing something yourself that meets your own needs.
To run this workflow:
.. code-block:: console
./runner.py -e spiff_example.spiff.service_task add -p order_product \
-b bpmn/tutorial/{top_level_service_task,call_activity_service_task}.bpmn

280
doc/bpmn/serialization.rst Normal file
View File

@@ -0,0 +1,280 @@
Introduction
============
Given the long-running nature of many workflows, robust serialization capabilities are critical to any kind of
workflow execution library. We face several problems in serializing workflows:
- workflows may contain arbitrary data whose serialization mechanisms cannot be built into the library itself
- workflows may contain custom tasks and these also cannot be built into the library
- workflows may contain hundreds of tasks, generating very large serializations
- objects contained in the workflow data might also be very large
- the serialized data needs to be stored somewhere and there is no one-size-fits-all way of doing this
In the first section of this document, we'll show how to handle the first problem.
:doc:`custom_task_spec` contains an example of handling the second problem.
In the second section of this document, we'll discuss some of the ways the remaining problems might be alleviated
though creative use of the serializer's capabilities.
.. _serializing_custom_objects:
Serializing Custom Objects
==========================
In :doc:`script_engine`, we add some custom methods and objects to our scripting environment. We create a simple
class (a :code:`namedtuple`) that holds the product information for each product.
We'd like to be able to save and restore our custom object. This code lives in :app:`spiff/product_info.py`.
.. code:: python
ProductInfo = namedtuple('ProductInfo', ['color', 'size', 'style', 'price'])
def product_info_to_dict(obj):
return {
'color': obj.color,
'size': obj.size,
'style': obj.style,
'price': obj.price,
}
def product_info_from_dict(dct):
return ProductInfo(**dct)
And in :app:`spiff/custom_object.py`:
.. code-block:: python
from SpiffWorkflow.spiff.serializer.config import SPIFF_CONFIG
from ..serializer.file import FileSerializer
registry = FileSerializer.configure(SPIFF_CONFIG)
registry.register(ProductInfo, product_info_to_dict, product_info_from_dict)
serializer = FileSerializer(dirname, registry=registry)
We don't have any custom task specs in this example, so we can use the default serializer configuration for the
module we're using. We'll use the :app:`spiff/serializer/file/serializer.py` serializer. This is a very simple
serializer -- it converts the entire workflow to the default JSON format and writes it to disk in a readable
way.
.. note::
The default :code:`BpmnWorkflowSerializer` has a `serialize_json` method that essentially does the same thing,
except without formatting the JSON. We bypass this so we can intercept the JSON-serializable representation
and write it ourselves to a location of our choosing.
We initialize a :code:`registry` using the serializer; this registry contains the conversions for the objects
used workflow-internally.
Now we can add our custom serialization methods to this registry using the :code:`registry.register` method. The
arguments here are:
- the class that requires serialization
- a method that creates a dictionary representation of the object
- a method that recreates the object from that representation
Registering an object sets up relationships between the class and the serialization and deserialization methods.
The :code:`register` method assigns a :code:`typename` for the class, and generates partial functions that call the
appropriate methods based on the :code:`typename`, and stores *these* conversion mechanisms.
.. note::
The supplied :code:`to_dict` and :code:`from_dict` methods must always return and accept dictionaries, even if
they might have been serialized some other way.
If you're interested in how this works, the heart of the registry is the
`DictionaryConverter <https://github.com/sartography/SpiffWorkflow/blob/main/SpiffWorkflow/bpmn/serializer/helpers/dictionary.py>`_.
The price is a slightly less customizable serialized format; the benefit is that these partial functions can
replace humongous :code:`if/else` blocks that test for specific classes and attributes.
Optimizing Serializations
=========================
File Serializer
---------------
Now we'll turn to the customizations we made in the :app:`serializer/file/serializer.py`.
We've extended the :code:`BpmnWorkflowSerializer` to take a directory where we'll write our files, and additionally
we'll impose some structure inside this dictionary. We'll separate serialized workflow specs from instance data, and
set an output format that we can actually read.
Our engine requires a certain API from our serializer, and that's what the remainder of the methods are. We won't
go into these method here, as they don't *actually* have much to do with the library. We made few (the
:app:`spiff/custom_object.py`) or no modifications (the :app:`spiff/file.py`) so there isn't much to discuss.
We call `self.to_dict` and `self.from_dict`, which handle all conversions based on how we've set up the
:code:`registry`.
.. note::
We haven't referenced any particular code, as almost all the code here is about managing our directory
structure and formatting the JSON output appropriately.
The file serializer is actually *not* particularly optimized, but it is simple to understand, while also providing
the evidence that you probably want to do more. The output here is essentially the what you get by default. This
useful to be able to easily see in and of itself, and if you examine it, you'll see there would be a lot of
opportunity for splitting the output into its components and handling them separately.
SQLite Serializer
-----------------
We have a second example serializer that stores serializations in a SQLite database in
:app:`serializer/sqlite/serializer.py`. This might be a slightly more realistic use case of what you want to do,
so we'll discuss this in a little more detail (but it is also a considerably more complex example).
Our database schema actually takes care of much of the work, but since this isn't an SQL tutorial, I'll just refer you
to the file that contains it: :app:`serializer/sqlite/schema.sql`. Of course, you do not have to interact with the
database directly (or even use a database at all) and some of all of the triggers and views and so forth could be
replaced with Python code (or simplified quite a bit if using a more robust DB).
This is intended to be a somewhat extreme example in order to make it clear that you really aren't bound to
retrieving and storing a gigantic blob, and the logic for dealing with it does not have to be interspersed with the
rest of your code.
In addition to our triggers, we also rely pretty heavily on SQLite adapters. Between these two things, we hardly
have to worry about the types of objects we get back at all!
From our :code:`execute` method:
.. code-block:: python
conn = sqlite3.connect(self.dbname, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
conn.execute("pragma foreign_keys=on")
sqlite3.register_adapter(UUID, lambda v: str(v))
sqlite3.register_converter("uuid", lambda s: UUID(s.decode('utf-8')))
sqlite3.register_adapter(dict, lambda v: json.dumps(v))
sqlite3.register_converter("json", lambda s: json.loads(s))
We use :code:`UUID` for spec and instance IDs and store all our workflow data as JSON. Our serializer guarantees
that its output will be JSON-serializable, so when we store it, we can just drop its output right into the DB, and
feed the DB output back into the serializer.
To help this process along, we've customized a few of the default conversions for our specs.
.. code-block:: python
class WorkflowConverter(BpmnWorkflowConverter):
def to_dict(self, workflow):
dct = super(BpmnWorkflowConverter, self).to_dict(workflow)
dct['bpmn_events'] = self.registry.convert(workflow.bpmn_events)
dct['subprocesses'] = {}
dct['tasks'] = list(dct['tasks'].values())
return dct
class SubworkflowConverter(BpmnSubWorkflowConverter):
def to_dict(self, workflow):
dct = super().to_dict(workflow)
dct['tasks'] = list(dct['tasks'].values())
return dct
class WorkflowSpecConverter(BpmnProcessSpecConverter):
def to_dict(self, spec):
dct = super().to_dict(spec)
dct['task_specs'] = list(dct['task_specs'].values())
return dct
We aren't making extensive customizations here, mainly just switching some dictionaries to lists; this is because we
store these items in separate tables, so it's convenient to get an output that can be passed directly to an
:code:`insert` statement.
When we configure our engine, we update the serializer configuration to use these classes (this code is from
:app:`spiff/sqlite.py`:
.. code-block:: python
from SpiffWorkflow.spiff.serializer import DEFAULT_CONFIG
from ..serializer.sqlite import (
SqliteSerializer,
WorkflowConverter,
SubworkflowConverter,
WorkflowSpecConverter
)
DEFAULT_CONFIG[BpmnWorkflow] = WorkflowConverter
DEFAULT_CONFIG[BpmnSubWorkflow] = SubworkflowConverter
DEFAULT_CONFIG[BpmnProcessSpec] = WorkflowSpecConverter
dbname = 'spiff.db'
with sqlite3.connect(dbname) as db:
SqliteSerializer.initialize(db)
registry = SqliteSerializer.configure(DEFAULT_CONFIG)
serializer = SqliteSerializer(dbname, registry=registry)
Finally, let's look at two of the methods we've implemented for the API required by our engine:
.. code-block:: python
def _create_workflow(self, cursor, workflow, spec_id):
dct = super().to_dict(workflow)
wf_id = uuid4()
stmt = "insert into workflow (id, workflow_spec_id, serialization) values (?, ?, ?)"
cursor.execute(stmt, (wf_id, spec_id, dct))
if len(workflow.subprocesses) > 0:
cursor.execute("select serialization->>'name', descendant from spec_dependency where root=?", (spec_id, ))
dependencies = dict((name, id) for name, id in cursor)
for sp_id, sp in workflow.subprocesses.items():
cursor.execute(stmt, (sp_id, dependencies[sp.spec.name], self.to_dict(sp)))
return wf_id
def _get_workflow(self, cursor, wf_id, include_dependencies):
cursor.execute("select workflow_spec_id, serialization as 'serialization [json]' from workflow where id=?", (wf_id, ))
row = cursor.fetchone()
spec_id, workflow = row[0], self.from_dict(row[1])
if include_dependencies:
workflow.subprocess_specs = self._get_subprocess_specs(cursor, spec_id)
cursor.execute(
"select descendant as 'id [uuid]', serialization as 'serialization [json]' from workflow_dependency where root=? order by depth",
(wf_id, )
)
for sp_id, sp in cursor:
task = workflow.get_task_from_id(sp_id)
workflow.subprocesses[sp_id] = self.from_dict(sp, task=task, top_workflow=workflow)
return workflow
We store subprocesses in the same table as top level processes because they are essentially the same thing.
We maintain a table that stores the parent/child relationships in a separate spec dependency table. While we don't do
this currently, we could modify our queries to ignore subprocesses that have been completed when we retrieve a workflow:
they could potentially contain many tasks that will never be revisited. Or, conversely, we could limit what we restore
to subprocesses that had :code:`READY` tasks to avoid loading something that is waiting for a timer that will fire in
two weeks.
We did not show the code for serializing workflow specs, but it is similar -- all specs, whether top-level or for
subprocesses and call activities live in one table, with a second that keeps track of dependencies between them. This
would make it possible to wait to load a spec until the task it was associated with needed to be executed.
We also maintain task data separately from information about workflow state; so while we're not doing this now, it provides
the potential to selectively retrieve it -- for example, it could be omitted from :code:`COMPLETED` tasks.
What I aim to get across here is that there are quite a few possiblities for customizing how your application serializes
its workflows -- you're not limited to a giant JSON blob that you get by default.
Serialization Versions
======================
As we make changes to Spiff, we may change the serialization format. For example, in 1.2.1, we changed
how subprocesses were handled interally in BPMN workflows and updated how they are serialized and we upraded the
serializer version to 1.1.
Since workflows can contain arbitrary data, and even SpiffWorkflow's internal classes are designed to be customized in ways
that might require special serialization and deserialization, it is possible to override the default version number, to
provide users with a way of tracking their own changes.
If you have not provided a custom version number, SpiffWorkflow will attempt to migrate your workflows from one version
to the next if they were serialized in an earlier format.
If you've overridden the serializer version, you may need to incorporate our serialization changes with
your own. You can find our migrations in
`SpiffWorkflow/bpmn/serilaizer/migrations <https://github.com/sartography/SpiffWorkflow/tree/main/SpiffWorkflow/bpmn/serializer/migration>`_
These are broken up into functions that handle each individual change, which will hopefully make it easier to incoporate them
into your upgrade process, and also provides some documentation on what has changed.

67
doc/bpmn/supported.rst Normal file
View File

@@ -0,0 +1,67 @@
List of Supported Elements
==========================
Tasks
-----
* User Task
* Manual Task
* Business Rule Task
* Script Task
* Service Task
.. note::
Spiff's implementation of Service Tasks is abstract, so while they will be parsed, the
library provides no built-in mechanism for executing them.
Gateways
--------
* Parallel Gateway
* Exclusive Gateway
* Inclusive Gateway
* Event-Based Gateway
Subrocesses and Call Activities
-------------------------------
* Subprocess
* Call Activity
* Transaction Subprocess
Events
------
* Cancel Event
* Escalation Event
* Error Event
* Message Event
* Signal Event
* Terminate Event
* Timer Event
Data
----
* Data Object
* Data Store
.. note::
Spiff's Data Store implementation is abstract; Spiff can parse a Data Store, but does not
provide any built-in mechanism for reading and writing to it.
Loops
-----
* Loop Task
* Parallel MultiInstance Task
* Sequential MultiInstance Task
.. note::
Parallel MultiInstance tasks are *not* executed by SpiffWorkflow in parallel. SpiffWorkflow
merely indicates that parallel tasks become ready at the same time and that the workflow
engine may execute them in parallel.

View File

@@ -1,321 +0,0 @@
Putting it All Together
=======================
In this section we'll be discussing the overall structure of the workflow
runner we developed in `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_.
Our example application contains two different workflow runners, one that uses tasks with with Spiff extensions
(`spiff-bpmn-runner.py <https://github.com/sartography/spiff-example-cli/blob/main/spiff-bpmn-runner.py>`_)
and one that uses the **deprecated** Camunda extensions
(`camunda-bpmn-runner.py <https://github.com/sartography/spiff-example-cli/blob/main/camunda-bpmn-runner.py>`_).
The primary differences between the two are in handling User and MultiInstance Tasks. We have some documentation
about how we interpret Camunda forms in :doc:`camunda/tasks`. That particular page comes from an earlier version of
our documentation, and `camunda-bpmn-runner.py` can run workflows with these tasks. However, we are not actively
maintaining the :code:`camunda` package, and it should be considered deprecated.
Base Application Runner
-----------------------
The core functions your application will have to accomodate are
* parsing workflows
* serializing workflows
* running workflows
Task specs define how tasks are executed, and creating the task specs depends on a parser which initializes a spec of
the appropriate class. And of course serialization is also heavily dependent on the same information needed to create
the instance. To that end, our BPMN runner requires that you provide a parser and serializer; it can't operate unless
it knows what to do with each task spec it runs across.
Here is the initialization for the :code:`runner.SimpleBpmnRunner` class that is used by both scripts.
.. code:: python
def __init__(self, parser, serializer, script_engine=None, handlers=None):
self.parser = parser
self.serializer = serializer
self.script_engine = script_engine
self.handlers = handlers or {}
self.workflow = None
If you read the introduction to BPMN, you'll remember that there's a Script Task; the script engine executes scripts
against the task data and updates it. Gateway conditions are also evaluated against the same context by the engine.
SpiffWorkflow provides a default scripting environment that is suitable for simple applications, but a serious application
will probably need to extend (or restrict) it in some way. See :doc:`advanced` for a few examples. Therefore, we have the
ability to optionally pass one in.
The `handlers` argument allows us to let our application know what to do with specific task spec types. It's a mapping
of task spec class to its handler. Most task specs won't need handlers outside of how SpiffWorkflow executes them
(that's probably why you are using this library). You'll only have to be concerned with the task spec types that
require human interaction; Spiff will not handle those for you. In your application, these will probably be built into
it and you won't need to pass anything in.
However, here we're trying to build something flexible enough that it can at least deal with two completely different
mechanisms for handling User Tasks, and provide a means for you to experiment with this application.
Parsing Workflows
-----------------
Here is the method we use to parse the workflows;
.. code:: python
def parse(self, name, bpmn_files, dmn_files=None, collaboration=False):
self.parser.add_bpmn_files(bpmn_files)
if dmn_files:
self.parser.add_dmn_files(dmn_files)
if collaboration:
top_level, subprocesses = self.parser.get_collaboration(name)
else:
top_level = self.parser.get_spec(name)
subprocesses = self.parser.get_subprocess_specs(name)
self.workflow = BpmnWorkflow(top_level, subprocesses, script_engine=self.script_engine)
We add the BPMN and DMN files to the parser and use :code:`parser.get_spec` to create a workflow spec for a process
model.
SpiffWorkflow needs at least one spec to create a workflow; this will be created from the name of the process passed
into the method. It also needs specs for any subprocesses or call activities. The method
:code:`parser.get_subprocess_specs` will search recursively through a starting spec and collect specs for all
referenced resources.
It is possible to have two processes defined in a single model, via a Collaboration. In this case, there is no "top
level spec". We can use :code:`self.parser.get_collaboration` to handle this case.
.. note::
The only required argument to :code:`BpmnWorkflow` is a single workflow spec, in this case `top_level`. The
parser returns an empty dict if no subprocesses are present, but it is not required to pass this in. If there
are subprocess present, `subprocess_specs` will be a mapping of process ID to :code:`BpmnWorkflowSpec`.
In :code:`simple_bpmn_runner.py` we create the parser like this:
.. code:: python
from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser, BpmnValidator
parser = SpiffBpmnParser(validator=BpmnValidator())
The validator is an optional argument, which can be used to validate the BPMN files passed in. The :code:`BpmnValidator`
in the :code:`spiff` package is configured to validate against the BPMN 2.0 spec and our spec describing our own
extensions.
The parser we imported is pre-configured to create task specs that know about Spiff extensions.
There are parsers in both the :code:`bpmn` and :code:`camunda` packages that can be similarly imported. There is a
validator that uses only the BPMN 2.0 spec in the :code:`bpmn` package (but no similar validator for Camunda).
It is possible to override particular task specs for specific BPMN Task types. We'll cover an example of this in
:doc:`advanced`.
Serializing Workflows
---------------------
In addition to the pre-configured parser, each package has a pre-configured serializer.
.. code:: python
from SpiffWorkflow.spiff.serializer.config import SPIFF_SPEC_CONFIG
from runner.product_info import registry
wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter(SPIFF_SPEC_CONFIG)
serializer = BpmnWorkflowSerializer(wf_spec_converter, registry)
The serializer has two components:
* the `workflow_spec_converter`, which knows about objects inside SpiffWorkflow
* the `registry`, which can tell SpiffWorkflow how to handle arbitrary data from your scripting environment
(required only if you have non-JSON-serializable data there).
We discuss the creation and use of `registry` in :doc:`advanced` so we'll ignore it for now.
`SPIFF_SPEC_CONFIG` has serialization methods for each of the task specs in its parser and we can create a
converter from it directly and pass it into our serializer.
Here is our deserialization code:
.. code:: python
def restore(self, filename):
with open(filename) as fh:
self.workflow = self.serializer.deserialize_json(fh.read())
if self.script_engine is not None:
self.workflow.script_engine = self.script_engine
We'll just pass the contents of the file to the serializer and it will restore the workflow. The scripting environment
was not serialized, so we have to make sure we reset it.
And here is our serialization code:
.. code:: python
def dump(self):
filename = input('Enter filename: ')
with open(filename, 'w') as fh:
dct = self.serializer.workflow_to_dict(self.workflow)
dct[self.serializer.VERSION_KEY] = self.serializer.VERSION
fh.write(json.dumps(dct, indent=2, separators=[', ', ': ']))
The serializer has a companion method :code:`serialize_json` but we're bypassing that here so that we can make the
output readable.
The heart of the serialization process actually happens in :code:`workflow_to_dict`. This method returns a
dictionary representation of the workflow that contains only JSON-serializable items. All :code:`serialize_json`
does is add a serializer version and call :code:`json.dumps` on the returned dict. If you are developing a serious
application, it is unlikely you want to store the entire workflow as a string, so you should be aware that this method
exists.
The serializer is fairly complex: not only does it need to handle SpiffWorkflow's own internal objects that it
knows about, it needs to handle arbitrary Python objects in the scripting environment. The serializer is covered in
more depth in :doc:`advanced`.
Defining Task Handlers
----------------------
In :code:`spiff-bpmn-runner.py`, we also define the functions :code:`complete_user_task`. and
:code:`complete_manual_task`.
We went over these handlers in :doc:`tasks`, so we won't delve into them here.
We create a mapping of task type to handler, which we'll pass to our workflow runner.
.. code:: python
handlers = {
UserTask: complete_user_task,
ManualTask: complete_manual_task,
NoneTask: complete_manual_task,
}
In SpiffWorkflow the :code:`NoneTask` (which corresponds to the `bpmn:task` is treated as a human task, and therefore
has no built in way of handling them. Here we treat them as if they were Manual Tasks.
Running Workflows
-----------------
Our application's :code:`run_workflow` method takes one argument: `step` is a boolean that lets the runner know
if if should stop and present the menu at every step (if :code:`True`) or only where there are human tasks to
complete.
.. code:: python
def run_workflow(self, step=False):
while not self.workflow.is_completed():
if not step:
self.advance()
tasks = self.workflow.get_tasks(TaskState.READY|TaskState.WAITING)
runnable = [t for t in tasks if t.state == TaskState.READY]
human_tasks = [t for t in runnable if t.task_spec.manual]
current_tasks = human_tasks if not step else runnable
self.list_tasks(tasks, 'Ready and Waiting Tasks')
if len(current_tasks) > 0:
action = self.show_workflow_options(current_tasks)
else:
action = None
if len(tasks) > 0:
input("\nPress any key to update task list")
In the code above we first get the list of all `READY` or `WAITING` tasks; these are the currently active tasks.
`READY` tasks can be run, and `WAITING` tasks may change to `READY` (see :doc:`../concepts` for a discussion of task
states). We aren't going to do anything with the `WAITING` tasks except display them.
We can further filter our runnable tasks on the :code:`task_spec.manual` attribute. If we're stepping though the
workflow, we'll present the entire list; otherwise only the human tasks. There are actually many points where no
human tasks are available to execute; the :code:`advance` method runs the other runnable tasks if we've opted to
skip displaying them; we'll look at that method after this one.
There may also be points where there are no runnable tasks at all (for example, if the entire process is waiting
on a timer). In that case, we'll do nothing until the user indicates we can proceeed (the timer will fire
regardless of what the user does -- we're just preventing this loop from executing repeatedly when there's nothing
to do).
.. code:: python
if action == 'r':
task = self.select_task(current_tasks)
handler = self.handlers.get(type(task.task_spec))
if handler is not None:
handler(task)
task.run()
In the code above, we present a menu of runnable tasks to the user and run the one they chose, optionally
calling one of our handlers.
Each task has a `data` attribute, which can by optionally updated when the task is `READY` and before it is
run. The task `data` is just a dictionary. Our handler modifies the task data if necessary (eg adding data
collected from forms), and :code:`task.run` propogates the data to any tasks following it, and changes its state to
one of the `FINISHED` states; nothing more will be done with this task after this point.
We'll skip over most of the options in :code:`run_workflow` since they are pretty straightforward.
.. code:: python
self.workflow.refresh_waiting_tasks()
At the end of each iteration, we call :code:`refresh_waiting_tasks` to ensure that any currently `WAITING` tasks
will move to `READY` if they are able to do so.
After the workflow finishes, we'll give the user a few options for looking at the end state.
.. code:: python
while action != 'q':
action = self.show_prompt('\nSelect action: ', {
'a': 'List all tasks',
'v': 'View workflow data',
'q': 'Quit',
})
if action == 'a':
self.list_tasks([t for t in self.workflow.get_tasks() if t.task_spec.bpmn_id is not None], "All Tasks")
elif action == 'v':
dct = self.serializer.data_converter.convert(self.workflow.data)
print('\n' + json.dumps(dct, indent=2, separators=[', ', ': ']))
Note that we're filtering the task lists with :code:`t.task_spec.bpmn_id is not None`. The workflow contains
tasks other than the ones visible on the BPMN diagram; these are tasks that SpiffWorkflow uses to manage execution
and we'll omit them from the displays. If a task is visible on a diagram it will have a non-null value for its
`bpmn_id` attribute (because all BPMN elements require IDs), otherwise the value will be :code:`None`. See
:doc:`advanced` for more information about BPMN task spec attributes.
When a workflow completes, the task data from the "End" task, which has built up through the operation of the
workflow, is copied into the workflow data, so we want to give the option to display this end state. We're using
the serializer's `data_converter` to handle the workflow data (the `registry`) we passed in earlier, because
it may contain arbitrary data.
Let's take a brief look at the advance method:
.. code:: python
def advance(self):
engine_tasks = [t for t in self.workflow.get_tasks(TaskState.READY) if not t.task_spec.manual]
while len(engine_tasks) > 0:
for task in engine_tasks:
task.run()
self.workflow.refresh_waiting_tasks()
engine_tasks = [t for t in self.workflow.get_tasks(TaskState.READY) if not t.task_spec.manual]
This method is really just a condensed version of :code:`run_workflow` that ignore human tasks and doesn't need to
present a menu. We use it to get to a point in our workflow where there are only human tasks left to run.
In general, an application that uses SpiffWorkflow will use these methods as a template. It will consist of a
loop that:
* runs any `READY` engine tasks (where :code:`task_spec.manual == False`)
* presents `READY` human tasks to users (if any)
* updates the human task data if necessary
* runs the human tasks
* refreshes any `WAITING` tasks
until there are no tasks left to complete.
The rest of the code is all about presenting the tasks to the user and dumping the workflow state. These are the
parts that you'll want to customize in your own application.

View File

@@ -1,234 +0,0 @@
Tasks
=====
BPMN Model
----------
In this example, we'll model a customer selecting a product to illustrate the basic task types that
can be used with SpiffWorkflow.
We'll be using the following files from `spiff-example-cli <https://github.com/sartography/spiff-example-cli>`_:
- `task_types <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/task_types.bpmn>`_ workflow
- `product_prices <https://github.com/sartography/spiff-example-cli/blob/main/bpmn/tutorial/product_prices.dmn>`_ DMN table
User Tasks
^^^^^^^^^^
User Tasks would typically be used in the case where the task would be completed from within the
application. Our User tasks present forms that collect data from users.
We'll ask our hypothetical user to choose a product and quantity.
.. figure:: figures/tasks/user_task.png
:scale: 30%
:align: center
User Task
We can use the form builder to create the form.
.. figure:: figures/tasks/user_task_form.png
:scale: 30%
:align: center
User Task form
See the `Handling User Tasks`_ section for a discussion of sample code.
We have also retained some limited support for the now deprecated
camunda forms, which you can read about in our Camunda Specific section on :doc:`camunda/tasks`.
Business Rule Tasks
^^^^^^^^^^^^^^^^^^^
In our Business Rule Task, we'll use a DMN table to look up the price of the
product the user chose.
We'll need to create a DMN table.
What is DMN?
++++++++++++
Decision Model and Notation (DMN) is a standard for business decision
modeling. DMN allows modelers to separate decision logic from process logic
and maintain it in a table format. DMN is linked into BPMN with a *decision
task*.
With DMN, business analysts can model the rules that lead to a decision
in an easy to read table. Those tables can be executed directly by SpiffWorkflow.
This minimizes the risk of misunderstandings between business analysts and
developers, and allows rapid changes in production.
BPMN includes a decision task that refers to the decision table. The outcome of
the decision lookup allows the next gateway or activity to route the flow.
Our Business Rule Task will make use of a DMN table.
.. figure:: figures/tasks/dmn_table.png
:scale: 30%
:align: center
DMN Table
.. note::
We add quote marks around the product names in the table. Spiff will
create an expression based on the exact contents of the table, so if
the quotes are omitted, the content will be interpreted as a variable
rather than a string.
Then we'll refer to this table in the task configuration.
.. figure:: figures/tasks/business_rule_task.png
:scale: 30%
:align: center
Business Rule Task configuration
Script Tasks
^^^^^^^^^^^^
The total order cost will need to be calculated on the fly. We can do this in
a Script Task. We'll configure the task with some simple Python code.
.. figure:: figures/tasks/script_task.png
:scale: 30%
:align: center
Script Task configuration
The code in the script will have access to the task data, so variables that
have been defined previously will be available to it.
Manual Tasks
^^^^^^^^^^^^
Our final task type is a Manual Task. Manual Tasks represent work that occures
outside of SpiffWorkflow's control. Say that you need to include a step in a
process where the participant needs to stand up, walk over to the coffee maker,
and poor the cup of coffee. Manual Tasks pause the process, and wait for
confirmation that the step was completed.
Text that will be displayed to the user is added in the "Instructions" panel.
.. figure:: figures/tasks/manual_task.png
:scale: 30%
:align: center
Manual Task
Spiff's manual tasks may contain references to data inside the workflow. We have used
`Jinja <https://jinja.palletsprojects.com/en/3.0.x/>`_, but Spiff is set up in a way that
you could use any templating library you want, as well as Markdown formatting directives
(we won't implement those here though, because it doesn't make sense for a command
line app).
.. figure:: figures/tasks/manual_task_instructions.png
:scale: 30%
:align: center
Editing Instructions
See the `Handling Manual Tasks`_ section for a discussion of sample code.
For information about how Spiff handles Manual Tasks created with Camunda please
refer to the Camunda Specific section on :doc:`camunda/tasks`.
Running The Model
^^^^^^^^^^^^^^^^^
If you have set up our example repository, this model can be run with the following command:
.. code-block:: console
./spiff-bpmn-runner.py -p order_product -d bpmn/tutorial/product_prices.dmn -b bpmn/tutorial/task_types.bpmn
Example Application Code
------------------------
Handling User Tasks
^^^^^^^^^^^^^^^^^^^
We will need to provide a way to display the form data and collect the user's
responses.
.. code:: python
filename = task.task_spec.extensions['properties']['formJsonSchemaFilename']
schema = json.load(open(os.path.join(forms_dir, filename)))
for field, config in schema['properties'].items():
if 'oneOf' in config:
option_map = dict([ (v['title'], v['const']) for v in config['oneOf'] ])
options = "(" + ', '.join(option_map) + ")"
prompt = f"{field} {options} "
option = input(prompt)
while option not in option_map:
print(f'Invalid selection!')
option = input(prompt)
response = option_map[option]
else:
response = input(f"{config['title']} ")
if config['type'] == 'integer':
response = int(response)
task.data[field] = response
SpiffWorkflow uses JSON Schema to represent forms, specifically
`react-jsonschema-form <https://react-jsonschema-form.readthedocs.io/en/latest/>`_.
Our forms are really intended to be displayed in a browser, and attempting to handle them in a command
line appliction is a little awkward. The form specifications can be quite complex.
This simple implementation will present a list of options for simple enumerated fields and simply
directly stores whatever the user enters otherwise, with integer conversions if the field is so
specified. This is robust enough to collect enough information from a user to make it through our example.
SpiffWorkflow provides a mechanism for you to provide your own form specification and leaves it up to you
to decide how to present it.
Handling Business Rule Tasks
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
We do not need to do any special configuration to handle these Business Rule Tasks. SpiffWorkflow does it all for us.
Handling Script Tasks
^^^^^^^^^^^^^^^^^^^^^
We do not need to do any special configuration to handle Script Tasks, although it
is possible to implement a custom script engine. We demonstrate that process in
Custom Script Engines section :doc:`advanced` features. However, the default script
engine will be adequate for now.
Handling Manual Tasks
^^^^^^^^^^^^^^^^^^^^^
Our code for manual tasks simply asks the user to confirm that the task has been
completed.
.. code:: python
def complete_manual_task(task):
display_instructions(task)
input("Press any key to mark task complete")
:code:`display_instructions` handles presenting the task to the user.
.. code:: python
def display_instructions(task):
text = task.task_spec.extensions.get('instructionsForEndUser')
print(f'\n{task.task_spec.bpmn_name}')
if text is not None:
template = Template(text)
print(template.render(task.data))
The template string can be obtained from :code:`task.task_spec.extensions.get('instructionsForEndUser')`.
As noted above, our template class comes from Jinja. We render the template
using the task data, which is just a dictionary.
.. note::
Most of Spiff's task specifications contain this extension, not just Manual Tasks. We also use it to display
information along with forms, and about certain events.

375
doc/bpmn/workflows.rst Normal file
View File

@@ -0,0 +1,375 @@
Instantiating a Workflow
========================
From the :code:`start_workflow` method of our BPMN engine (:app:`engine/engine.py`):
.. code-block:: python
def start_workflow(self, spec_id):
spec, sp_specs = self.serializer.get_workflow_spec(spec_id)
wf = BpmnWorkflow(spec, sp_specs, script_engine=self._script_engine)
wf_id = self.serializer.create_workflow(wf, spec_id)
return wf_id
We'll use our serializer to recreate the workflow spec based on the id. As discussed in :ref:`parsing_subprocesses`,
a process has a top level specification and dictionary of process id -> spec containing any other processes referenced
by the top level process (Call Actitivies and Subprocesses).
Running a Workflow
==================
In the simplest case, running a workflow involves implementing the following loop:
* runs any `READY` engine tasks (where :code:`task_spec.manual == False`)
* presents `READY` human tasks to users (if any)
* updates the human task data if necessary
* runs the human tasks
* refreshes any `WAITING` tasks
until there are no tasks left to complete.
Here are our engine methods:
.. code-block:: python
def run_until_user_input_required(self, workflow):
task = workflow.get_next_task(state=TaskState.READY, manual=False)
while task is not None:
task.run()
self.run_ready_events(workflow)
task = workflow.get_next_task(state=TaskState.READY, manual=False)
def run_ready_events(self, workflow):
workflow.refresh_waiting_tasks()
task = workflow.get_next_task(state=TaskState.READY, spec_class=CatchingEvent)
while task is not None:
task.run()
task = workflow.get_next_task(state=TaskState.READY, spec_class=CatchingEvent)
In the first, we retrieve and run any tasks that can be executed automatically, including processing any events that
might have occurred.
The second method handles processing events. A task that corresponds to an event remains in state :code:`WAITING` until
it catches whatever event it is waiting on, at which point it becomes :code:`READY` and can be run. The
:code:`workflow.refresh_waiting_tasks` method iterates over all the waiting tasks and changes the state to :code:`READY`
if the conditions for doing so have been met.
We'll cover using the `workflow.get_next_task` method and handling Human tasks later in this document.
Tasks
=====
In this section, we'll give an overview of some of the general attributes of Task Specs and then delve into a few
specific types. See :ref:`specs_vs_instances` to read about Tasks vs Task Specs.
BPMN Task Specs
---------------
BPMN Task Specs inherit quite a few attributes from :code:`SpiffWorkflow.specs.base.TaskSpec`, but you probably
don't have to pay much attention to most of them. A few of the important ones are:
* `name`: the unique id of the TaskSpec, and it will correspond to the BPMN ID if that is present
* `description`: we use this attribute to provide a description of the BPMN task type
* `manual`: :code:`True` if human input is required to complete tasks associated with this Task Spec
BPMN Task Specs have the following additional attributes.
* `bpmn_id`: the ID of the BPMN Task (this will be :code:`None` if the task is not visible on the diagram)
* `bpmn_name`: the BPMN name of the Task
* `lane`: the lane of the BPMN Task
* `documentation`: the contents of the BPMN `documentation` element for the Task
In the example application, we use these :code:`bpmn_name` (or :code:`name` when a :code:`bpmn_name` isn't specified),
and :code:`lane` to display information about the tasks in a workflow (see the :code:`update_task_tree` method of
:app:`curses_ui/workflow_view.py`).
The :code:`manual` attribute is particularly important, because SpiffWorkflow does not include built-in
handling of these tasks so you'll need to implement this as part of your application. We'll go over how this is
handled in this application in the next section.
.. note::
NoneTasks (BPMN tasks with no more specific type assigned) are treated as Manual Tasks by SpiffWorkflow.
Instantiated Tasks
------------------
Actually all Tasks are instantiated -- that is what distinguishes a Task from a Task Spec; however, it is impossible to
belabor this point too much.
Tasks have a few additional attributes that contain important details about particular instances:
* :code:`id`: a UUID that uniquely identifies the Task (remember that a Task Spec may be reached more than once, but a new
Task is created each time)
* :code:`task_spec`: the Task Spec associated with this Task
* :code:`state`: the state of the Task, represented as one of the values in :code:`TaskState`
* :code:`last_state_change`: the timestamp of the last time this Task changed state
* :code:`data`: a dictionary that holds task/workflow data
Human (User and Manual) Tasks
-----------------------------
Remember that the :code:`bpmn` module does not provide any default capability for gathering information from a user,
and this is something you'll have to implement. In this example, we'll assume that we are using Task Specs from the
:code:`spiff` module (there is an alternative implementation in the :code:`camunda` module).
Spiff Arena uses JSON schemas to define forms associated with User Tasks and
`react-jsonschema-form <https://github.com/rjsf-team/react-jsonschema-form>`_ to render them. Additionally, our User
and Manual tasks have a custom extension :code:`instructionsForEndUser` which stores a Jinja template with Markdown
formatting that is rendered using the task data. A different format for defing forms could be used and Jinja and
Markdown could be easily replaced by other templating and rendering schemes depending on your application's needs.
Our User and Manual Task handlers render the instructions (this code is from :app:`spiff/curses_handlers.py`):
.. code-block:: python
from jinja2 import Template
def get_instructions(self):
instructions = f'{self.task.task_spec.bpmn_name}\n\n'
text = self.task.task_spec.extensions.get('instructionsForEndUser')
if text is not None:
template = Template(text)
instructions += template.render(self.task.data)
instructions += '\n\n'
return instructions
We're not going to attempt to handle Markdown in a curses UI, so we'll assume we just have text. However, we do
want to be able to incorporate data specific to the workflow in information that is presented to a user; this is
something that your application will certainly need to do. Here, we use the :code:`data` attribute of the Task
(recall that this is a dictionary) to render the template.
Our application contains a :code:`Field` class (defined in :app:`curses_ui/user_input.py`) that tells us
how to convert to and from a string representation that can be displayed on the screen and can interact with the form
display screen. Our User Task handler also has a method for translating a couple of basic JSON schema types into
something that can be displayed (supporting only text, integers, and 'oneOf'). The form screen collects and validates
the user input and collects the results in a dictionary.
We won't go into the details about how the form screen works, as it's specific to this application rather than the
library itself; instead we'll skip to the code that runs the task after it has been presented to the user; any
application needs to do this.
Simply running the task is sufficient for Manual Tasks.
.. code-block:: python
def on_complete(self, results):
self.task.run()
However, we need to extend this method for User Tasks, to incorporate the user-submitted data into the workflow:
.. code-block:: python
def on_complete(self, results):
self.task.set_data(**results)
super().on_complete(results)
Here we are setting a key for each field in the form. Other possible options here are to set one key that contains
all of the form data, or map the schema to Python class and use that in lieu of a dictionary. It's up to you to
decide the best way of managing this.
The key points here are that your application will need to have the capability to display information, potentially
incorporating data from the workflow instance, as well as update this data based on user input. We'll go through a
simple example next.
We'll refer to the process modeled in :bpmn:`task_types.bpmn` contains a simple form which asks a user to input a
product and quantity as well a manual task presenting the order information at the end of the process (the form is
defined in :form:`select_product_and_quantity.json`
After the user submits the form, we'll collect the results in the following dictionary:
.. code-block:: python
{
'product_name': 'product_a',
'product_quantity': 2,
}
We'll add these variables to the task data before we run the task. The Business Rule task looks up the price from a
DMN table based on :code:`product_name` and the Script Task sets :code:`order_total` based on the price and quantity.
Our Manual Task's instructions look like this:
.. code-block::
Order Summary
{{ product_name }}
Quantity: {{ product_quantity }}
Order Total: {{ order_total }}
and when rendered against the instance data, reflects the details of this particular order.
Business Rule Tasks
-------------------
Business Rule Tasks are not implemented in the :code:`SpiffWorkflow.bpmn` module; however, the library does contain
a DMN implementation of a Business Rule Task in the :code:`SpiffWorkflow.dmn` module. Both the :code:`spiff` and
:code:`camunda` modules include DMN support.
Gateways
--------
You will not need special code to handle gateways (this is one of the things this library does for you), but it is
worth emphasizing that gateway conditions are treated as Python expressions which are evaluated against the context of
the task data. See :doc:`script_engine` for more details.
Script and Service Tasks
------------------------
See :doc:`script_engine` for more information about how Spiff handles these tasks. There is no default Service Task
implementation, but we'll go over an example of one way this might be implemented there. Script tasks assume the
:code:`script` attribute contains the text of a Python script, which is executed in the context of the task's data.
.. _task_filters:
Filtering Tasks
===============
SpiffWorkflow has two methods for retrieving tasks:
- :code:`workflow.get_tasks`: returns a list of matching tasks, or an empty list
- :code:`workflow.get_next_task`: returns the first matching task, or None
Both of these methods use the same helper classes and take the same arguments -- the only difference is the return
type.
These methods return a :code:`TaskIterator`, which in turn uses a :code:`TaskFilter` to determine what tasks match.
Tasks can be filtered by:
- :code:`state`: a :code:`TaskState` value (see :ref:`states` for the possible states)
- :code:`spec_name`: the name of a Task Spec (this will typically correspond to the BPMN ID)
- :code:`manual`: whether the Task Spec requires manual input
- :code:`updated_ts`: limits results to after the provided timestamp
- :code:`spec_class`: limits results to a particular Task Spec class
- :code:`lane`: the lane of the Task Spec
- :code:`catches_event`: Task Specs that catch a particular :code:`BpmnEvent`
Examples
--------
We reference the following processes here:
- :bpmn:`top_level.bpmn`
- :bpmn:`call_activity.bpmn`
To filter by state, We need to import the :code:`TaskState` object (unless you want to memorize which numbers
correspond to which states).
.. code-block:: python
from SpiffWorkflow.util.task import TaskState
Ready Human Tasks
^^^^^^^^^^^^^^^^^
.. code-block:: python
tasks = workflow.get_tasks(state=TaskState.READY, manual=False)
Completed Tasks
^^^^^^^^^^^^^^^
.. code-block:: python
tasks = workflow.get_tasks(state=TaskState.COMPLETED)
Tasks by Spec Name
^^^^^^^^^^^^^^^^^^
.. code-block:: python
tasks = workflow.get_tasks(spec_name='customize_product')
will return a list containing the Call Activities for the customization of a product in our example workflow.
Tasks Updated After
^^^^^^^^^^^^^^^^^^^
.. code-block:: python
ts = datetime.now() - timedelta(hours=1)
tasks = workflow.get_tasks(state=TaskState.WAITING, updated_ts=ts)
Returns Tasks that changed to :code:`WAITING` in the past hour.
Tasks by Lane
^^^^^^^^^^^^^
.. code:: python
ready_tasks = workflow.get_tasks(state=TaskState.READY, lane='Customer')
will return only Tasks in the 'Customer' lane in our example workflow.
Subprocesses and Call Activities
================================
In the first section of this document, we noted that :code:`BpmnWorkflow` is instantiated with a top level spec as
well as a collection of specs for any referenced processes. The instantiated :code:`BpmnSubWorkflows` are maintained
as mapping of :code:`task.id` to :code:`BpmnSubworkflow` in the :code:`subprocesses` attribute.
Both classes inherit from :code:`Workflow` and maintain tasks in separate task trees. However, only
:code:`BpmnWorkflow` maintains subworkflow information; even deeply nested workflows are stored at the top level (for
ease of access).
Task iteration also works differently as well. :code:`BpmnWorkflow.get_tasks` has been extended to retrieve
subworkflows associated with tasks and iterate over those as well; when iterating over tasks in a
:code:`BpmnSubWorkflow`, only tasks from that workflow will be returned.
.. code-block:: python
task = workflow.get_next_task(spec_name='customize_product')
subprocess = workflow.get_subprocess(task)
subprocess_tasks = subprocess.get_tasks()
This code block finds the first product customization of our example workflow and gets only the tasks inside that
workflow.
A :code:`BpmnSubworkflow` always uses the top level workflow's script engine, to ensure consistency.
Additionally, the class has a few extra attributes to make it more convenient to navigate across nested workflows:
- :code:`subworkflow.top_workflow` returns the top level workflow
- :code:`subworkflow.parent_task_id` returns the UUID of the task the workflow is associated with
- :code:`parent_workflow`: returns the workflow immediately above it in the stack
These methods exist on the top level workflow as well, and return :code:`None`.
Events
======
BPMN Events are represented by :code:`BpmnEvent` class. An instance of this class contains an :code:`EventDefinition`,
an optional payload, message correlations for Messages that define them, and (also optionally) a target subworkflow.
The last property is used internally by SpiffWorkflow by subworkflows that need to communicate with other subworkflows
and can be safely ignored.
The relationship between the :code:`EventDefinition` and :code:`BpmnEvent` is analagous to that of :code:`TaskSpec`
and :code:`Task`: a :code:`TaskSpec` defining a BPMN Event has an additional :code:`event_definition` attribute that
contains the information about the Event that will be caught or thrown.
When an event is thrown, a :code:`BpmnEvent` will be created using the :code:`EventDefinition` associated with the
task's spec, and payload, if applicable. For events with payloads, the :code:`EventDefinition` will define how to
create the payload based on the workflow instance and include this with the event. A Timer Event will know how to
parse and evaluate the provided expression. And so forth.
The event will be passed to the :code:`workflow.catch` method, which will iterate over the all the tasks and pass the
event to any tasks that are waiting for that event. If no tasks that catch the event are present in the workflow, the
event will placed in a pending event queue and these events can be retrieved with the :code:`workflow.get_events`
method.
.. note::
This method clears the event queue, so if your application retrieves the event and does not handle it, it is gone
forever!
The application in this repo is designed to run single workflows, so it does not have any external event handling.
If you implement such functionality, you'll need a way of identifying which processes any retrieved events should be
sent to.
The :code:`workflow.waiting_events` will return a list of :code:`PendingBpmnEvents`, which contain the name and type
of event and might be used to help determine this.
Once you have determined which workflow should receive the event, you can pass it to :code:`workflow.catch` to handle
it.

View File

@@ -1,13 +1,25 @@
SpiffWorkflow Concepts
======================
Fundamental SpiffWorkflow Concepts
==================================
Overview
--------
The purpose of this library is to provide a general workflow execution enviroment with a wide variety of built-in task
types that support many different workflow patterns.
SpiffWorkflow keeps track of task dependencies and states and provides the ability to serialize or deserialize a
workflow that has not been completed. Developers using this library can focus on displaying a workflows state and
presenting its tasks to users of their application.
.. _specs_vs_instances:
Specifications vs. Instances
----------------------------
SpiffWorkflow consists of two different categories of objects:
- **Specification objects**, which represent the definitions and derive from :code:`WorkflowSpec` and :code:`TaskSpec`
- **Instance objects**, which represent the state of a running workflow (:code:`Workflow`, :code:`BpmnWorkflow` and :code:`Task`)
- **Specification objects**, which represent definitions of structure and behavior and derive from :code:`WorkflowSpec` and :code:`TaskSpec`
- **Instance objects**, which represent the state of a running workflow (:code:`Workflow`/:code:`BpmnWorkflow` and :code:`Task`)
In the workflow context, a specification is model of the workflow, an abstraction that describes *every path that could
be taken whenever the workflow is executed*. An instance is a particular instantiation of a specification. It describes *the
@@ -23,21 +35,28 @@ Specifications are unique, whereas instances are not. There is *one* model of a
Imagine a workflow with a loop. The loop is defined once in the specification, but there can be many tasks associated with
each of the specs that comprise the loop.
In our BPMN example, described a product selection process.::
In our BPMN example (:ref:`quickstart`), we described a product selection process:
Start -> Select and Customize Product -> Continue Shopping?
Since the customer can potentially select more than one product, how our instance looks depends on the customer's actions. If
they choose three products, then we get the following tree::
they choose three products, then we get the following path::
Start --> Select and Customize Product -> Continue Shopping?
|-> Select and Customize Product -> Continue Shopping?
Start --> Select and Customize Product -> Continue Shopping? -> |
/-------------------------------------------------------- /
|-> Select and Customize Product -> Continue Shopping? -> |
/-------------------------------------------------------- /
|-> Select and Customize Product -> Continue Shopping?
There is *one* TaskSpec describing product selection and customization and *one* TaskSpec that determines whether to add more
items, but it may execute any number of imes, resulting in as many Tasks for these TaskSpecs as the number of products the
items, but it may execute any number of times, resulting in as many Tasks for these TaskSpecs as the number of products the
customer selects.
A Task Spec may have multiple inputs (if there are multiple paths to reach it) but a Task has only one parent. A specification
may contains cycles, but an instantiated workflow is *always* a tree.
.. _states:
Understanding Task States
-------------------------
@@ -52,7 +71,7 @@ Understanding Task States
* **DEFINITE** Tasks
Definite tasks are certain to run as the workflow pregresses.
Definite tasks are certain to run as the workflow progresses.
- **FUTURE**: The task will definitely run.
- **WAITING**: A condition must be met before the task can become **READY**
@@ -67,13 +86,11 @@ Understanding Task States
- **ERROR**: The task finished unsucessfully.
- **CANCELLED**: The task was cancelled before it ran or while it was running.
Tasks start in either a **PREDICTED** or **FUTURE** state, move through one or more **DEFINITE** states, and end in a
**FINISHED** state. State changes are determined by several task spec methods:
**FINISHED** state. State changes are determined by task spec methods.
Hooks
=======
-----
SpiffWorkflow executes a Task by calling a series of hooks that are tightly coupled
to Task State. These hooks are:

View File

@@ -18,24 +18,33 @@
# -- Project information -----------------------------------------------------
project = 'SpiffWorkflow'
copyright = '2023, Sartography'
copyright = '2024, Sartography'
author = 'Sartography'
# The full version, including alpha/beta/rc tags
release = '2.0.0rc0'
release = '3.0.0rc0'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinx.ext.autodoc', # 'sphinx.ext.coverage',
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.autosummary',
'sphinx.ext.extlinks',
'sphinx_rtd_theme',
#'sphinx.ext.intersphinx',
]
# Configure links to example repo
branch = 'improvement/better-interactive-workflow-runner'
extlinks = {
'example': (f'https://github.com/sartography/spiff-example-cli/tree/{branch}/' + '%s', '%s'),
'bpmn': (f'https://github.com/sartography/spiff-example-cli/tree/{branch}/bpmn/tutorial/' + '%s', '%s'),
'form': (f'https://github.com/sartography/spiff-example-cli/tree/{branch}/bpmn/tutorial/forms/' + '%s', '%s'),
'app': (f'https://github.com/sartography/spiff-example-cli/tree/{branch}/spiff_example/' + '%s', '%s'),
}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -55,7 +64,7 @@ html_theme = "sphinx_rtd_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
#html_static_path = ['_static']
# Set the master index file.
master_doc = 'index'

View File

@@ -1,10 +1,9 @@
Core Library
============
SpiffWorkflow's BPMN support is built on top of a core library that aims to be a general workflow
execution environment. Workflow specifications can be created from a simple XML format, or even
easily in python code. It supports a wide range of task specifications and workflow patterns, making
it amenable to adaptation to many different schemas for defining workflow behavior.
Workflow specifications can be created from a simple XML format, or even directly in python code. The library includes
a wide range of task specifications and supports many different workflow patterns, making it amenable to adaptation to
many different schemas for defining workflow behavior.
.. toctree::
:maxdepth: 2

View File

@@ -20,15 +20,17 @@ What is SpiffWorkflow?
**SpiffWorkflow is a library that provides a flexible workflow execution environment.**
Recent development has largely focused on allowing Python applications to process
BPMN diagrams (think of them as very powerful flow charts; see :doc:`bpmn/intro`). to
accomplish what would otherwise require writing a lot of complex business logic in your
code. You can use these diagrams to accomplish a number of tasks, such as:
SpiffWorkflow is the workflow library underlying `Spiff Arena <https://github.com/sartography/spiff-arena>`_.
- Creating a questionnaire with multiple complex paths
- Implement an approval process that requires input from multiple users
- Allow non-programmers to modify the flow and behavior of your application
- Visualize and manage long running data processing tasks
It consists of a generic core library, with packages supporting parsing and execution of BPMN diagrams that extend
this core.
Extensive documentation about BPMN and how SpiffWorkflow interprets it, as well as information about custom extensions
implemented in the :code:`spiff` package can be found in the
`Spiff Arena documentation <https://spiff-arena.readthedocs.io/en/latest/>`_. If you are not familiar with BPMN, you
should start there. If you are looking for a full-fledged BPMN application, you can start and end there. This
documentation will focus on the library itself and is geared towards developers who are building their own
applications.
Please visit `SpiffWorkflow.org <https://www.spiffworkflow.org>`_ for
additional articles, videos, and tutorials about SpiffWorkflow and its
@@ -54,8 +56,7 @@ Contents
.. toctree::
:maxdepth: 2
bpmn/overview
bpmn/intro
concepts
modules
bpmn/index
core/index

54
doc/modules.rst Normal file
View File

@@ -0,0 +1,54 @@
Modules
=======
SpiffWorkflow consists several modules. The modules included in this package and reasons for its structure are historical.
The library originally consisted of only the core library specs; it was later extended to provide BPMN support (in a way
that did not entirely fit in with the operation of the original library); then this BPMN support was further refined to
provide support for custom extensions to the BPMN spec. Recent redevlopment has mainly focused on BPMN, with minimal
changes to the core library. Therefore there are fairly significant differences between the BPMN and non-BPMN parts of the
library. We're working towards making these consistent!
The Core Library
----------------
The core library provides basic implementations of a large set of task spec types and basic workflow execution
capabilities.
- Specs implementations are in :code:`specs`
- Workflow implementation is in :code:`workflow.py`
- Task implementation is in :code:`task.py`, with utilities for iteration and filtering in :code:`util.task.py`
It is documented in :doc:`core/index`.
Generic BPMN Implementation
---------------------------
This module extends the core implementation to support the parsing and execution of BPMN diagrams.
- The base specs of the core library are extended to implement generic BPMN attributes and behavior in
:code:`bpmn.specs`. Task specs are extended in two ways: to provide generic behavior common to all BPMN tasks
(:code:`bpmn.specs.mixins.bpmn_spec_mixin.BpmnSpecMixin`) and type-specific behavior (other task specs in
:code:`bpmn.specs.mixins`). The reason is to allow either category of properties to be extended separately.
- The workflow implementation in the BPMN package handles subworkflows in an entirely different way from the core
library. It also allows for filtering on and iterating over tasks with certain BPMN attributes.
- A workflow has a scripting environment, in which task specific code will be executed (in the core library, this
would be accomplished through an execute task that spawns a subprocess or a custom task spec).
- The serializer has been completely replaced.
This module is documented in :doc:`bpmn/index`.
Spiff BPMN Extensions
---------------------
This module extends the generic BPMN implementation with support for custom extensions used by
`Spiff Arena <https://spiff-arena.readthedocs.io/en/latest/>`_.
Camunda BPMN Extensions
-----------------------
This module extends the generic BPMN implementation with limited support for Camunda extensions.
.. warning::
The Camunda package is not under development. We'll accept contributions from Camunda users, but we are not
actively maintaining this package.