Python workflow extensions
Python Workflow extensions in OmniFi allow developers to add custom functionality to extend the platform’s workflow engine with additional abilities. Extensions enable seamless integration of external systems and automation of complex operations directly within OmniFi workflows.
🛠 Building Workflow extensions in OmniFi
What is a Workflow extension?
A Workflow extension is a custom Python extension that seamlessly integrates with OmniFi's workflow engine to add specialized functionality. These extensions enable developers to create reusable components that handle complex business logic, external system integrations, and data transformations without requiring changes to OmniFi's core architecture.
Key capabilities of Workflow extension:
- Custom business logic: Implement specialized data processing, calculations, or decision-making algorithms
- External integrations: Connect to APIs, databases, file systems, or third-party services
- Dynamic workflows: Create conditional logic and routing based on runtime data
- Extensible architecture: Add new functionality to extend native OmniFi components
To create a Workflow extension in Python, it is required to implement the WorkflowPlugins class:
class WorkflowPlugin:
def __init__(self, system, user, password):
def SetKvStorageValues(self, StorageValues):
def GetConfigurationParameters(self):
def GetParameters(self, confArgs):
def GetOutput(self, confArgs):
def GetPorts(self, confArgs):
def Execute(self, confArgs, parameters):
def Shutdown(self):This is a Python class that extends OmniFi’s workflow engine with custom logic. It lets developers extend built-in capabilities (e.g., information processing, integrations, file handling, APIs).
- Configuration parameters: Set once when the extension is configured.
- Runtime parameters: Set each time the workflow runs (e.g., input values, URLs, options).
- Outputs: The data your extension returns (e.g., file paths, result arrays, response objects).
- Ports: Named output channels for routing results used by the workflow engine to continue execution.
- Execution logic: What happens when the workflow runs.
- Shutdown logic: Cleanup before the extension is unloaded (e.g., file removal, session closure).
Header file
Workflow extensions use the same header file as other extension types, with the addition of an IconClass property that can be used to select an font icon.
Required structure
To implement the Workflow structure, below steps need to be followed:
- Class name: A class named exactly WorkflowPlugin (
class WorkflowPlugin:). - Mandatory methods: All of the following methods must be implemented.
- Location: The Python file containing all the mentioned methods below should be placed in the Plugins\Workflow directory.
| Method | Purpose |
|---|---|
__init__(system, user, password) | Initialize plugin, set up logging/resources. |
GetParameters(confArgs) | Return a list of WorkflowArgument objects for runtime execution. |
GetOutput(confArgs) | Define the output schema via WorkflowOutput objects. |
GetPorts(confArgs) | Return a list of Port objects for routing. |
Execute(confArgs, parameters) | Main logic — process inputs and return a WorkflowResult. |
Shutdown() | Clean up resources before extension unload. |
These two methods are optional to implement.
Understanding data types
OmniFi uses the DataTypes class to define parameter and output types.
Understanding data types in OmniFi is essential for defining how data is represented, validated, and passed between different components of a workflow. The DataTypes class provides standardized type definitions that map to native Python types such as string, integer, double, bool, and date ensuring consistency across configuration, runtime parameters, and outputs.
Specialized types like html, password, and file handle more complex data securely and appropriately, for example protecting sensitive credentials or managing file paths. By correctly using DataTypes, developers ensure that workflows interpret and process data reliably and predictably.
| DataTypes method | Python type |
|---|---|
DataTypes.string() | str |
DataTypes.integer() | int |
DataTypes.double() | float |
DataTypes.date() | datetime.date |
DataTypes.datetime() | datetime.datetime |
DataTypes.bool() | bool |
DataTypes.uint() | non-negative int |
DataTypes.long() | int |
DataTypes.ulong() | non-negative int |
DataTypes.duration() | datetime.timedelta |
DataTypes.html() | HTML content as str |
DataTypes.password() | Password/secret as str |
DataTypes.file() | File path as str |
DataTypes.array() | Array |
Restricted types
Some data types are security sensitive and restricted in e.g. how they can be used with expressions.
The HTML type
HTML can be used freely, but values are heavily sanitized, removing JavaScript, event handlers CSS etc. If you provide HTML snippets as part of the extension output, ensure to inspect the output manually to see what they look like after sanitation.
Passwords
Passwords are treated securely, kept encrypted throughout execution, and decrypted only when provided to the extension.
- Extension password parameters are provided clear-text to the extension.
- Extension password outputs are required to be clear text, and are encrypted immediately after they are returned.
When Passwords cannot be used with expressions.
Never log passwords, they are clear-text. OmniFi automatically logs parameter and outputvalues, and anonymizes passwords to values that can be tracked throughout the execution, but not recognized or decrypted to either the clear-text or encrypted password.
Files
Files can be used freely by extensions and are copied in to the execution working directory at runtime.
- Files provided to the extension as parameters are always within the execution working directory.
- Files provided as output from an extension can be anywhere on disk, and will be copied to the execution working directory.
- It is the extensions responsibility to clean up any temporary files in the
Shutdown()method.
Make sure that file parameters or outputs are not created from unchecked user input.
Accepting a file name as a string input and returning that file as an output provides a very nasty injection attack vector to download any file on the computer with the permission of the Web Backend service account.
- Preferably, hard-code any file paths and don't let the user affect the path in any way.
- If you must allow the user to select among several files, use a restricted file area and ensure the path is actually within that restricted area.
Array types
The type system supports arrays of any data type. Arrays are defined by DataTypes.array(<element-type>), e.g. DataTypes.array(DataTypes.string()) defines a string array. For example, the built-in Send Email
You can define any array rank following the same pattern, DataTypes.array(DataTypes.array(DataTypes.string())) .creates a jagged 2D array.
While the type system allows you to create arrays of any rank, the UI only supports up to 2D arrays.
Defining parameters
The methods GetConfigurationParameters() and GetParameters() each return a list of WorkflowArgument objects, consisting of the following properties:
WorkflowArgument(
Name, # Display name or identifier as as string
Type, # DataTypes.<type>() — defines input type
DefaultValue, # Default value should match the above DataType (optional)
Domain, # Restricted list of allowed values - dropdown (optional)
Mandatory, # True or False — whether parameter must be provided
Comment # Description shown in OmniFi UI
)DefaultValue
A DefaultValue defines the initial or fallback value for a parameter when no user input is provided. It must always match the parameter’s declared DataType to ensure consistency and prevent runtime errors. For example, if the DataType is DataTypes.string(), the default value should be a string (e.g., "default text"); if it’s DataTypes.integer(), the default value should be an integer (e.g., 0). Setting a proper default value helps workflows run smoothly even when optional inputs are not supplied, providing predictable and type-safe behavior throughout the execution.
WorkflowArgument("ArrayType", DataTypes.string(), "string", None, None, "Select array type")Configuration parameters
Configuration parameters are configured design-time once when a node is initially configured, and provided to the WorkflowPluginimplementation with every subsequent call. This allows an extensions to obtain configuration information before they are required to provide Parameters and Output metadata.
Configuration parameters are configured by enabling the ConfigurationParameters feature in the .pyplugin file and implementing the GetConfigurationParameters()Python method in WorkflowPlugin:
Features=ConfigurationParameters
def GetConfigurationParameters(self):
return [
WorkflowArgument("Text Value", DataTypes.string(), "string", None, None, "Value")
]Configuration parameters can be mandatory or optional and supports domains. They don't, however, support operators'.
Configuration parameters are supplied to the GetParameters() , GetOutput(), GGetPorts()and Execute() methods.
def GetParameters(self, confArgs):
array_type_str = next((c.Value for c in confArgs if c.Name == "ArrayType"), "string")
array_type = self.map_type(array_type_str)
return [
WorkflowArgument("ArrayInput", DataTypes.array(array_type), None, None, True, f"Input array of type {array_type_str}")...def Execute(self, confArgs, parameters):
self.logger.log_debug("Execute(): Enter")
array = next((p.Value for p in parameters if p.Name == "ArrayInput"), [])
index = next((p.Value for p in parameters if p.Name == "Index"), 0)
selected = array[index]
array_type_str = next((c.Value for c in confArgs if c.Name == "ArrayType"), "string")...
Runtime parameters
Extensions that require some form of user input can define parameters. The return value is a list of WorkflowArguments. Sets each time the workflow runs, means per execution. Unlike configuration parameters (which are set once during extension setup), runtime parameters can vary between executions, allowing workflows to behave differently depending on the input data or context of the run.
Example:
This method returns the runtime parameters required to download a file from a given URL and is presented to the user each time the workflow runs. It exposes four WorkflowArgument entries; each argument specifies name, data type, default value, required flag, and a short description. No domain (dropdown) is provided for these arguments, so the domain field is set to None.
Example:
def GetParameters(self, confArgs):
return [
WorkflowArgument("UrlPath", DataTypes.string(), None, None, True, "Url to download"),
WorkflowArgument("UserName", DataTypes.string(), None, None, False, "Username"),
WorkflowArgument("Password", DataTypes.password(), None, None, False, "password"),
WorkflowArgument("UseSsL", DataTypes.bool(), False, None, False, "Use SSL verification (true/false)")
]The below code snippet sets two selectable choices to the user via WorkflowDomainItem() in the dropdown.
def GetParameters(self, confArgs):
return [
WorkflowArgument(
"StringMultiSelectParam",
DataTypes.array(DataTypes.string()),
None,
[
WorkflowDomainItem(DataTypes.string(), "Option1", "Option 1"),
WorkflowDomainItem(DataTypes.string(), "Option2", "Option 2")
],
False,
"Choose one or more options"
)Parameter Domain
Domains allow parameter input to be restricted to a certain subset of discrete values.
This creates a configuration parameter named ArrayType (string) with a domain of selectable array types, a dropdown of predefined list in the OmniFi UI (String, Integer, Double, DateTime, Date, UInt, Byte, Long, ULong, Duration, Boolean, File).
The parameter is required, defaults to "string", and is set once when the extension is configured; its value is reused on every execution without needing to be specified again.
Example:
def GetConfigurationParameters(self):
domain = [
WorkflowDomainItem(DataTypes.string(), "string", "String"),
WorkflowDomainItem(DataTypes.string(), "integer", "Integer"),
WorkflowDomainItem(DataTypes.string(), "double", "Double"),
WorkflowDomainItem(DataTypes.string(), "datetime", "DateTime"),
WorkflowDomainItem(DataTypes.string(), "date", "Date"),
WorkflowDomainItem(DataTypes.string(), "uint", "UInt"),
WorkflowDomainItem(DataTypes.string(), "byte", "Byte"),
WorkflowDomainItem(DataTypes.string(), "long", "Long"),
WorkflowDomainItem(DataTypes.string(), "ulong", "ULong"),
WorkflowDomainItem(DataTypes.string(), "duration", "Duration"),
WorkflowDomainItem(DataTypes.string(), "bool", "Boolean"),
WorkflowDomainItem(DataTypes.string(), "file", "File")
]
return [
WorkflowArgument("Text Value", DataTypes.string(), "string", domain, True, "Selectable values")
]
Multi-select domains
Multi-select behavior is supported using arrays, and defining the parameter domain in the arrays element type. For example, defining a DataTypes.array(DataTypes.string())parameter with a DataTypes.string()domain allows the user to configure a list of string elements restricted by the domain.
Mandatory parameters
The fifth parameter (True in this example) specifies whether the argument is mandatory. When set to True, the parameter is displayed in bold and must be provided for the workflow to execute successfully and If set to False, the parameter is optional and the workflow can run without explicitly specifying a value.
WorkflowArgument("UrlPath", DataTypes.string(), "string", None, True, "Path to the url to download")
Parameter Operators
A parameter can be associated with a set of operators. OmniFi doesn't have support for any particular operators, instead you define a set of string options that the user can choose from, e.g. `["=", ">", "<"]. The extension is responsible for implementing the operator logic, allowing you to define any operators that make sense in your context.
This argument is used to specify operator parameters, typically as the last parameter in the WorkflowArgument(). During execution, the value selected or provided for this parameter can be accessed within the Execute method, where it is used to perform the corresponding logical or comparison operation.
Example:
WorkflowArgument(
"CharOperatorParam",
DataTypes.string(),
None,
None,
False,
"Param comment",
["=", ">", "<", "!="] # string options that the user can choose from
),def Execute(self, confArgs, parameters):
# Retrive user inputs from the parameters
for param in parameters
self.logger.log_debug(param.Value)
self.logger.log_debug(param.Operator)
output.append(WorkflowParameter(parameter.Name, parameter.Type, parameter.Value)) Array parameters
Arrays of any type: (lists of values)
DataTypes.array(DataTypes.string()) # Array of strings
DataTypes.array(DataTypes.integer()) # Array of integers
DataTypes.array(DataTypes.date()) # Array of datesDefining outputs
In OmniFi workflow extensions, outputs define what data your extension produces after the execution. In other words, the results your extension outputs.
For example, when a workflow processes some input data and produces a result, such as a summary table, a calculated number, or a status message that information is defined as an output.
This method must return a list of WorkflowOutput objects. Each WorkflowOutput describes one result produced by your extension.
Each output is a WorkflowOutput with:
- Name - A unique identifier for the output (ProcessedData)
- Type - The data type of the output (
DataTypes.string()) - Comment - A short description explaining what the output represents
Example:
def GetOutput(self, confArgs):
return [
WorkflowOutput("ResultText", DataTypes.string(), "The summary message after processing"),
WorkflowOutput("ProcessedCount", DataTypes.integer(), "Total number of records processed")
]Ports
In OmniFi workflow extensions, ports are logical “output channels” that determine how and where your extension`s results flow within a workflow. When your plugin finishes executing, it can direct the workflow to a specific port, allowing the workflow to continue differently depending on the result.
This method returns a list of Port objects. Each port represents a possible exit path from the extension.
Example : Return single (success) or multiple paths, if exists.
def GetPorts(confArgs):
return [Port("One"), Port("Two"), Port("Three")]
A dedicated Error port is available to handle workflow failures, eliminating the need to define a separate failure port.
Execution logic
This is the main execution method of your workflow extension, where all actual work happens. It is executed automatically by OmniFi each time the workflow runs.
The execution logic of a workflow extension, implemented in the Execute(confArgs, parameters) method, is where the extension performs its main function during runtime. It begins by reading configuration and runtime parameters provided by OmniFi, then carries out the defined business logic such as data processing, API calls, or computations.
Once the operation completes, the extension packages the results into a list of WorkflowParameter objects that match the outputs defined earlier. Finally, it returns a WorkflowResult that includes both the output data and the selected port name (Success), which determines how the workflow continues in OmniFi.
- Read configuration and runtime parameters.
- Perform your business logic (query a database, call an API).
- Build a list of
WorkflowParameterobjects for the output. - Return a
WorkflowResultwith:- The chosen port name.
- The list of output parameters.
Example:
def Execute(self, confArgs, parameters):
self.logger.log_debug("Execute(): Enter")
# Get the input string
text = next((p.Value for p in parameters if p.Name == "TextContent"), "")
# Create a temporary file and write the string to it
fd, path = tempfile.mkstemp(suffix=".txt", prefix="plugin_output_")
with os.fdopen(fd, 'w') as tmp:
tmp.write(text)
self.temp_file_path = path
self.logger.log_debug(f"File created at: {path}")
# Return the file path as output
output = [WorkflowParameter("FilePath", DataTypes.file(), path)]
output = []
for confArg in confArgs:
output.append(WorkflowParameter(confArg.Name, confArg.Type, confArg.Value))
for param in parameters:
output.append(WorkflowParameter(param.Name, param.Type, param.Value))
# Add computed value
output.append(WorkflowParameter("StringArrayParamNew", DataTypes.array(DataTypes.string()), ["A", "B"]))
return WorkflowResult("Default", output)Shutdown
Use Shutdown() to release resources (close DB connections, stop threads).
Example:
def Shutdown(self):
self.logger.log_debug("Shutdown(): Cleaning up resources ...")
if self.temp_file_path and os.path.exists(self.temp_file_path):
try:
os.remove(self.temp_file_path)The
Shutdown()method is called just before closing down the extension. It is important that all resources used throughout the execution is cleaned up properly, including database connections, temporary files etc.
Icons
Raster or MDI font icons can be used to enhance the visual look-and-feel of an extension.
Raster icons
A raster icon can be provided as an icon.png file within the extension folder. This will apply to that folder in the workflow toolbox, as well as any extensions within that folder when added to the design view. For best rendering, use an icon that is 46x46 pixels.
Raster icons are typically used for groups of extensions that integrate with external systems.
Font icons
The toolbox will never display the raster icon for every individual extension, since the icon is often a brand logo. Instead you can provide a font icon in the header file:
IconClass=mdi mdi-ab-testing
If there is no raster icon for the folder, the font icon is displayed also in the design view.
Icons can be selected from the Material Design icons.
Icons are frequently added and removed from the material design icons, o the icons available in the documentation may not be available in OmniFi. Especially brand-specific icons are often removed or changed, so avoid using any mdi-apple*, mdi-google* or other icons tied to a specific brand.
The IconClass field is validated and modified before it is rendered to the view. Use space-separated class names consisting of letters, digits, dash and underscore. The total length cannot be more than 256 characters.
Logging best practices
Logging best practices ensure that your extension remains transparent, debugable, and production-ready. Use structured logging such as OmniFi’s built-in OmniFiLog class to record key events consistently throughout your extensions lifecycle. Always trace method entry and exit points to make it easier to follow the execution flow during troubleshooting.
Avoid logging sensitive information (passwords or authentication tokens).
Log entries are shown in the UI.
Instead, focus on logging meaningful decisions, parameter values (when safe), and error conditions to provide clear insights into what the extension is doing and why. Keep your logs concise yet informative so they remain useful in both development and production environments without introducing noise or security risks.
- Use structured logging: OmniFiLog or your preferred logging mechanism to trace execution flow.
- Trace boundaries: Log method entry/exit points for easier debugging.
- Be mindful of sensitivity: Record parameter values excluding sensitive data like passwords.
- Log decisions and errors: Capture key decision points and error conditions.
- Keep it concise: Make logs informative without being verbose, suitable for dev and prod
Installing workflow extensions
Workflow extensions run on OmniFi Web Backend, and like other extension types they are installed in their corresponding root folder. For workflow extensions this is <plugins root>\Workflow, where the plugins root path is determined by (in order):
A) Web Backend bootstrap configuration
Web Backend uses a bootstrap configuration file defined by configuring SkySparc.Web.Backend.exe.config -> appSettings -> BootstrapLocation.
The bootstrap.config file can optionally contain the setting Bootstrap -> ExtensionDirectory. Setting this to e.g. C:\omnifi\extensions, the workflow extensions root is C:\omnifi\extensions\Workflow.
B) Working Directory
The working directory is configured in the web portal Settings -> Execution -> Working Directory.
If the web portal Working Directory is configured, this is used as the installation root path. Configuring Working Directory to C:\omnifi, the workflow extension root is C:\omnifi\Plugins\Workflow.
C) Main installation directory
By default, the main (client) installation directory is used as root. If OmniFi is installed in C:\Program Files (x86)\SkySparc\OmniFi, the workflow root directory is C:\Program Files (x86)\SkySparc\OmniFi\Plugins\Workflow.
Updated 12 days ago