diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index f42bbc277..5ed70e798 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -83,7 +83,8 @@ struct ovn_extend_table;
     OVNACT(ND_NS,             ovnact_nest)            \
     OVNACT(SET_METER,         ovnact_set_meter)       \
     OVNACT(OVNFIELD_LOAD,     ovnact_load)            \
-    OVNACT(CHECK_PKT_LARGER,  ovnact_check_pkt_larger)
+    OVNACT(CHECK_PKT_LARGER,  ovnact_check_pkt_larger) \
+    OVNACT(TRIGGER_EVENT,     ovnact_controller_event)
 
 /* enum ovnact_type, with a member OVNACT_<ENUM> for each action. */
 enum OVS_PACKED_ENUM ovnact_type {
@@ -318,6 +319,14 @@ struct ovnact_check_pkt_larger {
     struct expr_field dst;      /* 1-bit destination field. */
 };
 
+/* OVNACT_EVENT. */
+struct ovnact_controller_event {
+    struct ovnact ovnact;
+    int event_type;   /* controller event type */
+    struct ovnact_gen_option *options;
+    size_t n_options;
+};
+
 /* Internal use by the helpers below. */
 void ovnact_init(struct ovnact *, enum ovnact_type, size_t len);
 void *ovnact_put(struct ofpbuf *, enum ovnact_type, size_t len);
@@ -486,6 +495,9 @@ enum action_opcode {
      * The actions, in OpenFlow 1.3 format, follow the action_header.
      */
     ACTION_OPCODE_ICMP4_ERROR,
+
+    /* "trigger_event (event_type)" */
+    ACTION_OPCODE_EVENT,
 };
 
 /* Header. */
@@ -515,6 +527,10 @@ struct ovnact_parse_params {
     /* hmap of 'struct gen_opts_map' to support 'put_nd_ra_opts' action */
     const struct hmap *nd_ra_opts;
 
+    /* Array of hmap of 'struct gen_opts_map' to support 'trigger_event'
+     * action */
+    const struct controller_event_options *controller_event_opts;
+
     /* Each OVN flow exists in a logical table within a logical pipeline.
      * These parameters express this context for a set of OVN actions being
      * parsed:
diff --git a/ovn/controller/lflow.c b/ovn/controller/lflow.c
index feb8f8ff7..1aafafb33 100644
--- a/ovn/controller/lflow.c
+++ b/ovn/controller/lflow.c
@@ -70,6 +70,7 @@ static bool consider_logical_flow(
     struct hmap *dhcp_opts,
     struct hmap *dhcpv6_opts,
     struct hmap *nd_ra_opts,
+    struct controller_event_options *controller_event_opts,
     const struct shash *addr_sets,
     const struct shash *port_groups,
     const struct sset *active_tunnels,
@@ -297,12 +298,16 @@ add_logical_flows(
     struct hmap nd_ra_opts = HMAP_INITIALIZER(&nd_ra_opts);
     nd_ra_opts_init(&nd_ra_opts);
 
+    struct controller_event_options controller_event_opts;
+    controller_event_opts_init(&controller_event_opts);
+
     SBREC_LOGICAL_FLOW_TABLE_FOR_EACH (lflow, logical_flow_table) {
         if (!consider_logical_flow(sbrec_multicast_group_by_name_datapath,
                                    sbrec_port_binding_by_name,
                                    lflow, local_datapaths,
                                    chassis, &dhcp_opts, &dhcpv6_opts,
-                                   &nd_ra_opts, addr_sets, port_groups,
+                                   &nd_ra_opts, &controller_event_opts,
+                                   addr_sets, port_groups,
                                    active_tunnels, local_lport_ids,
                                    flow_table, group_table, meter_table,
                                    lfrr, conj_id_ofs)) {
@@ -315,6 +320,7 @@ add_logical_flows(
     dhcp_opts_destroy(&dhcp_opts);
     dhcp_opts_destroy(&dhcpv6_opts);
     nd_ra_opts_destroy(&nd_ra_opts);
+    controller_event_opts_destroy(&controller_event_opts);
 }
 
 bool
@@ -371,6 +377,10 @@ lflow_handle_changed_flows(
             lflow_resource_destroy_lflow(lfrr, &lflow->header_.uuid);
         }
     }
+
+    struct controller_event_options controller_event_opts;
+    controller_event_opts_init(&controller_event_opts);
+
     SBREC_LOGICAL_FLOW_TABLE_FOR_EACH_TRACKED (lflow, logical_flow_table) {
         if (!sbrec_logical_flow_is_deleted(lflow)) {
             /* Now, add/modify existing flows. If the logical
@@ -389,7 +399,8 @@ lflow_handle_changed_flows(
                                        sbrec_port_binding_by_name,
                                        lflow, local_datapaths,
                                        chassis, &dhcp_opts, &dhcpv6_opts,
-                                       &nd_ra_opts, addr_sets, port_groups,
+                                       &nd_ra_opts, &controller_event_opts,
+                                       addr_sets, port_groups,
                                        active_tunnels, local_lport_ids,
                                        flow_table, group_table, meter_table,
                                        lfrr, conj_id_ofs)) {
@@ -401,6 +412,7 @@ lflow_handle_changed_flows(
     dhcp_opts_destroy(&dhcp_opts);
     dhcp_opts_destroy(&dhcpv6_opts);
     nd_ra_opts_destroy(&nd_ra_opts);
+    controller_event_opts_destroy(&controller_event_opts);
     return ret;
 }
 
@@ -466,6 +478,9 @@ lflow_handle_changed_ref(
     struct hmap nd_ra_opts = HMAP_INITIALIZER(&nd_ra_opts);
     nd_ra_opts_init(&nd_ra_opts);
 
+    struct controller_event_options controller_event_opts;
+    controller_event_opts_init(&controller_event_opts);
+
     /* Re-parse the related lflows. */
     LIST_FOR_EACH (lrln, ref_list, &rlfn->ref_lflow_head) {
         const struct sbrec_logical_flow *lflow =
@@ -483,11 +498,13 @@ lflow_handle_changed_ref(
                  UUID_ARGS(&lrln->lflow_uuid),
                  ref_type, ref_name);
         ofctrl_remove_flows(flow_table, &lrln->lflow_uuid);
+
         if (!consider_logical_flow(sbrec_multicast_group_by_name_datapath,
                                    sbrec_port_binding_by_name,
                                    lflow, local_datapaths,
                                    chassis, &dhcp_opts, &dhcpv6_opts,
-                                   &nd_ra_opts, addr_sets, port_groups,
+                                   &nd_ra_opts, &controller_event_opts,
+                                   addr_sets, port_groups,
                                    active_tunnels, local_lport_ids,
                                    flow_table, group_table, meter_table,
                                    lfrr, conj_id_ofs)) {
@@ -506,6 +523,7 @@ lflow_handle_changed_ref(
     dhcp_opts_destroy(&dhcp_opts);
     dhcp_opts_destroy(&dhcpv6_opts);
     nd_ra_opts_destroy(&nd_ra_opts);
+    controller_event_opts_destroy(&controller_event_opts);
     return ret;
 }
 
@@ -530,6 +548,7 @@ consider_logical_flow(
     struct hmap *dhcp_opts,
     struct hmap *dhcpv6_opts,
     struct hmap *nd_ra_opts,
+    struct controller_event_options *controller_event_opts,
     const struct shash *addr_sets,
     const struct shash *port_groups,
     const struct sset *active_tunnels,
@@ -574,6 +593,7 @@ consider_logical_flow(
         .dhcp_opts = dhcp_opts,
         .dhcpv6_opts = dhcpv6_opts,
         .nd_ra_opts = nd_ra_opts,
+        .controller_event_opts = controller_event_opts,
 
         .pipeline = ingress ? OVNACT_P_INGRESS : OVNACT_P_EGRESS,
         .n_tables = LOG_PIPELINE_LEN,
diff --git a/ovn/controller/pinctrl.c b/ovn/controller/pinctrl.c
index b8ed375fe..81b91cf68 100644
--- a/ovn/controller/pinctrl.c
+++ b/ovn/controller/pinctrl.c
@@ -211,6 +211,10 @@ static void pinctrl_handle_put_icmp4_frag_mtu(struct rconn *swconn,
                                               struct ofputil_packet_in *pin,
                                               struct ofpbuf *userdata,
                                               struct ofpbuf *continuation);
+static void
+pinctrl_handle_event(struct ofpbuf *userdata)
+    OVS_REQUIRES(pinctrl_mutex);
+static void wait_controller_event(struct ovsdb_idl_txn *ovnsb_idl_txn);
 static void init_ipv6_ras(void);
 static void destroy_ipv6_ras(void);
 static void ipv6_ra_wait(long long int send_ipv6_ra_time);
@@ -1897,6 +1901,12 @@ process_packet_in(struct rconn *swconn, const struct ofp_header *msg)
                                           &pin, &userdata, &continuation);
         break;
 
+    case ACTION_OPCODE_EVENT:
+        ovs_mutex_lock(&pinctrl_mutex);
+        pinctrl_handle_event(&userdata);
+        ovs_mutex_unlock(&pinctrl_mutex);
+        break;
+
     default:
         VLOG_WARN_RL(&rl, "unrecognized packet-in opcode %"PRIu32,
                      ntohl(ah->opcode));
@@ -2405,6 +2415,7 @@ void
 pinctrl_wait(struct ovsdb_idl_txn *ovnsb_idl_txn)
 {
     wait_put_mac_bindings(ovnsb_idl_txn);
+    wait_controller_event(ovnsb_idl_txn);
     int64_t new_seq = seq_read(pinctrl_main_seq);
     seq_wait(pinctrl_main_seq, new_seq);
 }
@@ -3448,3 +3459,106 @@ exit:
         dp_packet_delete(pkt_out);
     }
 }
+
+static void
+wait_controller_event(struct ovsdb_idl_txn *ovnsb_idl_txn)
+{
+    if (!ovnsb_idl_txn) {
+        return;
+    }
+
+    for (size_t i = 0; i < OVN_EVENT_MAX; i++) {
+        if (!hmap_is_empty(&event_table[i])) {
+            poll_immediate_wake();
+            break;
+        }
+    }
+}
+
+static bool
+pinctrl_handle_empty_lb_backends_opts(struct ofpbuf *userdata)
+{
+    struct controller_event_opt_header *userdata_opt;
+    uint32_t hash = 0;
+    char *vip = NULL;
+    char *protocol = NULL;
+    char *load_balancer = NULL;
+
+    while (userdata->size) {
+        userdata_opt = ofpbuf_try_pull(userdata, sizeof *userdata_opt);
+        if (!userdata_opt) {
+            return false;
+        }
+        size_t size = ntohs(userdata_opt->size);
+        char *userdata_opt_data = ofpbuf_try_pull(userdata, size);
+        if (!userdata_opt_data) {
+            return false;
+        }
+        switch (ntohs(userdata_opt->opt_code)) {
+        case EMPTY_LB_VIP:
+            vip = xmemdup0(userdata_opt_data, size);
+            break;
+        case EMPTY_LB_PROTOCOL:
+            protocol = xmemdup0(userdata_opt_data, size);
+            break;
+        case EMPTY_LB_LOAD_BALANCER:
+            load_balancer = xmemdup0(userdata_opt_data, size);
+            break;
+        default:
+            OVS_NOT_REACHED();
+        }
+        hash = hash_bytes(userdata_opt_data, size, hash);
+    }
+    if (!vip || !protocol || !load_balancer) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "missing lb parameters in userdata");
+        return false;
+    }
+
+    struct empty_lb_backends_event *event;
+
+    event = pinctrl_find_empty_lb_backends_event(vip, protocol,
+                                                 load_balancer, hash);
+    if (!event) {
+        if (hmap_count(&event_table[OVN_EVENT_EMPTY_LB_BACKENDS]) >= 1000) {
+            COVERAGE_INC(pinctrl_drop_controller_event);
+            return false;
+        }
+
+        event = xzalloc(sizeof *event);
+        hmap_insert(&event_table[OVN_EVENT_EMPTY_LB_BACKENDS],
+                    &event->hmap_node, hash);
+        event->vip = vip;
+        event->protocol = protocol;
+        event->load_balancer = load_balancer;
+        event->timestamp = time_msec();
+        notify_pinctrl_main();
+    } else {
+        free(vip);
+        free(protocol);
+        free(load_balancer);
+    }
+    return true;
+}
+
+static void
+pinctrl_handle_event(struct ofpbuf *userdata)
+    OVS_REQUIRES(pinctrl_mutex)
+{
+    ovs_be32 *pevent;
+
+    pevent = ofpbuf_try_pull(userdata, sizeof *pevent);
+    if (!pevent) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "event not present in the userdata");
+        return;
+    }
+
+    switch (ntohl(*pevent)) {
+    case OVN_EVENT_EMPTY_LB_BACKENDS:
+        pinctrl_handle_empty_lb_backends_opts(userdata);
+        break;
+    default:
+        return;
+    }
+}
diff --git a/ovn/lib/actions.c b/ovn/lib/actions.c
index d132214bf..041fd1a3f 100644
--- a/ovn/lib/actions.c
+++ b/ovn/lib/actions.c
@@ -38,6 +38,8 @@
 #include "packets.h"
 #include "openvswitch/shash.h"
 #include "simap.h"
