Tutorial: OCEL Graph¶
This tutorial provides a general example for developing an Ocelescope plugin from scratch.
In this tutorial, we will build an OCEL Graph inspired by the OCELGraph feature of the OCPQ tool.
An OCEL graph visualizes how objects and events are related to each other. The plugin lets you choose an object or event ID as the starting point (the root), and then builds a spanning tree from that root based on the connected relationships in the ocel. You can also set how far the graph should expand from the starting point.
Try out OCELGraph
You can explore the source code in the repository below, or download the plugin and try it out yourself.
Requirements
This project requires Python 3.13 to be installed on your system. For easy and reproducible package management, we recommend using uv.
Step 1: Setup¶
To get started, use the plugin template. Clone it like this:
Now install the dependencies:
If you do not want to use uv, you can use any other Python package manager. For example, with pip you can run:
After that, your project should look similar to this:
The template is a minimal example. Most of your work will happen in plugin.py.
Step 2: Writing the Plugin¶
Now we start writing the actual plugin.
Writing Plugin Metadata¶
An Ocelescope plugin is defined by a plugin class. This class inherits from Plugin and contains your plugin methods.
Open src/plugin-template/plugin.py, find the existing plugin class, and update the class name and metadata to something like this:
| src/plugin-template/plugin.py | |
|---|---|
- The class name (
OcelGraphDiscovery) is the unique name of your plugin and helps distinguish it from other plugins. - The label is what will be shown in the UI.
- The description briefly explains what your plugin does.
- The version lets you update your plugin over time.
Adding a Plugin Method¶
Now let’s add the function that will generate the OCEL Graph.
Add a new method to your plugin class called mine_ocel_graph. You mark plugin methods with @plugin_method. The label and description you set there will be shown in the UI.
| src/plugin-template/plugin.py | |
|---|---|
Planning a Plugin Method¶
Before you implement the method, it helps to plan what it should take as input and what it should return.
For our OCEL Graph plugin, we need the following inputs:
ocel: the OCEL to analyzeroot_id: the root of the graph (this can be an object ID or an event ID from the log)max_depth: how far the graph should expand from the rootmax_neighbours: how many neighbours to include per node (so the graph does not become too large)
As output, the method should return the OCEL graph.
In Ocelescope, plugin methods can only return either an OCEL or a Resource.
So we will implement the OCEL Graph as a custom Resource and return that.
Adding an OCEL Input¶
Since we want to build an OCEL Graph, our method needs an OCEL as input.
You can add it by adding an ocel: OCEL parameter to mine_ocel_graph:
| src/plugin-template/plugin.py | |
|---|---|
To make this input nicer in the UI, you can add a label and description with OCELAnnotation.
For that, wrap the type using Annotated[...]:
Now users will see a friendly label and description when selecting the OCEL input in the UI.
Adding a Configuration Input¶
Now we add the remaining settings (root_id, max_depth, max_neighbours).
For that, we create a configuration input class.
Create a new file called input.py next to plugin.py. In it, create a class called OCELGraphInput that inherits from PluginInput:
| src/plugin-template/input.py | |
|---|---|
Now extend OCELGraphInput with two numeric settings:
- the maximum depth of the OCEL graph
- the maximum number of neighbours per node
Use Pydantic’s Field to set titles, descriptions, defaults, and constraints:
Next, we need a way for users to select the root of the graph.
The root can be either:
- an object ID, or
- an event ID
To let users select the root, we define two small input models:
ObjectRootfor selecting an object by its IDEventRootfor selecting an event by its ID
Both models use OCEL_FIELD.
This links the field to the selected OCEL log, so the UI can offer autocomplete and validation.
Important
The ocel_id in OCEL_FIELD must match the name of your OCEL parameter in the plugin method.
In our case the parameter is named ocel, so we use ocel_id="ocel".
Now combine both options in OCELGraphInput using a union type (ObjectRoot | EventRoot):
This will create an input form like this:

Adding the Configuration Input to the plugin method¶
Now we can use OCELGraphInput as the configuration input for our plugin method.
Import the input class and add it as a parameter named input:
Important
Each plugin method can have exactly one PluginInput parameter. It must be named input.
Defining a Resource¶
Now that we have the inputs, we can define the output.
Our plugin should return an OCEL Graph. In Ocelescope, custom outputs are implemented as resources.
A resource is a Python class that inherits from Resource.
Create a new file called resource.py next to plugin.py and define the resource like this:
Important
Any nested types you use inside a resource (like EventNode, ObjectNode, or Relation) should inherit from Pydantic’s BaseModel.
This makes sure the resource can be validated and serialized correctly.
You can also add helper properties or methods to your resource. For example:
| src/plugin-template/resource.py | |
|---|---|
Finally, make sure your plugin method returns your resource by adding it as the return type:
| src/plugin-template/plugin.py | |
|---|---|
Visualization¶
At this point, OCELGraph can already be returned as a resource.
But it is only a data structure. By default, it has no visualization in the frontend.
To visualize the resource in Ocelescope, add a visualize() method.
This method returns one of Ocelescope’s visualization objects (for example, Graph).
Add this to your OCELGraph class:
Now your resource will show up as an interactive graph in the frontend:

Implementing the Plugin Method¶
Now you can implement the plugin method that transforms your input (the OCEL and configuration) and returns your resource.
For this tutorial, we won’t go into the implementation details.
Instead, we will put the logic in a utility function to keep plugin.py clean and readable.
Download util.py and add it next to plugin.py:
Now import the function and return its result:
Warning
Currently, Ocelescope plugins only support relative imports.
This means all imports inside your plugin must use relative paths:
Step 3: Build Plugin¶
Before you build the plugin, make sure your package exposes the plugin class at the top level.
Open src/plugin-template/__init__.py and export your plugin class:
| src/plugin-template/__init__.py | |
|---|---|
Ocelescope plugins are distributed as a zipped Python package. After building, your zip should look like this:
You can create the zip manually, but it is easier to use the build command.
Run this from the project root:
Or, if you are using uv:
The build script also checks for absolute imports and raises an error if it finds any.
That’s it. You can now upload the zip from dist/ to an Ocelescope instance and run your plugin.