SCXML Workflow Execution
Overview
The SCXML Workflow Engine in Bloomreach Content provides a generic org.onehippo.repository.scxml.SCXMLWorkflowExecutor for executing SCXML workflow definitions stored in the repository.
A SCXMLWorkflowExecutor is a generic class. You instantiate it (and optionally extend it) using a org.onehippo.repository.scxml.SCXMLWorkflowContext and, if needed, an implementation of the org.onehippo.repository.scxml.SCXMLWorkflowData interface.
The following example from org.onehippo.repository.documentworkflow.DocumentWorkflowImpl.setNode(Node) demonstrates how to create a SCXMLWorkflowExecutor:
import org.onehippo.repository.scxml.SCXMLWorkflowContext; import org.onehippo.repository.scxml.SCXMLWorkflowExecutor; public static final String SCXML_DEFINITION_KEY = "scxml-definition"; private SCXMLWorkflowExecutor<SCXMLWorkflowContext, DocumentHandle> workflowExecutor; @Override public void setNode(final Node node) throws RepositoryException { super.setNode(node); String scxmlId = "documentworkflow"; try { final RepositoryMap workflowConfiguration = getWorkflowContext().getWorkflowConfiguration(); // check if a custom scxml-definition identifier is configured for this workflow instance if (workflowConfiguration != null && workflowConfiguration.exists() && workflowConfiguration.get(SCXML_DEFINITION_KEY) instanceof String) { // use custom scxml-definition identifier scxmlId = (String) workflowConfiguration.get(SCXML_DEFINITION_KEY); } // instantiate SCXMLWorkflowExecutor using default SCXMLWorkflowContext and extended SCXMLWorkflowData class DocumentHandle workflowExecutor = new SCXMLWorkflowExecutor<>(new SCXMLWorkflowContext(scxmlId, getWorkflowContext()), new DocumentHandle(node)); } catch (WorkflowException wfe) { if (wfe.getCause() != null && wfe.getCause() instanceof RepositoryException) { throw (RepositoryException)wfe.getCause(); } throw new RepositoryException(wfe); } }
The SCXMLWorkflowContext supplies the scxmlId (the node name) of the SCXML workflow definition in the repository. The SCXMLWorkflowExecutor loads this definition using the org.onehippo.repository.scxml.SCXMLRegistry.
The SCXMLRegistry is a service module registered via org.onehippo.cms7.services.HippoServiceRegistry.
After loading the SCXML workflow definition, the engine instantiates a SCXML state machine using org.apache.commons.scxml2.SCXMLExecutor. The SCXMLWorkflowExecutor manages all further interactions with this state machine.
Within the SCXML state machine, the root context exposes the SCXMLWorkflowContext and the optional SCXMLWorkflowData as the "workflowContext" and "workflowData" variables.
Interacting with the SCXML Workflow State Machine
To interact with SCXML workflow state machines, you must follow specific conventions in both the SCXML state machine definition and your integration code.
Defining and Restricting Workflow Actions
To control which workflow operations (actions) are available at any time, use the <Map<String, Boolean> SCXMLWorkflowContext.getActions() method. The SCXML state machine should update this map to reflect which actions are currently enabled.
Use the org.onehippo.repository.scxml.ActionAction (see SCXML Workflow Actions and Tasks) in your SCXML definition as follows:
<!-- if not draft document holder AND granted hippo:admin --> <if cond="!editor and workflowContext.isGranted(draft,'hippo:admin')"> <!-- then "unlock" action" is enabled --> <hippo:action action="unlock" enabledExpr="true"/> </if>
This stores the "unlock" action with value Boolean.TRUE in the actions map.
After starting the SCXML workflow state machine, your workflow implementation can read the actions map (for example, in the hints() method) to communicate available operations to the workflow invoker, such as the CMS.
The SCXMLWorkflowExecutor checks the actions map when you call triggerAction(String action). If the action is not present or not enabled, it throws a WorkflowException.
You can also provide a custom actions map using triggerAction(String action, Map<String, Boolean> actionsMap).
SCXML actions should correspond to SCXML event names in the workflow definition:
<transition event="unlock"> <!-- unlock the current draft document by setting the holder to the current (hippo:admin) user --> <hippo:setHolder holder="user"/> </transition>
Trigger the workflow operation as follows:
@Override public void unlock() throws WorkflowException { workflowExecutor.start(); workflowExecutor.triggerAction("unlock"); }
Important: Always call workflowExecutor.start() before invoking triggerAction(). This ensures the state machine is (re)initialized, the current allowed actions are updated, and the workflow data is ready.
Providing Additional Feedback
The SCXML workflow state machine can supply additional feedback using the Map<String, Serializable> SCXMLWorkflowContext.getFeedback() method.
Use the org.onehippo.repository.scxml.FeedbackAction (see SCXML Workflow Actions and Tasks) in your SCXML:
<!-- provide the current draft document holder as "inUseBy" feedback --> <hippo:feedback key="inUseBy" value="holder"/>
The workflow engine itself does not use this feedback, but you can return it (for example, in hints()) to the workflow invoker.
Retrieving Results from Workflow Events
When a SCXML workflow event produces a result, you can return it to the workflow invoker or process it further.
Use the org.onehippo.repository.scxml.ResultAction (see SCXML Workflow Actions and Tasks) in your SCXML:
<transition event="obtainEditableInstance"> <if cond="!!unpublished"> <!-- unpublished document exists: copy it to draft first --> <hippo:copyVariant sourceState="unpublished" targetState="draft"/> <elseif cond="!!published"/> <!-- else if published document exists: copy it to draft first --> <hippo:copyVariant sourceState="published" targetState="draft"/> </if> <!-- mark the draft document as modified, set the user as editor and remove possibly copied availabilities --> <hippo:configVariant variant="draft" applyModified="true" setHolder="true" availabilities=""/> <!-- store the newly created or updated draft document as result --> <hippo:result value="draft"/> </transition>
In this example, a draft document is created or updated. The resulting document object is stored in the SCXMLWorkflowContext via <hippo:result>. You can retrieve the result using SCXMLWorkflowContext.getResult() or as the return value of SCXMLWorkflowExecutor.triggerAction():
@Override public Document obtainEditableInstance() throws RepositoryException, WorkflowException { workflowExecutor.start(); return (Document)workflowExecutor.triggerAction("obtainEditableInstance"); }
Checking Access Privileges in the State Machine
The SCXMLWorkflowContext provides methods to check privileges on a document:
boolean isGranted(Document, String privileges)boolean isGranted(Node, String privileges)
The privileges parameter can be a comma-separated list.
Use these methods in your SCXML to control access, as shown in the earlier "unlock" example. The SCXMLWorkflowContext evaluates privileges using the WorkflowContext.getSubjectSession() JCR session and caches the result. The cache is cleared whenever the state machine is (re)started.
Using SCXMLWorkflowData
To expose external data to the SCXML state machine, instantiate the SCXMLWorkflowExecutor with an implementation of SCXMLWorkflowData.
The SCXMLWorkflowData interface defines two methods: initialize() and reset(). The executor calls these automatically.
The SCXML state machine accesses the SCXMLWorkflowData instance via the root context. You can reference it in Groovy expressions or custom SCXML actions.
For example, DocumentWorkflow uses a custom org.onehippo.repository.documentworkflow.DocumentHandle (which implements SCXMLWorkflowData) to provide access to the current document state, its variants, and workflow requests.
When implementing a custom SCXMLWorkflowData class, ensure that all volatile data is loaded only after initialize() and cleared in reset().
Using a Global Script in the SCXML State Machine
The SCXML specification allows a global script element that runs when the state machine initializes.
Apache Commons SCXML extends this by inheriting the root context (the <scxml> element) to all states. The global script can inject additional data or methods, making them available throughout the state machine.
Because Bloomreach uses Groovy as the SCXML expression language, you can define Groovy methods in the global script. These methods are inherited by all child scripts and condition expressions.
Restrictions:
- Each script and condition expression is compiled as a separate Groovy class and cached for the lifetime of the workflow definition.
- Compilation occurs on first access.
- Do not use instance variables, conditionally defined methods, or Groovy closures in scripts.
- All other Groovy features are supported.
Warning: Invoking methods such as System.exit() from scripts will terminate the CMS instance.
The SCXML DocumentWorkflow uses the global script to define helper methods for evaluating SCXMLWorkflowData values:
<scxml version="1.0" xmlns="http://www.w3.org/2005/07/scxml" xmlns:hippo="http://www.onehippo.org/cms7/repository/scxml" xmlns:cs="http://commons.apache.org/scxml" initial="handle"> <script> ... // published variant property method def getPublished() { workflowData.documents['published'] } ... // current requests map property method def getRequests() { workflowData.requests } .. // true if draft exists and currently being edited def boolean isEditing() { !!holder } .. // true if there is an outstanding workflow request def boolean isRequestPending() { workflowData.requestPending } </script> ... <state id="no-request"> <!-- transition to state "requested" when requests exists --> <transition target="requested" cond="!empty(requests)"/> </state> ... <!-- transition to state "editing" when there is no pending request and the draft variant is being edited --> <transition target="editing" cond="!requestPending and editing"/> <!-- else transition to state "editable" when there is no pending request and the draft variant doesn't exist yet or isn't being edited --> <transition target="editable" cond="!requestPending"/> ... <if cond="workflowContext.isGranted(published, 'hippo:editor')"> <hippo:action action="depublish" enabledExpr="true"/> </if> ... </scxml>
State Machine State Configuration
When defining SCXML workflow states, especially with parallel states, use the following convention:
<state id="mystate"> <state id="no-mystate"> <transition target="mystate-enabled" cond="mystate.condition == true"/> </state> <state id="mystate-enabled"> ... </state> </state>
For example, in SCXML DocumentWorkflow:
<state id="versioning"> <state id="no-versioning"> <!-- a document only becomes versionable once an unpublished document variant exists --> <transition target="versionable" cond="!!unpublished"/> </state> <state id="versionable"> ... </state> </state>
This approach ensures that the state machine only transitions to "versionable" when the condition is met, enabling related actions and events. Otherwise, it remains in the initial "no-versioning" state. This pattern improves readability and testability.
Using SCXML Event Payload Data
You can provide additional payload data when triggering SCXML events, either externally via SCXMLWorkflowExecutor.triggerAction(String action, Map<String, Object> payload) or internally using the <send> element.
The SCXMLWorkflowExecutor requires the payload as a Map to align with how the <send> element provides payloads. This enables consistent event handling for both internal and external events.
Access the payload in the SCXML state machine using the standard _event system variable. For example:
<transition event="copy"> <hippo:copyDocument destinationExpr="_event.data?.destination" newNameExpr="_event.data?.name"/> </transition>
Trigger this event in Java:
protected Map<String, Object> createPayload(String var1, Object val1, String var2, Object val2) { HashMap<String, Object> map = new HashMap<>(); map.put(var1, val1); map.put(var2, val2); return map; } @Override public void copy(final Document destination, final String newName) throws WorkflowException { workflowExecutor.start(); workflowExecutor.triggerAction("copy", createPayload("destination", destination, "name", newName)); }
Note: Use the Groovy Safe Navigation Operator ?. to avoid NullPointerException when accessing optional payload data. If the event is triggered without a payload, _event.data will be null.
Using Local SCXML Data Variables
Commons SCXML allows you to define temporary data variables within the current state context using the org.apache.commons.scxml2.model.Var custom action element.
Use the <http://commons.apache.org/scxml:var> element to define variables accessible only within the current state and its children. These variables are automatically cleared when the state is exited.
This is useful when triggering internal events with the <send> element, which can include a namelist of variable names to build the event payload:
<scxml version="1.0" xmlns="http://www.w3.org/2005/07/scxml" xmlns:hippo="http://www.onehippo.org/cms7/repository/scxml" xmlns:cs="http://commons.apache.org/scxml"> ... <transition event="acceptRequest"> <!-- define temporary request variable for the event payload request parameter --> <cs:var name="request" expr="_eventdatamap.acceptRequest?.request"/> <!-- store the request workflow type as temporary variable --> <cs:var name="workflowType" expr="request.workflowType"/> <!-- store the request targetDate as temporary variable --> <cs:var name="targetDate" expr="request.scheduledDate"/> <!-- First delete the request itself. Note: after this the request object no longer can be accessed... which is why we had to create the temporary variables workflowType and targetDate above first! --> <hippo:deleteRequest requestExpr="request"/> <if cond="!targetDate"> <!-- the request didn't have a targetDate defined, simply trigger the "workflowType" value as event --> <send event="workflowType"/> <else/> <!-- the request did have a targetDate: trigger a 'scheduled' workflow action event --> <send event="workflowType" namelist="targetDate"/> </if> </transition>
In this example, <cs:var> defines temporary variables request, workflowType, and targetDate. The <send> element triggers an internal event, optionally including a payload based on the namelist.
Note: As of Commons SCXML 2.0 milestone 1, this is the only supported usage for the <send> element.
Raising a WorkflowException from the State Machine
You can raise a org.hippoecm.repository.api.WorkflowException directly from within the SCXML workflow state machine using the org.onehippo.repository.scxml.WorkflowExceptionAction.
Provide an error message and a condition expression. For example:
<hippo:workflowException condExpr="!_event?.data" errorExpr="No payload provided for the workflow copy event"/>
This action throws a WorkflowException if the event payload is missing.