+#include "uuid.h"
+#include "socket-util.h"
 
 VLOG_DEFINE_THIS_MODULE(actions);
 
@@ -1259,6 +1261,21 @@ format_CLONE(const struct ovnact_nest *nest, struct ds *s)
     format_nested_action(nest, "clone", s);
 }
 
+static void
+format_TRIGGER_EVENT(const struct ovnact_controller_event *event,
+                     struct ds *s)
+{
+    ds_put_format(s, "trigger_event(event = \"%s\"",
+                  event_to_string(event->event_type));
+    for (const struct ovnact_gen_option *o = event->options;
+         o < &event->options[event->n_options]; o++) {
+        ds_put_cstr(s, ", ");
+        ds_put_format(s, "%s = ", o->option->name);
+        expr_constant_set_format(&o->value, s);
+    }
+    ds_put_cstr(s, ");");
+}
+
 static void
 encode_nested_actions(const struct ovnact_nest *on,
                       const struct ovnact_encode_params *ep,
@@ -1362,6 +1379,52 @@ encode_CLONE(const struct ovnact_nest *on,
     ofpact_finish_CLONE(ofpacts, &clone);
 }
 
+static void
+encode_event_empty_lb_backends_opts(struct ofpbuf *ofpacts,
+        const struct ovnact_controller_event *event)
+{
+    for (const struct ovnact_gen_option *o = event->options;
+         o < &event->options[event->n_options]; o++) {
+        struct controller_event_opt_header *hdr =
+            ofpbuf_put_uninit(ofpacts, sizeof *hdr);
+        const union expr_constant *c = o->value.values;
+        size_t size;
+        hdr->opt_code = htons(o->option->code);
+        if (!strcmp(o->option->type, "str")) {
+            size = strlen(c->string);
+            hdr->size = htons(size);
+            ofpbuf_put(ofpacts, c->string, size);
+        } else {
+            /* All empty_lb_backends fields are of type 'str' */
+            OVS_NOT_REACHED();
+        }
+    }
+}
+
+static void
+encode_TRIGGER_EVENT(const struct ovnact_controller_event *event,
+                     const struct ovnact_encode_params *ep OVS_UNUSED,
+                     struct ofpbuf *ofpacts)
+{
+    size_t oc_offset;
+
+    oc_offset = encode_start_controller_op(ACTION_OPCODE_EVENT, false,
+                                           NX_CTLR_NO_METER, ofpacts);
+    ovs_be32 ofs = htonl(event->event_type);
+    ofpbuf_put(ofpacts, &ofs, sizeof ofs);
+
+    switch (event->event_type) {
+    case OVN_EVENT_EMPTY_LB_BACKENDS:
+        encode_event_empty_lb_backends_opts(ofpacts, event);
+        break;
+    case OVN_EVENT_MAX:
+    default:
+        OVS_NOT_REACHED();
+    }
+
+    encode_finish_controller_op(oc_offset, ofpacts);
+}
+
 static void
 ovnact_nest_free(struct ovnact_nest *on)
 {
@@ -1576,6 +1639,117 @@ free_gen_options(struct ovnact_gen_option *options, size_t n)
     free(options);
 }
 
+static void
+validate_empty_lb_backends(struct action_context *ctx,
+                           const struct ovnact_gen_option *options,
+                           size_t n_options)
+{
+    for (const struct ovnact_gen_option *o = options;
+         o < &options[n_options]; o++) {
+        const union expr_constant *c = o->value.values;
+        struct sockaddr_storage ss;
+        struct uuid uuid;
+
+        if (o->value.n_values > 1 || !c->string) {
+            lexer_error(ctx->lexer, "Invalid value for \"%s\" option",
+                        o->option->name);
+            return;
+        }
+
+        switch (o->option->code) {
+        case EMPTY_LB_VIP:
+            if (!inet_parse_active(c->string, 0, &ss, false)) {
+                lexer_error(ctx->lexer, "Invalid load balancer VIP '%s'",
+                            c->string);
+                return;
+            }
+            break;
+        case EMPTY_LB_PROTOCOL:
+            if (strcmp(c->string, "tcp") && strcmp(c->string, "udp")) {
+                lexer_error(ctx->lexer,
+                    "Load balancer protocol '%s' is not 'tcp' or 'udp'",
+                    c->string);
+                return;
+            }
+            break;
+        case EMPTY_LB_LOAD_BALANCER:
+            if (!uuid_from_string(&uuid, c->string)) {
+                lexer_error(ctx->lexer, "Load balancer '%s' is not a UUID",
+                            c->string);
+                return;
+            }
+            break;
+        }
+    }
+}
+
+static void
+parse_trigger_event(struct action_context *ctx,
+                    struct ovnact_controller_event *event)
+{
+    int event_type = 0;
+
+    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
+
+    /* Event type must be listed first */
+    if (!lexer_match_id(ctx->lexer, "event")) {
+        lexer_syntax_error(ctx->lexer, "Expecting 'event' option");
+        return;
+    }
+    if (!lexer_force_match(ctx->lexer, LEX_T_EQUALS)) {
+        return;
+    }
+
+    if (ctx->lexer->token.type != LEX_T_STRING ||
+        strlen(ctx->lexer->token.s) >= 64) {
+        lexer_syntax_error(ctx->lexer, "Expecting string");
+        return;
+    }
+
+    event_type = string_to_event(ctx->lexer->token.s);
+    if (event_type < 0 || event_type >= OVN_EVENT_MAX) {
+        lexer_syntax_error(ctx->lexer, "Unknown event '%d'", event_type);
+        return;
+    }
+
+    event->event_type = event_type;
+    lexer_get(ctx->lexer);
+
+    lexer_match(ctx->lexer, LEX_T_COMMA);
+
+    size_t allocated_options = 0;
+    while (!lexer_match(ctx->lexer, LEX_T_RPAREN)) {
+        if (event->n_options >= allocated_options) {
+            event->options = x2nrealloc(event->options, &allocated_options,
+                                     sizeof *event->options);
+        }
+
+        struct ovnact_gen_option *o = &event->options[event->n_options++];
+        memset(o, 0, sizeof *o);
+        parse_gen_opt(ctx, o,
+                      &ctx->pp->controller_event_opts->event_opts[event_type],
+                      event_to_string(event_type));
+        if (ctx->lexer->error) {
+            return;
+        }
+
+        lexer_match(ctx->lexer, LEX_T_COMMA);
+    }
+
+    switch (event_type) {
+    case OVN_EVENT_EMPTY_LB_BACKENDS:
+        validate_empty_lb_backends(ctx, event->options, event->n_options);
+        break;
+    default:
+        OVS_NOT_REACHED();
+    }
+}
+
+static void
+ovnact_controller_event_free(struct ovnact_controller_event *event OVS_UNUSED)
+{
+}
+
 static void
 parse_put_opts(struct action_context *ctx, const struct expr_field *dst,
                struct ovnact_put_opts *po, const struct hmap *gen_opts,
@@ -2514,6 +2688,8 @@ parse_action(struct action_context *ctx)
         parse_LOG(ctx);
     } else if (lexer_match_id(ctx->lexer, "set_meter")) {
         parse_set_meter_action(ctx);
+    } else if (lexer_match_id(ctx->lexer, "trigger_event")) {
+        parse_trigger_event(ctx, ovnact_put_TRIGGER_EVENT(ctx->ovnacts));
     } else {
         lexer_syntax_error(ctx->lexer, "expecting action");
     }
diff --git a/ovn/lib/ovn-l7.h b/ovn/lib/ovn-l7.h
index 362f537da..c93def450 100644
--- a/ovn/lib/ovn-l7.h
+++ b/ovn/lib/ovn-l7.h
@@ -22,6 +22,7 @@
 #include <netinet/icmp6.h>
 #include "openvswitch/hmap.h"
 #include "hash.h"
+#include "ovn/logical-fields.h"
 
 /* Generic options map which is used to store dhcpv4 opts and dhcpv6 opts. */
 struct gen_opts_map {
@@ -273,4 +274,49 @@ nd_ra_opts_init(struct hmap *nd_ra_opts)
     nd_ra_opt_add(nd_ra_opts, "mtu", ND_OPT_MTU, "uint32");
 }
 
+#define EMPTY_LB_VIP           1
+#define EMPTY_LB_PROTOCOL      2
+#define EMPTY_LB_LOAD_BALANCER 3
+
+/* Used in the OpenFlow PACKET_IN userdata */
+struct controller_event_opt_header {
+    ovs_be16 opt_code;
+    ovs_be16 size;
+};
+
+struct controller_event_options {
+    struct hmap event_opts[OVN_EVENT_MAX];
+};
+
+static inline void
+controller_event_opt_add(struct controller_event_options *event_opts,
+                         enum ovn_controller_event event_type, char *opt_name,
+                         size_t opt_code, char *opt_type)
+{
+    gen_opt_add(&event_opts->event_opts[event_type], opt_name, opt_code,
+                opt_type);
+}
+
+static inline void
+controller_event_opts_init(struct controller_event_options *opts)
+{
+    for (size_t i = 0; i < OVN_EVENT_MAX; i++) {
+        hmap_init(&opts->event_opts[i]);
+    }
+    controller_event_opt_add(opts, OVN_EVENT_EMPTY_LB_BACKENDS, "vip",
+                             EMPTY_LB_VIP, "str");
+    controller_event_opt_add(opts, OVN_EVENT_EMPTY_LB_BACKENDS, "protocol",
+                             EMPTY_LB_PROTOCOL, "str");
+    controller_event_opt_add(opts, OVN_EVENT_EMPTY_LB_BACKENDS,
+                             "load_balancer", EMPTY_LB_LOAD_BALANCER, "str");
+}
+
+static inline void
+controller_event_opts_destroy(struct controller_event_options *opts)
+{
+    for (size_t i = 0; i < OVN_EVENT_MAX; i++) {
+        gen_opts_destroy(&opts->event_opts[i]);
+    }
+}
+
 #endif /* OVN_DHCP_H */
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index 91f867ffb..51383b286 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1988,6 +1988,27 @@ tcp.flags = RST;
 
           <p><b>Prerequisite:</b> <code>tcp</code></p>
         </dd>
+
+        <dt><code>trigger_event;</code></dt>
+        <dd>
+          <p>
+            This action is used to allow ovs-vswitchd to report CMS related
+            events writing them in <ref table="Controller_Event"/> table.
+            Supported event:
+          </p>
+
+          <ul>
+            <li>
+              <p>
+                <dfn>empty_lb_backends</dfn>. This event is raised if a
+                received packet is destined for a load balancer VIP that has
+                no configured backend destinations. For this event, the event
+                info includes the load balancer VIP, the load balancer UUID,
+                and the transport protocol.
+              </p>
+            </li>
+          </ul>
+        </dd>
       </dl>
     </column>
 
diff --git a/ovn/utilities/ovn-trace.c b/ovn/utilities/ovn-trace.c
index fff432d61..91503871f 100644
--- a/ovn/utilities/ovn-trace.c
+++ b/ovn/utilities/ovn-trace.c
@@ -2135,6 +2135,9 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
             execute_ovnfield_load(ovnact_get_OVNFIELD_LOAD(a), super);
             break;
 
+        case OVNACT_TRIGGER_EVENT:
+            break;
+
         case OVNACT_CHECK_PKT_LARGER:
             break;
         }
