diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index 8dfb6fdc5..f55d77d47 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -59,6 +59,8 @@ struct ovn_extend_table;
     OVNACT(NEXT,              ovnact_next)            \
     OVNACT(LOAD,              ovnact_load)            \
     OVNACT(MOVE,              ovnact_move)            \
+    OVNACT(PUSH,              ovnact_push_pop)        \
+    OVNACT(POP,               ovnact_push_pop)        \
     OVNACT(EXCHANGE,          ovnact_move)            \
     OVNACT(DEC_TTL,           ovnact_null)            \
     OVNACT(CT_NEXT,           ovnact_ct_next)         \
@@ -234,6 +236,12 @@ struct ovnact_move {
     struct expr_field rhs;
 };
 
+/* OVNACT_PUSH, OVNACT_POP. */
+struct ovnact_push_pop {
+    struct ovnact ovnact;
+    struct expr_field field;
+};
+
 /* OVNACT_CT_NEXT. */
 struct ovnact_ct_next {
     struct ovnact ovnact;
diff --git a/lib/actions.c b/lib/actions.c
index 1c328f88d..7fe80f458 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -573,6 +573,75 @@ ovnact_move_free(struct ovnact_move *move OVS_UNUSED)
 {
 }
 
+
+static void
+parse_push_pop(struct action_context *ctx, bool is_push)
+{
+    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
+
+    struct expr_field f;
+    if (!expr_field_parse(ctx->lexer, ctx->pp->symtab, &f, &ctx->prereqs)) {
+        return;
+    }
+    size_t ofs = ctx->ovnacts->size;
+    char *error = expr_type_check(&f, f.n_bits, !is_push, ctx->scope);
+    if (error) {
+        ctx->ovnacts->size = ofs;
+        lexer_error(ctx->lexer, "%s", error);
+        free(error);
+        return;
+    }
+
+    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
+
+    struct ovnact_push_pop *p;
+    if (is_push) {
+        p = ovnact_put_PUSH(ctx->ovnacts);
+    } else {
+        p = ovnact_put_POP(ctx->ovnacts);
+    }
+    p->field = f;
+}
+
+static void
+format_PUSH(const struct ovnact_push_pop *push, struct ds *s)
+{
+    ds_put_cstr(s, "push(");
+    expr_field_format(&push->field, s);
+    ds_put_cstr(s, ");");
+}
+
+static void
+encode_PUSH(const struct ovnact_push_pop *push,
+            const struct ovnact_encode_params *ep OVS_UNUSED,
+            struct ofpbuf *ofpacts)
+{
+    ofpact_put_STACK_PUSH(ofpacts)->subfield =
+        expr_resolve_field(&push->field);
+}
+
+static void
+format_POP(const struct ovnact_push_pop *pop, struct ds *s)
+{
+    ds_put_cstr(s, "pop(");
+    expr_field_format(&pop->field, s);
+    ds_put_cstr(s, ");");
+}
+
+static void
+encode_POP(const struct ovnact_push_pop *pop,
+           const struct ovnact_encode_params *ep OVS_UNUSED,
+           struct ofpbuf *ofpacts)
+{
+    ofpact_put_STACK_POP(ofpacts)->subfield =
+        expr_resolve_field(&pop->field);
+}
+
+static void
+ovnact_push_pop_free(struct ovnact_push_pop *push OVS_UNUSED)
+{
+}
+
 static void
 parse_DEC_TTL(struct action_context *ctx)
 {
@@ -4237,6 +4306,10 @@ parse_action(struct action_context *ctx)
         parse_set_action(ctx);
     } else if (lexer_match_id(ctx->lexer, "next")) {
         parse_NEXT(ctx);
+    } else if (lexer_match_id(ctx->lexer, "push")) {
+        parse_push_pop(ctx, true);
+    } else if (lexer_match_id(ctx->lexer, "pop")) {
+        parse_push_pop(ctx, false);
     } else if (lexer_match_id(ctx->lexer, "output")) {
         ovnact_put_OUTPUT(ctx->ovnacts);
     } else if (lexer_match_id(ctx->lexer, "ip.ttl")) {
diff --git a/ovn-sb.xml b/ovn-sb.xml
index aa9ee0ff5..65bfc9a59 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -1271,6 +1271,21 @@
           </p>
         </dd>
 
+        <dt><code>push(<var>field</var>);</code></dt>
+        <dd>
+          <p>
+            Push the value of <var>field</var> to the stack top.
+          </p>
+        </dd>
+
+        <dt><code>pop(<var>field</var>);</code></dt>
+        <dd>
+          <p>
+            Pop the stack top and store the value to <var>field</var>,
+            which must be modifiable.
+          </p>
+        </dd>
+
         <dt><code>ip.ttl--;</code></dt>
         <dd>
           <p>
diff --git a/tests/ovn.at b/tests/ovn.at
index 6306719a5..e9badf477 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1982,6 +1982,17 @@ reg1[1] = lookup_fdb(outport, ip4.src);
 reg1[1] = lookup_fdb(ip4.src, eth.src);
     Cannot use numeric field ip4.src where string field is required.
 
+# push/pop
+push(xxreg0);push(xxreg1[10..20]);push(eth.src);pop(xxreg0[0..47]);pop(xxreg0[48..57]);pop(xxreg1);
+    formats as push(xxreg0); push(xxreg1[10..20]); push(eth.src); pop(xxreg0[0..47]); pop(xxreg0[48..57]); pop(xxreg1);
+    encodes as push:NXM_NX_XXREG0[],push:NXM_NX_XXREG1[10..20],push:NXM_OF_ETH_SRC[],pop:NXM_NX_XXREG0[0..47],pop:NXM_NX_XXREG0[48..57],pop:NXM_NX_XXREG1[]
+
+pop(eth.type);
+    Field eth.type is not modifiable.
+
+push(abc);
+    Syntax error at `abc' expecting field name.
+
 # Miscellaneous negative tests.
 ;
     Syntax error at `;'.
diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
index d6ff75886..4b652828d 100644
--- a/utilities/ovn-trace.c
+++ b/utilities/ovn-trace.c
@@ -1523,6 +1523,59 @@ execute_exchange(const struct ovnact_move *move, struct flow *uflow,
     mf_subfield_swap(&a, &b, uflow, NULL);
 }
 
+static void
+execute_push(const struct ovnact_push_pop *p, struct ofpbuf *stack,
+             struct flow *uflow OVS_UNUSED, struct ovs_list *super)
+{
+    struct mf_subfield sf = expr_resolve_field(&p->field);
+    union mf_subvalue sv;
+    mf_read_subfield(&sf, uflow, &sv);
+
+    struct ds s = DS_EMPTY_INITIALIZER;
+    ds_put_cstr(&s, "push(");
+    expr_field_format(&p->field, &s);
+    ds_put_cstr(&s, ") -> ");
+    mf_format_subvalue(&sv, &s);
+
+    ovntrace_node_append(super, OVNTRACE_NODE_MODIFY, "%s", ds_cstr(&s));
+    ds_destroy(&s);
+
+    uint8_t bytes = DIV_ROUND_UP(sf.n_bits, 8);
+    nx_stack_push(stack, &sv.u8[sizeof sv - bytes], bytes);
+}
+
+static void
+execute_pop(const struct ovnact_push_pop *p, struct ofpbuf *stack,
+            struct flow *uflow OVS_UNUSED, struct ovs_list *super)
+{
+    struct mf_subfield sf = expr_resolve_field(&p->field);
+    struct ds s = DS_EMPTY_INITIALIZER;
+    ds_put_cstr(&s, "pop(");
+    expr_field_format(&p->field, &s);
+    ds_put_cstr(&s, ") <- ");
+
+    uint8_t src_bytes;
+    const void *src = nx_stack_pop(stack, &src_bytes);
+    if (src) {
+        union mf_subvalue sv;
+        uint8_t dst_bytes = DIV_ROUND_UP(sf.n_bits, 8);
+
+        if (src_bytes < dst_bytes) {
+            memset(&sv.u8[sizeof sv - dst_bytes], 0,
+                   dst_bytes - src_bytes);
+        }
+        memcpy(&sv.u8[sizeof sv - src_bytes], src, src_bytes);
+        mf_write_subfield_flow(&sf, &sv, uflow);
+        mf_format_subvalue(&sv, &s);
+    } else {
+        ds_put_cstr(&s, "/* empty stack */");
+    }
+
+    ovntrace_node_append(super, OVNTRACE_NODE_MODIFY, "%s", ds_cstr(&s));
+
+    ds_destroy(&s);
+}
+
 static void
 trace__(const struct ovntrace_datapath *dp, struct flow *uflow,
         uint8_t table_id, enum ovnact_pipeline pipeline,
@@ -2558,6 +2611,8 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
         return;
     }
 
+    struct ofpbuf stack;
+    ofpbuf_init(&stack, 0);
     struct ds s = DS_EMPTY_INITIALIZER;
     const struct ovnact *a;
     OVNACT_FOR_EACH (a, ovnacts, ovnacts_len) {
@@ -2588,6 +2643,14 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
             execute_exchange(ovnact_get_EXCHANGE(a), uflow, super);
             break;
 
+        case OVNACT_PUSH:
+            execute_push(ovnact_get_PUSH(a), &stack, uflow, super);
+            break;
+
+        case OVNACT_POP:
+            execute_pop(ovnact_get_POP(a), &stack, uflow, super);
+            break;
+
         case OVNACT_DEC_TTL:
             if (is_ip_any(uflow)) {
                 if (uflow->nw_ttl) {
@@ -2829,6 +2892,7 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
             break;
         }
     }
+    ofpbuf_uninit(&stack);
     ds_destroy(&s);
 }
 
