Skip to main content

New handlers for gateways, collections and timers in Flow

· 4 min read
Sondre Gjellestad
Stacc - Team Arbeidsflyt
note

These new features are currently in beta.

Making use of features like timers, collections and gateways previously required managing a set of variables existing outside the Flow state that were used by JUEL expressions inside the diagram.

With these new handlers, it is possible to use those features without coordinating variables across the handler code and the expressions in the diagram.

By introducing new handlers defined in code, we can enable improved readability and easier diffing. More of the code is now in the process folder, as JavaScript. The only relation between diagram elements and the logic inside Flow is now the ID of the element.

Going forward, one of the goals of this is to remove the distinction between state and variables, eventually making variables unnecessary.

The new handlers will be used if no expression is set in the diagram and the handler file exists.

Usage

Setting the trigger time of timers

Handlers for timers are created inside the handlers/timers directory inside a process. The handler must export a function called triggerAt that returns a Date object.

// handlers/timers/my-timer.js
function triggerAt({ state }) {
if (state.urgent) {
return new Date();
}
return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
}

Selecting outgoing paths from gateways

Previously, defining the logic for determining the path taken out of a gateway, required setting relevant variables in code and defining a JUEL expression per branch.

The outgoing paths that are selected from gateways can be controlled in code using a gateway handler that returns the ID of the path (sequence flow) that should be taken.

// handlers/gateways/my-gateway.js
function selectBranches({ state, scope, variables, meta }) {
if (state.age < 18) {
return "no";
}
return "yes";
}

If a default path is defined in the diagram, the handler can return null to use that path.

// handlers/gateways/requires-special-attention.js
function selectBranches({ state, scope, variables, meta }) {
if (state.creditScore < 0.5) {
return "human-assessment";
}
// This results in an error if no default path is defined
return null;
}

It is also possible to return an array of paths to be taken for an inclusive gateway.

// handlers/gateways/ingredients-to-prepare.js
function selectBranches({ state, scope, variables, meta }) {
const paths = ["chili", "rice"];
if (state.vegetarian) {
paths.push("beans");
} else {
paths.push("meat");
}
return paths;
}

Defining collections for multi-instance activities

Collections make it possible to execute an activity multiple times, with each iteration being executed with a different element from the collection.

The collection-handler makes it possible to define a collection in regular code.

Each element from the returned values will be available in the activity scope under the key specified in the returned object, e.g. document in the example below.

// handlers/service-tasks/sign-documents.js
function collection({ state }) {
const documents = state.documents;
return {
key: "document",
// The execute function will be invoked once for each document
values: documents
};
}

function execute({ scope }) {
// The value of each element in the collection becomes available under the
// value of the returned `key`.
const document = scope.document;
console.log("signing document...", document.id);
return { id: document.id };
}

function map({ data }) {
return {
patch: [
{
op: "add",
path: "/signed",
value: data.id
}
]
};
}

module.exports = { collection, execute, map };

It is also possible to define a collection for sub-processes, call activities and transactions. The values they return will be available in the scope provided to each.

Here is an example of a collection handler for a sub-process, that executes a service task multiple times.

// state
{
"lasagna": { "selected": true },
"tacos": { "selected": false },
"pizza": { "selected": true }
}
// handlers/sub-processes/prepare-meal.js
function collection({ state }) {
const meals = state.meals;
const selectedMealIds = Object.keys(meals).filter(id => meals[id].selected);
return {
key: "mealId",
values: selectedMeals.map(meal => ({ id: mealId }))
};
}
// handlers/service-tasks/prepare-meal.js
function execute({ state, scope }) {
const meal = scope.mealId;
console.log("preparing meal...", meal);
return { id: meal.id };
}
note

For performance reasons, avoid placing large objects in the collection values. We recommend using identifiers that point to the full object in the flow state to minimize the amount of data stored in the collection values.

Going forward

These new features are currently in beta.

Hopefully these features make it easier to understand the relation between the BPMN diagrams and the handler code.

We're eager to hear your thoughts on these new features! Your feedback is important for us to continue improving. If you have suggestions for further enhancements or encounter any issues, please reach out.

Check out the full documentation for the new handlers in the Flow documentation.