Skip to content

Custom Actions

Produce Custom Action Messages

You can use Custom Actions to enable communication between two Applications on either the same cluster or across different clusters.

Note

To understand the purpose of Custom Actions or view the overall structure of how they work, check out the documentation in the overview page here.

To ensure the Custom Action being sent is handled properly, the app.yaml outputs needs to be declared:

Note

You can choose any name for the type.

This is how the Custom Action Manager chooses which Consumer Application (Executor) will receive the Custom Action object.

app.yaml Example
1
2
3
custom_actions:
  outputs:
    - type: custom-action-name

The Custom Action Object in the main.py script supports the following attributes :

Attribute Required Default Value Description
resource required N/A The KRNAsset that this Custom Action is meant for.
type required N/A The name of Custom Action.
title required N/A Title of the Custom Action
description required N/A Description details of the Custom Action
expiration_date required N/A Absolute datetime or a timedelta (from now) when the Custom Action will expire.
payload required N/A The custom information of the Custom Action that will be required by the Consumer Application
trace_id optional N/A A custom id for tracking the Custom Action status
custom_action_id optional N/A A UUID to use as the id of the Custom Action. Use it to match a response to the Custom Action, see Custom Action Responses.

Custom Action Responses

A Publisher Application can also receive a response to the Custom Action it sent.

This only happens if the Consumer (Executor) Application is programmed to send a response. The content of the response depends on how the developer of the Consumer (Executor) Application structured it. See Custom Action Responses in the Consume Custom Actions page.

There is nothing to declare in the app.yaml. A response is delivered for every Custom Action type that your Application declares under custom_actions.outputs. You receive it with @app.on_custom_action_report, when the Custom Action reaches a final state: completed, failed or cancelled.

Receive a Response Example
1
2
3
4
5
6
7
8
from kelvin.message import CustomActionReport, CustomActionState

@app.on_custom_action_report
async def on_report(report: CustomActionReport) -> None:
    if report.state == CustomActionState.COMPLETED:
        print(f"{report.action_id} completed: {report.response}")
    else:
        print(f"{report.action_id} ended as {report.state}: {report.message}")

The report has the following attributes :

Attribute Description
action_id The id of the Custom Action that the report is for.
state The final state of the Custom Action.
response The response sent by the Consumer (Executor) Application. It is None if the Consumer (Executor) Application did not send one.
message The message sent with the final state.
failure_reason Why the Custom Action did not complete.
payload The payload that the Custom Action was created with.
status_logs The history of the states of the Custom Action.
  • To know which Custom Action a report is for, set the custom_action_id when you create the Custom Action and compare it with the action_id of the report
  • Treat a response of None as no response
  • If the Consumer (Executor) Application never responds, the Custom Action expires. You still receive the report, with the failure_reason expired, so that you can retry or escalate

Custom Action Payload Schemas

A Custom Action type is owned by its Consumer (Executor) Application. That is the single Application deployed on the Platform that receives every request for that type and processes it centrally. The Publisher Application does not perform the work; it calls the Consumer (Executor) by publishing a Custom Action.

The payload is the request the Publisher sends to the Consumer (Executor). Its JSON schema defines the exact structure the Consumer (Executor) accepts, so that what a Publisher produces is always what the Consumer (Executor) can actually process.

Example

The email Consumer (Executor) Application processes every send-email action on the Platform. Its payload schema declares recipient, subject, and body. Any Publisher that calls send-email must supply exactly those fields.

Declare the schema for each type your Application outputs in the ui_schemas.custom_actions section of your app.yaml, keyed by the action type. Each key must match a type declared under custom_actions.inputs or custom_actions.outputs. The schemas are embedded into the app manifest at upload time.

app.yaml Example
1
2
3
4
5
6
7
8
9
custom_actions:
  outputs:
    - type: send-email
    - type: apply-setpoint

ui_schemas:
  custom_actions:
    send-email: "ui_schemas/send_email.json"
    apply-setpoint: "ui_schemas/apply_setpoint.json"

Each file is a JSON Schema that describes the payload of one Custom Action type:

ui_schemas/send_email.json
{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "Email Action",
    "description": "Payload schema for the email custom action.",
    "type": "object",
    "properties": {
        "recipient": {
            "type": "string",
            "title": "Recipient",
            "description": "Email address the action is sent to."
        },
        "subject": {
            "type": "string",
            "title": "Subject",
            "description": "Subject line of the email."
        },
        "body": {
            "type": "string",
            "title": "Body",
            "description": "Body content of the email."
        }
    },
    "required": ["recipient", "subject", "body"],
    "additionalProperties": false
}

The schema also gives the Kelvin UI a typed interface for displaying and validating the payload field when operators interact with the Custom Action.

Kelvin also enforces the schema. When a Custom Action is created, the payload is validated against the schema of its type, and the API returns 400 Bad Request if it does not match. At the edge, the Gateway validates the payload in the same way.

Note

Custom Action payload schemas are optional. If not provided, the payload field is treated as free-form JSON and any payload is accepted.

Note

If the Custom Action type already exists on the Platform, the schema in the Application you upload must match the existing schema. Otherwise the upload is rejected with 409 Conflict.

Note

The payload your Publisher Application sends must match the schema of the Consumer (Executor) Application that handles that action type.

See UI Schemas in the app.yaml reference for full details on the schema format.

Example

In this example we will create a Producer Application that will;

  1. Package the email details into a Custom Action Object
  2. Send the Custom Action object directly to the Consumer Application (Executor) for processing.
  3. Package the Custom Action object in a Recommendation and publish the "Recommendation with Custom Action" to the Kelvin UI for approval. (Typically, you would choose either direct sending or publishing with a Recommendation—not both.)

Check out the Consume Custom Actions documentation here to see how to receive this Custom Action in a Consumer Application (Executor).

app.yaml

app.yaml Example
spec_version: 5.0.0
type: app            # Any app type can handle and/or publish custom actions.

name: hello-app
title: Hello App
description: Lorem ipsum dolor sit amet, consectetur adipiscing elit
version: 1.0.0

custom_actions:
  outputs:
    - type: email

  ...

Publisher Application

main.py Example
import asyncio
from datetime import timedelta, datetime

from kelvin.application import KelvinApp
from kelvin.krn import KRNAsset
from kelvin.message import Recommendation, CustomAction

app = KelvinApp()

@app.timer(interval=10)
async def publish_data():

    asset = KRNAsset("air-conditioner-1")

    # Direct Custom Action
    action = CustomAction(resource=asset,
    type="email",
    title="Recommendation to reduce speed",
    description="It is recommended that the speed is reduced",
    expiration_date=datetime.now().astimezone() + timedelta(hours=1),
    payload={
        "recipient": "operations@example.com",
        "subject": "Recommendation to reduce speed",
        "body": "This is the email body",
    })
    await app.publish(action)


    # Or embedded the Custom Action into a Recommendation   
    rec = Recommendation(resource=asset,
    type="Reduce speed",
    actions=[action],
    )
    await app.publish(rec)

app.run()