diff --git a/tests/ovn.at b/tests/ovn.at
index 4da7059b3..e9ec715df 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1333,6 +1333,16 @@ tcp_reset { };
     encodes as controller(userdata=00.00.00.0b.00.00.00.00)
     has prereqs tcp
 
+# trigger_event
+trigger_event(event = "empty_lb_backends", vip = "10.0.0.1:80", protocol = "tcp", load_balancer = "12345678-abcd-9876-fedc-11119f8e7d6c");
+    encodes as controller(userdata=00.00.00.0f.00.00.00.00.00.00.00.00.00.01.00.0b.31.30.2e.30.2e.30.2e.31.3a.38.30.00.02.00.03.74.63.70.00.03.00.24.31.32.33.34.35.36.37.38.2d.61.62.63.64.2d.39.38.37.36.2d.66.65.64.63.2d.31.31.31.31.39.66.38.65.37.64.36.63)
+
+# Testing invalid vip results in extra error messages from socket-util.c
+trigger_event(event = "empty_lb_backends", vip = "10.0.0.1:80", protocol = "sctp", load_balancer = "12345678-abcd-9876-fedc-11119f8e7d6c");
+    Load balancer protocol 'sctp' is not 'tcp' or 'udp'
+trigger_event(event = "empty_lb_backends", vip = "10.0.0.1:80", protocol = "tcp", load_balancer = "bacon");
+    Load balancer 'bacon' is not a UUID
+
 # Contradictionary prerequisites (allowed but not useful):
 ip4.src = ip6.src[0..31];
     encodes as move:NXM_NX_IPV6_SRC[0..31]->NXM_OF_IP_SRC[]
