the flor language
Flor is a language for defining workflows for the flor workflow engine.
A flor engine is an interpreter for the flor language. Usually an interpreter lives in an OS process and interprets a single program. With a flor engine, there are multiple program instances, called workflow executions, being interpreted in the same engine. Flor can host multiple executions of the same workflow definition or executions of various, different, workflow definitions. They could be various versions of the same workflow definition, or even executions whose definitions diverged in-flight from their original definition (rewriting the program in-flight).
Executions route work between taskers. Taskers are orthogonal to workflow definitions/executions. Multiple definitions/executions share the same set of taskers. For example the "accounting department" tasker or the "send notification email" tasker may appear in multiple workflow definitions (in the same business domain usually).
Executions spend most of their time waiting for taskers' answers, especially if those taskers are human.
This meandering post tries to show what flor, the language, looks like, and to explain the motivations behind that. This post doesn't look at tasker implementation, it is sufficient to know that they are scripts (Ruby or not) accepting task messages and replying with an updated task payload or rejecting the task.
the language itself
Here is a sample flor workflow definition:
sequence
set item_id # the launch data is used
create_mandate _ # to create a new mandate
cursor
ops 'assign container number' # operations team
update_mandate status: f.status # ops decides on status
break _ if f.status == 'numbered' # break out of cursor if 'numbered'
rm 'rework mandate' # relationship manager
update_mandate status: 'agreed' # refines mandate data
continue _ # mandate goes back to operations...
pfs 'activate mandate' # portfolio team
update_mandate status: 'active' # activates the mandate
email 'rm' 'awm mandate activated' # success, email sent to rel manager
A few observations:
- It is indented
- There are no end of indentation levels, a leftward indentation change ends one or more indentation blocks, a rightward indentation change starts a new indentation block, it's like Python
- Lines begin with a word followed by arguments
- Arguments are strings or key: value entries, or sometimes
_
- Comments start with a
#
sign and end with the end of their line - The head of line word sometimes is an imperative (set, break, update_mandate, email, ...), sometimes a noun (sequence, rm, ...)
- Like in Ruby, some lines end in a condition, for example:
break _ if f.status == 'numbered'
A few answers:
Each non-comment line is a "call" of the head-of-line word procedure
Some of those heads are common flor procedures (set, sequence, cursor, break, continue, ...)
Some of those heads are taskers (pieces of code registered in the flor engine and available to multiple flor definitions), here, for example, "rm", "ops" and "pfs" point to three distinct group in the organization using the flor engine, they are "human taskers"
Some of those heads are taskers pointing to non-human taskers, like "email" and "update_mandate", such taskers tend to perform their task immediately, while human taskers are usually delivering the task to a queue/list system where humans may pick them (and drop them back eventually)
Some of those heads could be functions (sorry, no function calls in the example above).
The suffix conditionals are in fact syntactic sugar:
break _ if f.status == 'numbered'
#
# gets rewritten to:
#
if
f.status == 'numbered' # conditional
break _ # then branch
# nada... # else branch
Such rewrites occur when the flow reaches the break/if, not before.
The _
(underscore) as a single argument is necessary to distinguish getting the value of a head from calling it.
Consider:
define get-name
"Hector"
get-name # yields a pointer to the "get-name" function
get-name _ # yields "Hector"
Yielding a pointer to a function has its uses, for example, when aliasing it:
define add x y \ + x y
set plus add # alias plus to add
plus 1 2 # yields 3
set plus # set plus
add 2 3 # to the value of `(add 2 3)`, yields 5
plus 3 4 # yields 5, not 7
I'm sure there are more questions, feel free to open a question/issue on the flor issue tracker or to drop in for a chat in the flor chat room.
why such a language?
Like all programming languages, flor sits between the user and the interpreter. It has to be understood by the interpreter (fairly easy) and by the user (difficult) and his co-workers (even more difficult).
Business people out there know and use BPMN. Why not use that?
Please note that I describe flor as a "workflow engine", not a "business process management suite". I am not really targetting business users, I am targetting fellow developers. Funnily, for most developpers, a workflow is in the realm of "my workflow", the set of ways in which they weave their work, all alone. Granted sometimes, there are team workflows, but there are much more blog posts about "my development workflow" out there than about team workflows.
Still, why not use BPMN? I prefer a text programming language to a visual programming language. I could go christian and say "In the beginning was the word", not the diagram. I also do not think that a diagram is not a program, there is no "no code", a workflow definition, text or diagram, is a piece of code. Most diagrams out there are backed by textual notations, we're back to word, text.
The basic blocks in a programming language come most of the time straight out of the English language: GOTO, if, car, cdr, RETF, whatever... We strive to have programs reading like English. If I target developers, I have not to forget that they'll be sharing those programs, those workflow definitions, with business users. It'd better be readable.
As already written, the flor language essentially routes work among taskers. Concise definitions should be readable. Throwing the work over the wall to a tasker should hide/abstract most of the complexity, then flor allows for functions, a classical way to abstract details away, flor then routes work among taskers and functions. The way a tasker is called is similar to the way a function is called: tasker name or function name, followed by arguments.
define phase1
concurrence
alice 'call A-N customers'
bob 'call M-Z customers'
define phase2
sequence
alice 'get customer confirmation'
bob 'do something else'
sequence
phase1 _ # function call
phase2 _ # function call
charly 'final phase' # tasker invocation
#verb_or_noun arguments*
# verb_or_noun arguments*
I am not against diagrams, I think they are great to provide the coup d'oeil that business users want. My language should lend itself to (semi-)automatic generation of diagrams. If "technical details" are hidden behind abstractions, meaningful diagram generation should be possible.
Ruote, the predecessor to flor had a Javascript library for diagram generation. Flor should have one too.
It still takes discipline to write readable flow definitions or to write graspable BPMN diagrams.
a Lisp bastard
Flor is a Lisp bastard, one of those that lost the parentheses. A word on its own is replaced by the value it stands for. A word is followed by arguments and we have a function call or a tasker invocation, or simply a procedure call.
The 10th Greenspun rule needn't apply, I am deliberately bringing a Lisp to the table. Granted, "bug-ridden", "slow" and "half-complete" may still hold.
If Lisp is the mother, who's the father? Ruby for sure.
# Lispish
#
map [ 1, 2, 3 ] #
def elt #
+ elt 3 # "map" accepts a function
# Rubyish
#
collect [ 1, 2, 3 ] #
+ elt 3 # while "collect" is a macro rewriting to a "map"
# both yield [ 4, 5, 6 ]
Surely, using flor to add 3 to a sequence of integers is a waste. Here is maybe a better example:
define notify_warranty_void users
#
set emails #
users #
collect #
elt.email # collect the email for each user into "emails"
notify emails "warranty void" # call to function "notify"
#
# OR
#
notify (users | collect \ elt.email) "warranty void" # as a one-liner
There is a list of the flor procedures, sorry, it doesn't contain "car" or "cdr" for the time being.
Why a Lisp bastard for defining workflows? I could answer that it's because I got taught about programming with the Wizard Book, but it's rather my wish for a language close to its symbolic expressions. I want this (verb_or_noun arguments*)
structure.
conclusion
It can be said that I have an obsession for workflow engines, maybe it's more of a quest for a workflow language.
Lots of business processes are weaving among participants and services. Some of them are described (as-is BPMN diagrams), lots of them are bizlore.
Some business processes are backed by state machines, but most of the time, those are lifecycle business processes, they concern 1 entity, there might be a higher level business process driving the lifecycle of multiple entities.
Some business processes can be executed advantageously by a workflow engine. Having a workflow definition mapping one-to-one to a workflow execution is great. The definition can be versioned, executions of one or more versions of the same definition may coexist.
A portfolio of concise business processes / workflow definitions is very important. It takes discipline.
I am trying to provide a fun language to build that with.