EventListeners
An EventListener is a Kubernetes object that listens for events at a specified port on your Kubernetes cluster.
It exposes an addressable sink that receives incoming event and specifies one or more Triggers.
The sink is a Kubernetes service running the sink logic inside a dedicated Pod.
Each Trigger, in turn, allows you to specify one or more TriggerBindings that allow
you to extract fields and their values from event payloads, and one or more TriggerTemplates
that receive field values from the corresponding TriggerBindings and allow Tekton Triggers to instantiate resources
such as TaskRuns and PipelineRuns with that data.
If you need to modify, filter, or validate the event payload data before passing it to a TriggerBinding, you can optionally specify one
or more Interceptors.
- Structure of an
EventListener - Specifying the Kubernetes service account
- Specifying
Triggers - Specifying
TriggerGroups - Specifying
Resources - Specifying
Interceptors - Specifying
cloudEventURI - Constraining
EventListenersto specific namespaces - Constraining
EventListenersto specific labels - Disabling Payload Validation
- Labels in
EventListeners - Specifying
EventListenertimeouts - Annotations in
EventListeners - Understanding
EventListenerresponse - TLS HTTPS support in
EventListeners - Obtaining the status of deployed
EventListeners - Configuring logging for
EventListeners - Exposing an
EventListeneroutside of the cluster - Understanding the deployment of an
EventListener - Deploying
EventListenersin multi-tenant scenarios - CloudEvents during Trigger Processing
Structure of an EventListener
An EventListener definition consists of the following fields:
- Required:
apiVersion- specifies the target API version, for exampletriggers.tekton.dev/v1alpha1kind- specifies that this Kubernetes resource is anEventListenerobjectmetadata- specifies data that uniquely identifies thisEventListenerobject, for example anamespec- specifies the configuration of yourEventListener:serviceAccountName- Specifies theServiceAccounttheEventListenerwill use to instantiate Tekton resources
- Optional:
triggers- specifies a list ofTriggersto execute upon event detectioncloudEventURI- specifies the URI for cloudevent sinkresources- specifies the resources that will be available to the event listening servicenamespaceSelector- specifies the namespace for theEventListener; this is where theEventListenerlooks for the specifiedTriggersand stores the Tekton objects it instantiates upon event detectionlabelSelector- specifies the labels for which yourEventListenerrecognizesTriggersand instantiates the specified Tekton objects
See our Tekton Triggers examples for ready-to-use example EventListener definitions.
Specifying the Kubernetes service account
You must specify a Kubernetes service account in the serviceAccountName field that the EventListener will use to instantiate Tekton objects.
Tekton Trigger creates 2 clusterroles while installing with necessary permissions required for an eventlistener. You can directly create bindings for your serviceaccount with the clusterroles.
-
A Kubernetes RoleBinding with
tekton-triggers-eventlistener-rolesclusterrole. -
A Kubernetes ClusterRoleBinding with
tekton-triggers-eventlistener-clusterrolesclusterrole.You can checkout an example here.
-
If you’re using
namespaceSelectorsin yourEventListener, you will have to create an additionalClusterRoleBindingwithtekton-triggers-eventlistener-rolesclusterrole.
Specifying Triggers
You can optionally specify one or more Triggers that define the actions to take when the EventListener detects a qualifying event. You can specify either a reference to an
external Trigger object or reference/define the TriggerBindings, TriggerTemplates, and Interceptors in the Trigger definition. A Trigger definition
specifies the following fields:
name- (optional) a valid Kubernetes name that uniquely identifies theTriggerinterceptors- (optional) a list ofInterceptorsthat will process event payload data before passing it to the associatedTriggerBindingbindings- (optional) a list ofTriggerBindingsfor thisTrigger; you can either reference existingTriggerBindingsor embed their definitions directlytemplate- (optional) aTriggerTemplatefor thisTrigger; you can either reference an existingTriggerTemplateor embed its definition directlytriggerRef- (optional) a reference to an externalTrigger
Below is an example Trigger definition that references the desired TriggerBindings, TriggerTemplates, and Interceptors:
triggers:
- name: trigger-1
interceptors:
- github:
eventTypes: ["pull_request"]
bindings:
- ref: pipeline-binding # Reference to a TriggerBinding object
- name: message # Embedded Binding
value: Hello from the Triggers EventListener!
template:
ref: pipeline-template
Below is an example Trigger definition that specifies a reference to an external Trigger object:
triggers:
- triggerRef: trigger
Below is an example Trigger definition that embeds a triggerTemplate definition directly:
triggers:
- name: "my-trigger"
template:
spec:
params:
- name: "my-param-name"
resourceTemplates:
- apiVersion: "tekton.dev/v1beta1"
kind: TaskRun
metadata:
generateName: "pr-run-"
spec:
taskSpec:
steps:
- image: ubuntu
script: echo "hello there"
Below is an example Trigger definition tailored to a multi-tenant scenario in which you may not
want all of your Trigger objects to have the same permissions as the EventListener. In such case,
you can specify a different service account at the Trigger level. This service account overrides
the service account specified in the EventListener.
triggers:
- name: trigger-1
serviceAccountName: trigger-1-sa
interceptors:
- github:
eventTypes: ["pull_request"]
bindings:
- ref: pipeline-binding
- ref: message-binding
template:
ref: pipeline-template
You must update the Role assigned to the service account specified in the EventListener as shown below
to allow it to impersonate the service account specified in the Trigger:
rules:
- apiGroups: [""]
resources: ["serviceaccounts"]
verbs: ["impersonate"]
Specifying cloudEventURI
Specifying the URI for cloud event sink which receives cloud events during Trigger Processing.
spec:
cloudEventURI: http://eventlistener.free.beeceptor.com
Specifying TriggerGroups
TriggerGroups is a feature that allows you to specify a set of interceptors that will process before a set of
Trigger resources are processed by the eventlistener. The goal of this feature is described in
TEP-0053. TriggerGroups allow for
a common set of interceptors to be defined inline in the EventListenerSpec before Triggers are invoked.
A TriggerGroup definition specifies the following fields:
name- (optional) a valid Kubernetes name that uniquely identifies theTriggerGroupinterceptors- a list ofInterceptorsthat will process event payload data before passing it to the downstreamTriggerstriggerSelector- a combination of a KuberneteslabelSelectorand anamespaceSelectoras defined later in this document. These two fields work together to define theTriggersthat will be processed onceInterceptorsprocessing completes.
Below is an example EventListener that defines an inline triggerGroup:
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
name: eventlistener
spec:
triggerGroups:
- name: github-pr-group
interceptors:
- name: "validate GitHub payload and filter on eventType"
ref:
name: "github"
params:
- name: "secretRef"
value:
secretName: github-secret
secretKey: secretToken
- name: "eventTypes"
value: ["pull_request"]
triggerSelector:
labelSelector:
matchLabels:
type: github-pr
This configuration would first process any event that is sent to the EventListener and determine if it matches
the outlined conditions. If it passes these conditions, it will use the triggerSelector matching criteria to determine
the target Trigger resources to continue processing.
Any extensions fields added during triggerGroup processing are passed to the downstream Trigger execution. This allows
for shared data across all Triggers that are processed after group execution completes. As an example, extensions.myfield would
be available to all Trigger resources matched by this group:
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
name: eventlistener
spec:
triggerGroups:
- name: cel-filter-group
interceptors:
- name: "validate body and add field"
ref:
name: "cel"
params:
- name: "filter"
value: "body.action in ['opened', 'reopened']"
- name: "overlays"
value:
- key: myfield
expression: "body.pull_request.head.sha.truncate(7)"
triggerSelector:
namespaceSelector:
matchNames:
- foo
labelSelector:
matchLabels:
type: cel-preprocessed
At this time, each TriggerGroup determines its own downstream Triggers, so if two separate groups select the same
downstream Trigger resources, it may be executed multiple times. If you use this feature, ensure that Trigger resources
are labeled to be queried by the appropriate set of TriggerGroups.
Specifying Resources
You can optionally customize the sink deployment for your EventListener using the resources field. It accepts the following types of objects:
- Kubernetes Resource using the
kubernetesResourcefield - Custom Resource objects via the
CustomResourcefield
Legal values for the PodSpec sub-fields for both kubernetesResource and CustomResource are:
ServiceAccountName
NodeSelector
Tolerations
Containers
Affinity
TopologySpreadConstraints
SecurityContext
Legal values for the Containers sub-field for kubernetesResource and CustomResource are:
kubernetesResource:
Resources
Env
LivenessProbe
ReadinessProbe
StartupProbe
SecurityContext
CustomResource:
Resources
Env
SecurityContext
Specifying a kubernetesResource object
Below is an example resources: field definition specifying a kubernetesResource object:
spec:
resources:
kubernetesResource:
serviceType: NodePort
servicePort: 80
spec:
template:
metadata:
labels:
key: "value"
annotations:
key: "value"
spec:
serviceAccountName: tekton-triggers-github-sa
securityContext:
runAsNonRoot: true
nodeSelector:
app: test
tolerations:
- key: key
value: value
operator: Equal
effect: NoSchedule
containers:
- securityContext:
readOnlyRootFilesystem: true
Specifying Service configuration
The type and port for the Service created for the EventListener can be configured via the ServiceType and ServicePort
specifications respectively. By default, the Service type is set to ClusterIP and port is set to 8080.
spec:
resources:
kubernetesResource:
serviceType: LoadBalancer
servicePort: 8128
If you use a loadbalancer service, you can optionally define a LoadBalancerClass with the ServiceLoadBalancerClass attribute.
spec:
resources:
kubernetesResource:
serviceType: LoadBalancer
serviceLoadBalancerClass: internal
Specifying Replicas
You can optionally use the replicas field to instruct Tekton Triggers to deploy more than one instance of your EventListener in individual Kubernetes Pods.
If you do not specify this value, the default number of instances (and thus, the number of respective Pods) per EventListener is 1. If you set a value for the replicas field
while creating or upgrading the EventListener's YAML file, that value overrides any value you set manually later as well as a value set by any other deployment
mechanism, such as HPA.
Specifying a CustomResource object
You can specify a Kubernetes Custom Resource object using the CustomResource field. This field has one sub-field, runtime.RawExtension that allows you to specify dynamic objects.
Contract for the CustomResource object
The CustomResource object must abide by the contract shown below.
Contract-mandated CRD structure for the spec field:
spec:
template:
metadata:
spec:
Contract-mandated CRD structure for the status field:
type EventListenerStatus struct {
duckv1beta1.Status `json:",inline"`
// EventListener is addressable via the DNS address of the sink.
duckv1alpha1.AddressStatus `json:",inline"`
}
Note: The CRD must follow the WithPod{} spec.
Below is an example resources: field definition specifying a CustomResource object using a Knative Service:
Note: This example assumes that Knative is installed on your cluster.
spec:
resources:
customResource:
apiVersion: serving.knative.dev/v1
kind: Service
# metadata:
# name: knativeservice # name is optional; if not specified, Triggers substitutes the EventListener's name with an "el-" prefix, for example: el-github-knative-listener
spec:
template:
spec:
serviceAccountName: tekton-triggers-example-sa
containers:
- resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
Specifying Interceptors
An Interceptor is a “catch-all” event processor for a specific platform that runs before the TriggerBinding. It allows you to perform payload filtering,
verification (using a secret), transformation, define and test trigger conditions, and implement other useful processing. Once the event data passes through
an Interceptor, it then goes to the Trigger before you pass the payload data to the TriggerBinding. You can also use an Interceptor to modify the
behavior of the associated Trigger.
For more information, see Interceptors.
Constraining EventListeners to specific namespaces
You can optionally specify a list of namespaces in which your EventListener will search for Triggers and instantiate the specified Tekton objects using the namespaceSelector field.
If you omit this field, your EventListener will only recognize Triggers specified in its definition or found under one or more specified target labels.
Below is an example namespaceSelector field that configures the EventListener to use the foo and bar namespaces:
namespaceSelector:
matchNames:
- foo
- bar
If you want your EventListener to recognize Triggers across your entire cluster, use a wildcard between quote as the only namespace:
namespaceSelector:
matchNames:
- "*"
At present, if an EventListeners has Triggers inside its own spec as well as namespace-selector, Triggers in spec as well as in selected namespaces will be processed for a request. Triggers inside EventListener spec when using namespace-selector mode is deprecated and ability to specify both will be removed.
Constraining EventListeners to specific labels
You can optionally specify the labels for which your EventListener recognizes Triggers and instantiates the specified Tekton objects using the labelSelector field.
This field uses the standard Kubernetes labelSelector mechanism and supports the matchExpressions sub-field. If you omit the labelSelector field, the EventListener
accepts all resource labels.
Below is an example labelSelector field definition that constrains your EventListener to only recognize Triggers within its own namespace that are labeled foo=bar:
labelSelector:
matchLabels:
foo: bar
Below is an example labelSelector field definition that uses the matchExpression sub-field to specify expressions that allow the EventListener to recognize Triggers
across all namespaces in the cluster:
namespaceSelector:
matchNames:
- *
labelSelector:
matchExpressions:
- {key: environment, operator: In, values: [dev,stage]}
- {key: trigger-phase, operator: NotIn, values: [testing]}
Specifying EventListener timeouts
An EventListener times out if it cannot process an event request within a timeout specified in controller.yaml. The timeouts are as follows:
-el-readtimeout: Read timeout; default is 5 seconds.-el-writetimeout: Write timeout; default is 40 seconds.-el-idletimeout: Idle timeout; default is 120 seconds.-el-timeouthandler: Server route handler timeout; default is 30 seconds.
Disabling Payload Validation
To disable incoming payload validation for an EventListener, you can define an annotation tekton.dev/payload-validation: false
on EventListener.
apiVersion: triggers.tekton.dev/v1alpha1
kind: EventListener
metadata:
name: eventlistener
annotations:
tekton.dev/payload-validation: "false"
By default, payload validation is enabled and will be disabled only if the annotation is defined. Removing the annotation will enable the payload validation.
Labels in EventListeners
By default, each EventListener automatically attaches the following labels to all resources it instantiates:
| Name | Description |
|---|---|
| triggers.tekton.dev/eventlistener | Name of the EventListener that instantiated the resource. |
| triggers.tekton.dev/trigger | Name of the Trigger that instantiated the resource. |
| triggers.tekton.dev/eventid | UID of the incoming event. |
Note: Because they’re used as labels, EventListener and Trigger names must conform to the Kubernetes syntax and character set requirements.
Annotations in EventListeners
Tekton Triggers propagates all annotations that you include in your EventListener to the Kubernetes service and deployment created by that EventListener.
Keep in mind that annotations propagated from the EventListener override annotations already present in its Kubernetes service and deployment.
Below is an example load balancer protocol annotation in an EventListener definition that automatically propagates to the EventListener's service:
apiVersion: triggers.tekton.dev/v1alpha1
kind: EventListener
metadata:
name: eventlistener
annotations:
service.beta.kubernetes.io/aws-load-balancer-backend-protocol: https
Understanding EventListener response
An EventListener responds with a 202 ACCEPTED HTTP response when the EventListener
has been able to process the request and selected the appropriate triggers to process
based off the EventListener configuration.
After detecting an event, the EventListener responds with the following message:
{
"eventListener": "listener",
"namespace": "default",
"eventListenerUID": "ea71a6e4-9531-43a1-94fe-6136515d938c",
"eventID": "14a657c3-6816-45bf-b214-4afdaefc4ebd"
}
eventListenerUID- UID of the target EventListener.eventID- UID assigned to this event request
Response to CloudEvents
EventListener can acts as sink for CloudEvents. When it acts as such, then its response is different from above.
EventListener sends out this cloudevent as response assuming an EventListener with name listener in the default namespace with uuid ea71a6e4-9531-43a1-94fe-6136515d938c:
type: dev.tekton.event.triggers.accepted.v1
source: listener
subject: default.listener accepted eventID
id: 14a657c3-6816-45bf-b214-4afdaefc4ebd
id is assigned eventID.
It also has following data:
{
"eventListener": "listener",
"namespace": "default",
"eventListenerUID": "ea71a6e4-9531-43a1-94fe-6136515d938c",
"eventID": "14a657c3-6816-45bf-b214-4afdaefc4ebd"
}
eventID- UID assigned to this event request
Deprecated Fields
These fields are included in EventListener responses, but will be removed in a future release.
eventListener- name of the target EventListener. UseeventListenerUIDinstead.namespace- namespace of the target EventListener. UseeventListenerUIDinstead.
TLS HTTPS support in EventListeners
Tekton Triggers supports both HTTP and TLS-based HTTPS connections. To configure your EventListener for TLS,
add the TLS_CERT and TLS_KEY reserved environment variables using the secretKeyRef variable type, then
specify a secret containing the cert and key files. See TEP-0027
and our TLS configuration example for more information.
Obtaining the status of deployed EventListeners
Use the following command to get a list of EventListeners deployed on your cluster along with their statuses:
kubectl get el
You will get a response similar to the following:
NAME ADDRESS AVAILABLE REASON READY REASON
tls-listener-interceptor http://el-tls-listener-interceptor.default.svc.cluster.local True MinimumReplicasAvailable
Where for each returned line, the column values are, from left to right:
NAME- name of theEventListenerADDRESS- IP address or URL of theEventListenerAVAILABLE- readiness state of the associatedDeploymentandServiceREASON- reason for the value displayed in theAVAILABLEcolumnREADY- readiness state of the Kubernetes Custom Resource object specified in theEventListenerREASON- reason for the value displayed in theREADYcolumn
Note: The status messaging described above is being refactored. For more information, see Issue 932.
Configuring logging for EventListeners
You can configure logging for your EventListeners using the config-logging-triggers
ConfigMap located in the tekton-pipelines namespace (config-logging.yaml).
Tekton Triggers automatically reconciles this configmap into environment variables on your
event listener deployment.
To access your EventListener logs, query your cluster for Pods whose eventlistener label matches the name of your EventListener object. For example:
kubectl get pods --selector eventlistener=my-eventlistener
Configuring metrics for EventListeners
The following pipeline metrics are available on the eventlistener Service on port 9000.
| Name | Type | Labels/Tags | Status |
|---|---|---|---|
eventlistener_triggered_resources |
Counter | kind=<kind> |
experimental |
eventlistener_event_received_count |
Counter | status=<status> |
experimental |
eventlistener_http_duration_seconds_[bucket, sum, count] |
Histogram | - | experimental |
Several kinds of exporters can be configured for an EventListener, including Prometheus, Google Stackdriver, and many others.
You can configure metrics using the config-observability-triggers config map in the EventListener namespaces.
There is a config-observability-triggers configmap in the tekton-pipelines namespace that can be configured for the operation of the Triggers
webhook and controller components.
See the Knative documentation for more information about available exporters and configuration values.
Exposing an EventListener outside of the cluster
EventListeners create an underlying Kubernetes service (unless a user specifies a customResource EventListener deployment).
By convention, this service is the same name as the EventListener prefixed with el. So, an EventListener named foo
will create a service called el-foo.
This service, by default is of type ClusterIP which means it is only accessible within the cluster on which it is running.
You can expose this service as you would with any regular Kubernetes service. A few ways are highlighted below:
- Using a
LoadBalancerService type - Using a Kubernetes
Ingressobject - Using the NGINX Ingress Controller
- Using OpenShift Route
Exposing an EventListener using a LoadBalancer Service
If your Kubernetes cluster supports external load balancers,
you can set the serviceType field to LoadBalancer to switch the Kubernetes service type:
spec:
resources:
kubernetesResource:
serviceType: LoadBalancer
Note: You can find the external IP of this service by running kubectl get svc/el-${EVENTLISTENER-NAME} -o=jsonpath='{.status.loadBalancer.ingress[0].ip}'
Exposing an EventListener using a Kubernetes Ingress object
You can expose the service created by the EventListener using a regular Kubernetes Ingress.
To do this, you may first have to change the serviceType to NodePort:
spec:
resources:
kubernetesResource:
serviceType: NodePort
You can also use the Tekton create-ingress task to configure an Ingress object using self-signed certificates.
Exposing an EventListener using Openshift Route
Below are instructions for configuring an OpenShift 4.2 cluster running API version v1.14.6+32dc4a0. For more information,
see Route Configuration.
-
Obtain the name of your
EventListenerservice:oc get el <EVENTLISTENR_NAME> -o=jsonpath='{.status.configuration.generatedName}' -
Expose the
EventListenerservice:oc expose svc/[el-listener] # REPLACE el-listener WITH YOUR SERVICE NAME FROM STEP 1 -
Obtain the IP address of the exposed route:
oc get route el-listener -o=jsonpath='{.spec.host}' # REPLACE el-listener WITH YOUR SERVICE NAME FROM STEP 1 -
Test the configuration with
curlor set up a GitHub Webhook that sends events to it.
Understanding the deployment of an EventListener
Below is a high-level walkthrough through the deployment of an EventListener using a GitHub example provided by Tekton Triggers.
-
Instantiate the example
EventListeneron your cluster:kubectl create -f https://github.com/tektoncd/triggers/tree/main/examples/v1beta1/githubTekton Triggers creates a new
DeploymentandServicefor theEventListener. using theEventListenerdefinition,metadata.labels, and pre-existing values such as containerImage,Name, andPort. Tekton Triggers uses theEventListenername prefixed withel-to name theDeploymentandServicewhen instantiating them. For example, if theEventListenername isfoo, theDeploymentandServicenames are namedel-foo. -
Use
kubectlto verify theDeploymentis running on your cluster:kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE el-github-listener-interceptor 1/1 1 1 11s -
Use
kubectlto verify theServiceis running on your cluster:kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE el-github-listener-interceptor ClusterIP 10.99.188.140 <none> 8080/TCP 52s -
Obtain the URL on which the
EventListeneris listening for events:kubectl get eventlistener NAME ADDRESS AVAILABLE REASON github-listener-interceptor http://el-github-listener-interceptor.ptest.svc.cluster.local:8080 True MinimumReplicasAvailableSee our GitHub
EventListenerexample to try instantiating anEventListenerlocally.
Deploying EventListeners in multi-tenant scenarios
EventListeners are effectively Tekton clients that use HTTP to bypass the normal Kubernetes authentication
mechanism established through kubeconfig files and the kubectl config command tree. Because of this,
you must be conscious of your configuration decisions, such as:
- How to securely expose each
EventListenerto the outside of your cluster, - How to securely control how each
EventListenerand its associated objects, such asInterceptors, interact with data on your cluster.
At the minimum, each EventListener specifies its own Kubernetes Service account as explained earlier, and
it acts on all events it receives with the permissions of that service account. If your business needs mandate
more granular permission control across the Triggers and Interceptors specified in your EventListeners,
you have the following options:
- Deploy each
EventListenerin its own namespace - Deploy multiple
EventListenersin the same namespace - Specify a separate service account for each
Trigger
Deploying each EventListener in its own namespace
In this scenario, you create multiple EventListeners that in turn use a variety of Triggers and Interceptors,
each EventListener gets its own namespace. This way, you can use a different service account for each namespace
and tailor the permissions of those accounts to the functionality of their corresponding EventListeners. Because
creating a namespace often instantiates the necessary service accounts based on pre-configured permissions, this
also simplifies the deployment process as you simply need to update the permissions associated with those accounts.
However, this approach has the following drawbacks:
- Namespaces with separately associated
SecretsandServiceAccountscan be the most expensive items in the Kubernetesetcdstore; on large clusters, the capacity of theetcdstore can become a concern. - Since each
EventListenerrequires its own HTTP port to listen for events, you must configure your network to allow access to each corresponding IP address and port combination unless you configure an ingress abstraction layer, such as the KubernetesIngressobject,or OpenShift Route.
Deploying multiple EventListeners in the same namespace
In this scenario, you create multiple EventListeners in the same namespace. This will require customization of
the associated service account(s), secret(s), and RBAC, since the automatically generated defaults are not always
ideal, but you will not incur a significant etcd store cost as in the multiple namespace scenario. Network security
and configuration overhead concerns, however, still apply as described earlier. You can also achieve a similar result
by specifying a separate service account for each Trigger used across your EventListener pool at the cost of
increased administration overhead.
Cloud Events during Trigger Processing
The cloud event that is sent to a target URI during Trigger processing. The types of events send for now are:
| Type | Description |
|---|---|
| dev.tekton.event.triggers.started.v1 | triggers processing started in eventlistener |
| dev.tekton.event.triggers.successful.v1 | triggers processing successful and a resource created |
| dev.tekton.event.triggers.failed.v1 | triggers failed in eventlistener |
| dev.tekton.event.triggers.done.v1 | triggers processing done in eventlistener handle |
Feedback
Was this page helpful?