Recommendations
Recommendation Messages
Recommendations in Kelvin are a method to package one or more actions into a single Recommendation, which is then displayed in the Kelvin UI for Operations to review and approve.

The recommendation allows Operations to see and decide on the actions before they are implemented.
Note
In short this puts the Operations as the man-in-the-middle between the automation programs and the Assets.

In this brief demo you can see the different parts of a Recommendation;
The technical workflow of Recommendations from creation to approval/rejection can be found in the detailed concept overview page here.
Note
Currently the only action available is Control Changes.
The Recommendation Object supports the following attributes :
| Attribute |
Required |
Description |
resource |
required |
The KRNAsset that this Recommendation is meant for. |
type |
required |
The Recommendation type (String). (e.g. speed_increase, speed_decrease, etc) |
expiration_date |
optional |
Absolute datetime or a time delta (from now) when the Control Change will expire. |
description |
optional |
Detailed description for the Recommendation. |
confidence |
optional |
Confidence of the recommendation (from 1 to 4). |
control_changes |
required |
List of ControlChange Objects associated with the recommendation. |
metadata |
optional |
Metadata for the recommendation. |
auto_accepted |
optional |
Sets the Recommendation as auto accepted (Default is False). |
Open / Closed Loop Control
Kelvin supports both open-loop and closed-loop control approaches in its application logic, providing flexibility in how automation decisions are executed.
Info
This is programming advice to optionally include a human-in-the-loop by using App Parameters and conditional logic to decide whether to create a recommendation or execute actions directly.
This concept enables you to insert human oversight between your algorithms or ML models and the final actions applied to your assets.
To implement this, define a boolean App Parameter called closed_loop:
- If
true, actions are executed automatically without human intervention.
- If
false, a recommendation is created instead, allowing operators to review and decide whether to proceed.
There are two implementation strategies:
- (Recommended) Always package the control change or action inside a recommendation object. Use the
auto_accept field to control behavior:
true for automatic execution
-
false for manual review
This ensures consistent history logging and easier auditing or debugging during future upgrades.
-
Use conditional logic (if/else) in your app:
- If
closed_loop is true, apply the action directly.
- If false, wrap it in a recommendation with
auto_accept: false.
This approach gives full control over automation level per deployment while retaining traceability and operational flexibility.
Evidences
When creating Recommendations, it is useful to embed the data used in the calculations for creating the new value in the recommendation.
This data is intended for;
- Operations : They can view the data when deciding whether to accept or reject the recommendation.
- Data Scientists : They can use the data to correlate with the confidence level reported by Operations to improve their machine learning models.
Info
Evidence can only be embedded into Recommendation if used in a Kelvin SmartApp™.
You can not create Recommendations with evidences using the Kelvin API or the Kelvin API Client (Python)
There are a number of different types of evidences that can be embedded with the Recommendation;
Bar Chart
This evidence is for creating bar charts.
This is how you can add this evidence into your Recommendation.
| Add Bar Chart Evidence to Recommendation Python Example |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 | from kelvin.application import KelvinApp
from kelvin.message.evidences import BarChart
from kelvin.krn import KRNAsset
from datetime import datetime
async def main() -> None:
app = KelvinApp()
await app.connect()
evidences = [
BarChart(
title="Sample Bar Chart Title",
timestamp=datetime.now(),
x_axis={
"title": "X-Axis Title",
"categories": ["Category A", "Category B", "Category C", "Category D"]
},
y_axis={
"title": "Y-Axis Title",
"min": 0
},
series=[
{"name": "Series 1", "data": [5, 10, 15, 20]},
{"name": "Series 2", "data": [7, 14, 21, 28]}
]
)
]
recommendation = Recommendation(
resource=KRNAsset('pcp_51'),
type="decrease_speed",
control_changes=[],
evidences=evidences,
)
await app.publish(recommendation)
if __name__ == "__main__":
asyncio.run(main())
|
Line Chart
This evidence example is for creating line charts.
There are four types of line charts you can create;
- Linear
- Date Time
- Category
- Logarithmic
This is how you can add this evidence into your Recommendation.
| Add Line Chart Evidence to Recommendation Python Example |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | from kelvin.application import KelvinApp
from kelvin.message.evidences import LineChart
from kelvin.krn import KRNAsset
from datetime import datetime
async def main() -> None:
app = KelvinApp()
await app.connect()
evidences = [
LineChart(
title="Sample Chart Title",
timestamp=datetime.now(),
x_axis={
"type":"linear", # | 'linear' | 'datetime' | 'category' | 'logarithmic';
"categories": ["Category 1", "Category 2", "Category 3"],
"title": "X-Axis Title"
},
y_axis={"title": "Y-Axis Title"},
series=[
{"name": "Series 1", "data": [1, 2, 3, 4, 5]},
{"name": "Series 2", "data": [[1, 2], [2, 3], [3, 5], [4, 7]]}
]
)
]
recommendation = Recommendation(
resource=KRNAsset('pcp_51'),
type="decrease_speed",
control_changes=[],
evidences=evidences,
)
await app.publish(recommendation)
if __name__ == "__main__":
asyncio.run(main())
|
Dynacard
This evidence example is for creating line charts.
There are four types of line charts you can create;
- Linear
- Date Time
- Category
- Logarithmic
This is how you can add this evidence into your Recommendation.
| Add Dynacard Evidence to Recommendation Python Example |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | from kelvin.application import KelvinApp
from kelvin.message.evidences import Dynacard
from kelvin.krn import KRNAsset
from datetime import datetime
async def main() -> None:
app = KelvinApp()
await app.connect()
evidences = [
Dynacard(
title="Sample Chart Title",
timestamp=datetime.now(),
xAxis={
"title": "X-Axis Title"
},
yAxis={
"title": "Y-Axis Title"
},
series=[
{"name": "Series 1", "data": [1, 2, 3, 4, 5]},
{"name": "Series 2", "data": [[1, 2], [2, 3], [3, 5], [4, 7]]}
]
)
]
recommendation = Recommendation(
resource=KRNAsset('pcp_51'),
type="decrease_speed",
control_changes=[],
evidences=evidences,
)
await app.publish(recommendation)
if __name__ == "__main__":
asyncio.run(main())
|
HighCharts
This evidence is for creating any type of chart using the powerful High Charts format.
This is how you can add this evidence into your Recommendation.
| Add HighCharts Evidence to Recommendation Python Example |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | from kelvin.application import KelvinApp
from kelvin.message.evidences import Chart
from kelvin.krn import KRNAsset
from datetime import datetime
async def main() -> None:
app = KelvinApp()
await app.connect()
evidences = [
Chart(
title="Sample Chart Title",
timestamp=datetime.now(),
... # Content here will depend on the type of High Chart you choose to display.
)
]
recommendation = Recommendation(
resource=KRNAsset('pcp_51'),
type="decrease_speed",
control_changes=[],
evidences=evidences,
)
await app.publish(recommendation)
if __name__ == "__main__":
asyncio.run(main())
|
Image
This evidence is for showing images. This is particular useful for computer vision related Recommendations.
Note
The image must be either from the Kelvin File Storage or a publicly available link.
This is how you can add this evidence into your Recommendation.
| Add Picture (Image) Evidence to Recommendation Python Example |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | from kelvin.application import KelvinApp
from kelvin.message.evidences import Image
from kelvin.krn import KRNAsset
from datetime import datetime
async def main() -> None:
app = KelvinApp()
await app.connect()
evidences = [
Image(
title="My Image",
description="This is the image or evidence description.",
url="https://www.example.com/image.jpg",
)
]
recommendation = Recommendation(
resource=KRNAsset('pcp_51'),
type="decrease_speed",
control_changes=[],
evidences=evidences,
)
await app.publish(recommendation)
if __name__ == "__main__":
asyncio.run(main())
|
Markdown
This evidence is for showing text in markdown format.
Note
The image must be either from the Kelvin File Storage or a publicly available link.
This is how you can add this evidence into your Recommendation.
| Add Markdown Text Evidence to Recommendation Python Example |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | from kelvin.application import KelvinApp
from kelvin.message.evidences import Markdown
from kelvin.krn import KRNAsset
from datetime import datetime
async def main() -> None:
app = KelvinApp()
await app.connect()
evidences = [
Markdown(
title="My Markdown",
markdown="""
# Evidence 1
Lorem Ipsum is simply dummy text of the printing and typesetting industry.
# Evidence 2
...
""" # (Multi line) String
)
]
recommendation = Recommendation(
resource=KRNAsset('pcp_51'),
type="decrease_speed",
control_changes=[],
evidences=evidences,
)
await app.publish(recommendation)
if __name__ == "__main__":
asyncio.run(main())
|
IFrame
This evidence is for showing any web content in an iFrame.
This is how you can add this evidence into your Recommendation.
| Embed IFrame Evidence to Recommendation Python Example |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | from kelvin.application import KelvinApp
from kelvin.message.evidences import IFrame
from kelvin.krn import KRNAsset
from datetime import datetime
async def main() -> None:
app = KelvinApp()
await app.connect()
evidences = [
IFrame(
title="My IFrame",
url="https://www.example.com/content/",
)
]
recommendation = Recommendation(
resource=KRNAsset('pcp_51'),
type="decrease_speed",
control_changes=[],
evidences=evidences,
)
await app.publish(recommendation)
if __name__ == "__main__":
asyncio.run(main())
|
Examples
Basic Usage
Here is a minimal Recommendation.
| Create Basic Recommendation Python Example |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | from datetime import timedelta
from kelvin.application import KelvinApp
from kelvin.message import ControlChange, Recommendation
from kelvin.krn import KRNAssetDataStream, KRNAsset
(...)
# Create a Control Change
control_change = ControlChange(
resource=KRNAssetDataStream("my-motor-asset", "motor_speed_set_point"),
payload=1000,
expiration_date=timedelta(minutes=5)
)
# Create and Publish a Recommendation with one Control Change package
await app.publish(
Recommendation(
resource=KRNAsset("my-motor-asset"),
type="decrease_speed",
control_changes=[control_change]
)
)
|
Multiple Control Changes
| Create Recommendation with Multiple Control Changes Python Example |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | from datetime import timedelta
from kelvin.application import KelvinApp
from kelvin.message import ControlChange, Recommendation
from kelvin.krn import KRNAssetDataStream, KRNAsset
(...)
# Create a Control Change
control_change_01 = ControlChange(
resource=KRNAssetDataStream("my-motor-asset_01", "motor_speed_set_point"),
payload=1000,
expiration_date=timedelta(minutes=5)
)
control_change_02 = ControlChange(
resource=KRNAssetDataStream("my-motor-asset_02", "motor_speed_set_point"),
payload=1000,
expiration_date=timedelta(minutes=5)
)
control_change_03 = ControlChange(
resource=KRNAssetDataStream("valve_01", "position_set_point"),
payload=1000,
expiration_date=timedelta(minutes=5)
)
# Create and Publish a Recommendation with one Control Change package
await app.publish(
Recommendation(
resource=KRNAsset("my-motor-asset"),
type="decrease_speed",
control_changes=[control_change_01, control_change_02, control_change_03]
)
)
|
Typical ML Usage
When you have a machine learning model producing the recommended control changes, then you can store additional data produced by the ML output
| Create Recommendation with Metadata Python Example |
|---|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | from datetime import timedelta
from kelvin.application import KelvinApp
from kelvin.message import ControlChange, Recommendation
from kelvin.krn import KRNAssetDataStream, KRNAsset
# Normally your ML mode predictions go here
(...)
# Create a Control Change
control_change = ControlChange(
resource=KRNAssetDataStream("my-motor-asset", "motor_speed_set_point"),
payload=1000,
expiration_date=timedelta(minutes=5)
)
# Create and Publish a Recommendation with one Control Change package
# Add also the ML-specific Data
await app.publish(
Recommendation(
resource=KRNAsset("my-motor-asset"),
type="decrease_speed",
control_changes=[control_change],
metadata={
"predicted_speed": 2.5,
"confidence": 0.87,
"input_features": {"current_speed": 5, "load": 4},
"timestamp": "2024-11-18T12:00:00Z",
"model_version": "1.2.0"
}
)
)
|
Incorporating Evidences
There are a number of types of evidences you can embed into the Recommendation.
Note
Each Recommendation can have an unlimited amount of evidence added.
- Bar Charts
- Line Charts
- Dynacards
- Any type of HighCharts
- Images
- Markdown
- IFrames
You can see full examples here.