diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc
index 849e36c9a5058feef115feb8af2e803f5b96e791..b1f7100d344efe47c5402986e1c442ff5b3fea80 100644
--- a/src/metabase/lib/core.cljc
+++ b/src/metabase/lib/core.cljc
@@ -145,5 +145,8 @@
    native-query
    query
    saved-question-query]
+  [lib.stage
+   append-stage
+   drop-stage]
   [lib.temporal-bucket
    temporal-bucket])
diff --git a/src/metabase/lib/order_by.cljc b/src/metabase/lib/order_by.cljc
index 6d11f6f8425438e028d002c31030e2d8766cbded..a8860c94eafb6ed2ca12aed54526bdf3f76199cf 100644
--- a/src/metabase/lib/order_by.cljc
+++ b/src/metabase/lib/order_by.cljc
@@ -87,7 +87,7 @@
      (lib.util/update-query-stage query stage-number update :order-by (fn [order-bys]
                                                                         (conj (vec order-bys) new-order-by))))))
 
-(mu/defn order-bys :- [:sequential ::lib.schema.order-by/order-by]
+(mu/defn order-bys :- [:maybe [:sequential ::lib.schema.order-by/order-by]]
   "Get the order-by clauses in a query."
   ([query :- ::lib.schema/query]
    (order-bys query -1))
diff --git a/src/metabase/lib/stage.cljc b/src/metabase/lib/stage.cljc
index 74eafaebfbd74034f65993c01786df3ebb90f5c3..f8c6a59099e86aa2f6592a2b09db72f8a38090a5 100644
--- a/src/metabase/lib/stage.cljc
+++ b/src/metabase/lib/stage.cljc
@@ -8,6 +8,7 @@
    [metabase.lib.expression :as lib.expression]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
+   [metabase.lib.options :as lib.options]
    [metabase.lib.schema :as lib.schema]
    [metabase.lib.schema.common :as lib.schema.common]
    [metabase.lib.schema.id :as lib.schema.id]
@@ -248,3 +249,15 @@
      (lib.expression/expressions query stage-number)
      columns
      (implicitly-joinable-columns query columns))))
+
+(mu/defn append-stage :- ::lib.schema/query
+  "Adds a new blank stage to the end of the pipeline"
+  [query]
+  (update query :stages conj (lib.options/ensure-uuid {:lib/type :mbql.stage/mbql})))
+
+(mu/defn drop-stage :- ::lib.schema/query
+  "Drops the final stage in the pipeline"
+  [query]
+  (when (= 1 (count (:stages query)))
+    (throw (ex-info (i18n/tru "Cannot drop the only stage") {:stages (:stages query)})))
+  (update query :stages (comp vec butlast)))
diff --git a/test/metabase/lib/stage_test.cljc b/test/metabase/lib/stage_test.cljc
index ce32a2cb54619ead9662ebaca5322be5b32b1cc6..9a7885ccfc381bd8af8947591e72482ea3e7bdcc 100644
--- a/test/metabase/lib/stage_test.cljc
+++ b/test/metabase/lib/stage_test.cljc
@@ -64,3 +64,17 @@
                                :source-table "card__1"}]}]
     (is (= "My Card"
            (lib.metadata.calculation/display-name query -1 query)))))
+
+(deftest ^:parallel adding-and-removing-stages
+  (let [query (lib/query-for-table-name meta/metadata-provider "VENUES")
+        query-with-new-stage (-> query
+                                 lib/append-stage
+                                 (lib/order-by 1 (lib/field "VENUES" "NAME") :asc))]
+    (is (= 0 (count (lib/order-bys query-with-new-stage 0))))
+    (is (= 1 (count (lib/order-bys query-with-new-stage 1))))
+    (is (= query
+           (-> query-with-new-stage
+               (lib/filter (lib/= 1 (lib/field "VENUES" "NAME")))
+               (lib/drop-stage))))
+    (testing "Dropping with 1 stage should error"
+      (is (thrown-with-msg? #?(:cljs :default :clj Exception) #"Cannot drop the only stage" (-> query (lib/drop-stage)))))))