diff --git a/tests/test-ovn.c b/tests/test-ovn.c
index 27e65266d..0b9e8246e 100644
--- a/tests/test-ovn.c
+++ b/tests/test-ovn.c
@@ -157,7 +157,8 @@ create_symtab(struct shash *symtab)
 
 static void
 create_gen_opts(struct hmap *dhcp_opts, struct hmap *dhcpv6_opts,
-                struct hmap *nd_ra_opts)
+                struct hmap *nd_ra_opts,
+                struct controller_event_options *event_opts)
 {
     hmap_init(dhcp_opts);
     dhcp_opt_add(dhcp_opts, "offerip", 0, "ipv4");
@@ -197,6 +198,9 @@ create_gen_opts(struct hmap *dhcp_opts, struct hmap *dhcpv6_opts,
     /* IPv6 ND RA options. */
     hmap_init(nd_ra_opts);
     nd_ra_opts_init(nd_ra_opts);
+
+    /* OVN controller events options. */
+    controller_event_opts_init(event_opts);
 }
 
 static void
@@ -1229,12 +1233,13 @@ test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
     struct hmap dhcp_opts;
     struct hmap dhcpv6_opts;
     struct hmap nd_ra_opts;
+    struct controller_event_options event_opts;
     struct simap ports;
     struct ds input;
     bool ok = true;
 
     create_symtab(&symtab);
-    create_gen_opts(&dhcp_opts, &dhcpv6_opts, &nd_ra_opts);
+    create_gen_opts(&dhcp_opts, &dhcpv6_opts, &nd_ra_opts, &event_opts);
 
     /* Initialize group ids. */
     struct ovn_extend_table group_table;
@@ -1264,6 +1269,7 @@ test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
             .dhcp_opts = &dhcp_opts,
             .dhcpv6_opts = &dhcpv6_opts,
             .nd_ra_opts = &nd_ra_opts,
+            .controller_event_opts = &event_opts,
             .n_tables = 24,
             .cur_ltable = 10,
         };
@@ -1351,6 +1357,7 @@ test_parse_actions(struct ovs_cmdl_context *ctx OVS_UNUSED)
     dhcp_opts_destroy(&dhcp_opts);
     dhcp_opts_destroy(&dhcpv6_opts);
     nd_ra_opts_destroy(&nd_ra_opts);
+    controller_event_opts_destroy(&event_opts);
     ovn_extend_table_destroy(&group_table);
     ovn_extend_table_destroy(&meter_table);
     exit(ok ? EXIT_SUCCESS : EXIT_FAILURE);
