diff --git a/src/metabase/lib/common.cljc b/src/metabase/lib/common.cljc
index 4877fe18ca721d740550abb2dba405e98ab9eef6..e11b85e1760d44f1b2a8cf4f6216ce1d23116d36 100644
--- a/src/metabase/lib/common.cljc
+++ b/src/metabase/lib/common.cljc
@@ -4,10 +4,20 @@
    [metabase.lib.field :as lib.field]
    #_{:clj-kondo/ignore [:unused-namespace]}
    [metabase.lib.options :as lib.options]
+   [metabase.lib.schema.common :as schema.common]
    #_{:clj-kondo/ignore [:unused-namespace]}
    [metabase.util.malli :as mu])
   #?(:cljs (:require-macros [metabase.lib.common])))
 
+(mu/defn external-op :- [:maybe ::schema.common/external-op]
+  "Convert the internal operator `clause` to the external format."
+  [[operator options :as clause]]
+  (when clause
+    {:operator (cond-> operator
+                 (keyword? operator) name)
+     :options options
+     :args (subvec clause 2)}))
+
 (defmulti ->op-arg
   "Ensures that clause arguments are properly unwrapped"
   {:arglists '([query stage-number x])}
@@ -15,15 +25,17 @@
     (lib.dispatch/dispatch-value x)))
 
 (defmethod ->op-arg :default
-  [query stage-number x]
-  (if (vector? x)
-    (mapv #(->op-arg query stage-number %) x)
-    x))
+  [_query _stage-number x]
+  x)
 
 (defmethod ->op-arg :metadata/field
   [query stage-number field-metadata]
   (lib.field/field query stage-number field-metadata))
 
+(defmethod ->op-arg :lib/external-op
+  [query stage-number {:keys [operator options args] :or {options {}}}]
+  (->op-arg query stage-number (lib.options/ensure-uuid (into [(keyword operator) options] args))))
+
 (defmethod ->op-arg :dispatch-type/fn
   [query stage-number f]
   (->op-arg query stage-number (f query stage-number)))
@@ -38,26 +50,28 @@
             (every? #(not-any? #{'query 'stage-number} %) argvecs)]}
      (let [fn-rename #(name (get {'/ 'div} % %))]
        `(do
-          (mu/defn ~(symbol (str (fn-rename op-name) "-clause")) :- ~(keyword "mbql.clause" (name op-name))
+          (mu/defn ~(symbol (str (fn-rename op-name) "-clause")) :- :metabase.lib.schema.common/external-op
             ~(format "Create a standalone clause of type `%s`." (name op-name))
             ~@(for [argvec argvecs
                     :let [arglist-expr (if (contains? (set argvec) '&)
                                          (cons `list* (remove #{'&} argvec))
                                          argvec)]]
                 `([~'query ~'stage-number ~@argvec]
-                  (-> (into [~(keyword op-name)]
-                            (map (fn [~'arg]
-                                   (->op-arg ~'query ~'stage-number ~'arg)))
-                            ~arglist-expr)
-                      lib.options/ensure-uuid))))
+                  {:operator ~(keyword op-name)
+                   :args (mapv (fn [~'arg]
+                                 (->op-arg ~'query ~'stage-number ~'arg))
+                               ~arglist-expr)})))
 
           (mu/defn ~op-name :- fn?
             ~(format "Create a closure of clause of type `%s`." (name op-name))
             ~@(for [argvec argvecs
-                    :let [arglist-expr (if (contains? (set argvec) '&)
+                    :let [varargs? (contains? (set argvec) '&)
+                          arglist-expr (if varargs?
                                          (filterv (complement #{'&}) argvec)
-                                         (conj argvec []))]]
+                                         argvec)]]
                 `([~@argvec]
                   (fn ~(symbol (str (fn-rename op-name) "-closure"))
                     [~'query ~'stage-number]
-                    (apply ~(symbol (str (fn-rename op-name) "-clause")) ~'query ~'stage-number ~@arglist-expr)))))))))
+                    ~(cond->> (concat [(symbol (str (fn-rename op-name) "-clause")) 'query 'stage-number]
+                                      arglist-expr)
+                       varargs? (cons `apply))))))))))
diff --git a/src/metabase/lib/core.cljc b/src/metabase/lib/core.cljc
index e202e9b0e9905891750d1d836139913b16d76e15..a148d11410eddd4faa69531c8466ba7d86abf778 100644
--- a/src/metabase/lib/core.cljc
+++ b/src/metabase/lib/core.cljc
@@ -118,21 +118,7 @@
    starts-with ends-with
    contains does-not-contain
    time-interval
-   segment
-   ->and
-   ->or
-   ->not
-   ->= ->!=
-   ->< -><=
-   ->> ->>=
-   ->between
-   ->inside
-   ->is-null ->not-null
-   ->is-empty ->not-empty
-   ->starts-with ->ends-with
-   ->contains ->does-not-contain
-   ->time-interval
-   ->segment]
+   segment]
   [lib.join
    join
    join-clause
diff --git a/src/metabase/lib/dispatch.cljc b/src/metabase/lib/dispatch.cljc
index 552c6fa305ff1dd7e8874b1166c7534914ac0142..9987893bd4a8fe33b1cf084ee9ec4bba3969eb63 100644
--- a/src/metabase/lib/dispatch.cljc
+++ b/src/metabase/lib/dispatch.cljc
@@ -21,5 +21,10 @@
   ;; of [[toucan2.core/model]]?
   (or (mbql-clause-type x)
       (when (map? x)
-        (:lib/type x))
+        (if (and (or (keyword? (:operator x))
+                     (string? (:operator x)))
+                 (vector? (:args x))
+                 (map? (:options x {})))
+          :lib/external-op
+          (:lib/type x)))
       (u/dispatch-type-keyword x)))
diff --git a/src/metabase/lib/expression.cljc b/src/metabase/lib/expression.cljc
index 556a2b5a10013222419549844aeeca6e5b10dd48..e173572fefeeea4beaf2c7b06d6da8bda0729adf 100644
--- a/src/metabase/lib/expression.cljc
+++ b/src/metabase/lib/expression.cljc
@@ -23,12 +23,17 @@
    stage-number    :- :int
    expression-name :- ::lib.schema.common/non-blank-string]
   (let [stage (lib.util/query-stage query stage-number)]
-    (or (get-in stage [:expressions expression-name])
+    (or (some-> (get-in stage [:expressions expression-name])
+                lib.common/external-op)
         (throw (ex-info (i18n/tru "No expression named {0}" (pr-str expression-name))
                         {:expression-name expression-name
                          :query           query
                          :stage-number    stage-number})))))
 
+(defmethod lib.schema.expression/type-of* :lib/external-op
+  [{:keys [operator options args] :or {options {}}}]
+  (lib.schema.expression/type-of* (into [(keyword operator) options] args)))
+
 (defmethod lib.metadata.calculation/metadata :expression
   [query stage-number [_expression opts expression-name, :as expression-ref]]
   (let [expression (resolve-expression query stage-number expression-name)]
diff --git a/src/metabase/lib/filter.cljc b/src/metabase/lib/filter.cljc
index 289722ab9d0679b85aa9e3d64b65eb4d27a8b05e..b2ea06bdc3ae8870f1ed326cb3704d0623c91931 100644
--- a/src/metabase/lib/filter.cljc
+++ b/src/metabase/lib/filter.cljc
@@ -1,13 +1,12 @@
 (ns metabase.lib.filter
   (:refer-clojure :exclude [filter and or not = < <= > ->> >= not-empty case])
   (:require
-   [metabase.lib.dispatch :as lib.dispatch]
-   [metabase.lib.field :as lib.field]
+   [metabase.lib.common :as lib.common]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.options :as lib.options]
    [metabase.lib.schema]
-   [metabase.lib.schema.expression :as schema.expression]
+   [metabase.lib.schema.common :as schema.common]
    [metabase.lib.temporal-bucket :as lib.temporal-bucket]
    [metabase.lib.util :as lib.util]
    [metabase.shared.util.i18n :as i18n]
@@ -167,129 +166,70 @@
             (lib.metadata.calculation/display-name query stage-number expr)
             (lib.temporal-bucket/interval->i18n n unit)))
 
-(defmulti ^:private ->filter-arg
-  {:arglists '([query stage-number x])}
-  (fn [_query _stage-number x]
-    (lib.dispatch/dispatch-value x)))
-
-(defmethod ->filter-arg :default
-  [_query _stage-number x]
-  x)
-
-(defmethod ->filter-arg :metadata/field
-  [query stage-number field-metadata]
-  (lib.field/field query stage-number field-metadata))
-
-(defmethod ->filter-arg :dispatch-type/fn
-  [query stage-number f]
-  (->filter-arg query stage-number (f query stage-number)))
-
-#?(:clj
-   (defmacro ^:private deffilter
-     [filter-name argvec]
-     {:pre [(symbol? filter-name)
-            (vector? argvec) (every? symbol? argvec)
-            (not-any? #{'query 'stage-number} argvec)]}
-     (let [filter-name-str (name filter-name)
-           vararg? (.contains ^java.util.Collection argvec '&)
-           args (remove #{'&} argvec)
-           arglist-expr (if vararg?
-                          (cons 'list* args)
-                          argvec)]
-       `(do
-          (mu/defn ~filter-name :- ~(keyword "mbql.clause" filter-name-str)
-            ~(format "Create a filter clause of type `%s`." filter-name-str)
-            [~'query ~'stage-number ~@argvec]
-            (-> (into [~(keyword filter-name)]
-                      (map (fn [~'arg]
-                             (->filter-arg ~'query ~'stage-number ~'arg)))
-                      ~arglist-expr)
-                metabase.lib.options/ensure-uuid))
-
-          (mu/defn ~(symbol (str "->" filter-name-str)) :- fn?
-            ~(format "Return function creating a filter clause of type `%s`." filter-name-str)
-            ~argvec
-            (fn [~'query ~'stage-number]
-              ~(cond->> (concat [filter-name 'query 'stage-number] args)
-                 vararg? (cons `apply))))))))
-
-(metabase.lib.filter/deffilter and [x y & more])
-(metabase.lib.filter/deffilter or [x y & more])
-(metabase.lib.filter/deffilter not [x])
-(metabase.lib.filter/deffilter = [x y & more])
-(metabase.lib.filter/deffilter != [x y & more])
-(metabase.lib.filter/deffilter < [x y])
-(metabase.lib.filter/deffilter <= [x y])
-(metabase.lib.filter/deffilter > [x y])
-(metabase.lib.filter/deffilter >= [x y])
-(metabase.lib.filter/deffilter between [x lower upper])
-(metabase.lib.filter/deffilter inside [lat lon lat-max lon-min lat-min lon-max])
-(metabase.lib.filter/deffilter is-null [x])
-(metabase.lib.filter/deffilter not-null [x])
-(metabase.lib.filter/deffilter is-empty [x])
-(metabase.lib.filter/deffilter not-empty [x])
-(metabase.lib.filter/deffilter starts-with [whole part])
-(metabase.lib.filter/deffilter ends-with [whole part])
-(metabase.lib.filter/deffilter contains [whole part])
-(metabase.lib.filter/deffilter does-not-contain [whole part])
-(metabase.lib.filter/deffilter time-interval [x amount unit])
-(metabase.lib.filter/deffilter segment [segment-id])
-
-(defmulti ^:private ->filter-clause
-  {:arglists '([query stage-number x])}
-  (fn [_query _stage-number x]
-    (lib.dispatch/dispatch-value x)))
-
-(defmethod ->filter-clause :default
-  [query stage-number x]
-  (if (vector? x)
-    (-> (mapv #(clojure.core/->> %
-                                 (->filter-arg query stage-number)
-                                 (->filter-clause query stage-number)) x)
-        lib.options/ensure-uuid)
-    x))
-
-(defmethod ->filter-clause :dispatch-type/fn
-  [query stage-number f]
-  (->filter-clause query stage-number (f query stage-number)))
+(lib.common/defop and [x y & more])
+(lib.common/defop or [x y & more])
+(lib.common/defop not [x])
+(lib.common/defop = [x y & more])
+(lib.common/defop != [x y & more])
+(lib.common/defop < [x y])
+(lib.common/defop <= [x y])
+(lib.common/defop > [x y])
+(lib.common/defop >= [x y])
+(lib.common/defop between [x lower upper])
+(lib.common/defop inside [lat lon lat-max lon-min lat-min lon-max])
+(lib.common/defop is-null [x])
+(lib.common/defop not-null [x])
+(lib.common/defop is-empty [x])
+(lib.common/defop not-empty [x])
+(lib.common/defop starts-with [whole part])
+(lib.common/defop ends-with [whole part])
+(lib.common/defop contains [whole part])
+(lib.common/defop does-not-contain [whole part])
+(lib.common/defop time-interval [x amount unit])
+(lib.common/defop segment [segment-id])
 
 (mu/defn filter :- :metabase.lib.schema/query
   "Sets `boolean-expression` as a filter on `query`."
-  ([query boolean-expression]
-   (metabase.lib.filter/filter query -1 boolean-expression))
+  ([query :- :metabase.lib.schema/query
+    boolean-expression]
+   (metabase.lib.filter/filter query nil boolean-expression))
 
-  ([query stage-number boolean-expression]
+  ([query :- :metabase.lib.schema/query
+    stage-number :- [:maybe :int]
+    boolean-expression]
    (let [stage-number (clojure.core/or stage-number -1)
-         new-filter   (->filter-clause query stage-number boolean-expression)]
+         new-filter (lib.common/->op-arg query stage-number boolean-expression)]
      (lib.util/update-query-stage query stage-number assoc :filter new-filter))))
 
 (defn- and-clause? [clause]
   (clojure.core/and (vector? clause)
                     (clojure.core/= (first clause) :and)))
 
-(mu/defn current-filter :- [:maybe ::schema.expression/boolean]
+(mu/defn current-filter :- [:maybe ::schema.common/external-op]
   "Returns the current filter in stage with `stage-number` of `query`.
   If `stage-number` is omitted, the last stage is used.
   See also [[metabase.lib.util/query-stage]]."
-  ([query :- :metabase.lib.schema/query] (current-filter query -1))
+  ([query :- :metabase.lib.schema/query] (current-filter query nil))
   ([query :- :metabase.lib.schema/query
-    stage-number :- :int]
-   (:filter (lib.util/query-stage query stage-number))))
+    stage-number :- [:maybe :int]]
+   (-> (lib.util/query-stage query (clojure.core/or stage-number -1))
+       :filter
+       lib.common/external-op)))
 
-(mu/defn current-filters :- [:sequential ::schema.expression/boolean]
+(mu/defn current-filters :- [:sequential ::schema.common/external-op]
   "Returns the current filters in stage with `stage-number` of `query`.
   If `stage-number` is omitted, the last stage is used. Logicaly, the
   filter attached to the query is the conjunction of the expressions
   in the returned list. If the returned list is empty, then there is no
   filter attached to the query.
   See also [[metabase.lib.util/query-stage]]."
-  ([query :- :metabase.lib.schema/query] (current-filters query -1))
+  ([query :- :metabase.lib.schema/query] (current-filters query nil))
   ([query :- :metabase.lib.schema/query
-    stage-number :- :int]
-   (if-let [existing-filter (:filter (lib.util/query-stage query stage-number))]
+    stage-number :- [:maybe :int]]
+   (if-let [existing-filter (:filter (lib.util/query-stage query (clojure.core/or stage-number -1)))]
      (if (and-clause? existing-filter)
-       (subvec existing-filter 2)
-       [existing-filter])
+       (mapv lib.common/external-op (subvec existing-filter 2))
+       [(lib.common/external-op existing-filter)])
      [])))
 
 (defn- conjoin [existing-filter new-filter]
@@ -304,23 +244,28 @@
   yet, builds a conjunction with the current filter otherwise."
   ([query :- :metabase.lib.schema/query
     boolean-expression]
-   (metabase.lib.filter/add-filter query -1 boolean-expression))
+   (metabase.lib.filter/add-filter query nil boolean-expression))
 
   ([query :- :metabase.lib.schema/query
-    stage-number :- :int
+    stage-number :- [:maybe :int]
     boolean-expression]
    (let [stage-number (clojure.core/or stage-number -1)
-         new-filter   (->filter-clause query stage-number boolean-expression)]
+         new-filter (lib.common/->op-arg query stage-number boolean-expression)]
      (lib.util/update-query-stage query stage-number update :filter conjoin new-filter))))
 
 (mu/defn replace-filter :- :metabase.lib.schema/query
   "Replaces the expression with `target-uuid` with `boolean-expression` the filter of `query`."
-  ([query target-uuid boolean-expression]
-   (metabase.lib.filter/replace-filter query -1 target-uuid boolean-expression))
+  ([query :- :metabase.lib.schema/query
+    target-uuid :- :string
+    boolean-expression]
+   (metabase.lib.filter/replace-filter query nil target-uuid boolean-expression))
 
-  ([query stage-number target-uuid boolean-expression]
+  ([query :- :metabase.lib.schema/query
+    stage-number :- [:maybe :int]
+    target-uuid :- :string
+    boolean-expression]
    (let [stage-number (clojure.core/or stage-number -1)
-         new-filter   (->filter-clause query stage-number boolean-expression)]
+         new-filter   (lib.common/->op-arg query stage-number boolean-expression)]
      (lib.util/update-query-stage query stage-number
                                   update :filter
                                   lib.util/replace-clause target-uuid new-filter))))
diff --git a/src/metabase/lib/join.cljc b/src/metabase/lib/join.cljc
index 2d1476a61a9c63dc3c22b15b7fd303a0f935b98f..ed067364cc8db53180f46ee68b6eca5df08418af 100644
--- a/src/metabase/lib/join.cljc
+++ b/src/metabase/lib/join.cljc
@@ -97,6 +97,9 @@
   [query stage-number f]
   (->join-clause query stage-number (f query stage-number)))
 
+;; TODO this is basically the same as lib.common/->op-args,
+;; but requiring lib.common leads to crircular dependencies:
+;; join -> common -> field -> join.
 (defmulti ^:private ->join-condition
   {:arglists '([query stage-number x])}
   (fn [_query _stage-number x]
@@ -106,6 +109,11 @@
   [_query _stage-number x]
   x)
 
+(defmethod ->join-condition :lib/external-op
+  [query stage-number {:keys [operator options args] :or {options {}}}]
+  (->join-condition query stage-number
+                    (lib.options/ensure-uuid (into [operator options] args))))
+
 (defmethod ->join-condition :dispatch-type/fn
   [query stage-number f]
   (->join-condition query stage-number (f query stage-number)))
diff --git a/src/metabase/lib/schema/common.cljc b/src/metabase/lib/schema/common.cljc
index fc041acf381c66bfd4fa502927e74ed8088408b0..6bf0afd98787dbcc94b193d88fc8568a6d799e43 100644
--- a/src/metabase/lib/schema/common.cljc
+++ b/src/metabase/lib/schema/common.cljc
@@ -31,3 +31,9 @@
   [:fn
    {:error/message "valid base type"}
    #(isa? % :type/*)])
+
+(mr/def ::external-op
+  [:map
+   [:operator [:or :string :keyword]]
+   [:options {:optional true} ::options]
+   [:args [:sequential :any]]])
diff --git a/test/metabase/lib/aggregation_test.cljc b/test/metabase/lib/aggregation_test.cljc
index 75c120b77118058ebc0efe7230515ac59ac11d62..61efb32b310dff4d89c5ac4f2ed6e34b4d19d952 100644
--- a/test/metabase/lib/aggregation_test.cljc
+++ b/test/metabase/lib/aggregation_test.cljc
@@ -3,6 +3,7 @@
    [clojure.test :refer [are deftest is testing]]
    [metabase.lib.convert :as lib.convert]
    [metabase.lib.core :as lib]
+   [metabase.lib.field :as lib.field]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.query :as lib.query]
@@ -15,8 +16,7 @@
 (defn- is-fn? [op tag args expected-args]
   (let [f (apply op args)]
     (is (fn? f))
-    (is (=? (into [tag {:lib/uuid string?}]
-                  expected-args)
+    (is (=? {:operator tag, :args expected-args}
             (f {:lib/metadata meta/metadata} -1)))))
 
 (deftest ^:parallel aggregation-test
@@ -24,9 +24,8 @@
         venues-category-id-metadata (lib.metadata/field q1 nil "VENUES" "CATEGORY_ID")
         venue-field-check [:field {:base-type :type/Integer, :lib/uuid string?} (meta/id :venues :category-id)]]
     (testing "count"
-      (testing "without query/stage-number, return a function for later resolution"
-        (is-fn? lib/count :count [] [])
-        (is-fn? lib/count :count [venues-category-id-metadata] [venue-field-check])))
+      (is-fn? lib/count :count [] [])
+      (is-fn? lib/count :count [venues-category-id-metadata] [venue-field-check]))
     (testing "single arg aggregations"
       (doseq [[op tag] [[lib/avg :avg]
                         [lib/max :max]
@@ -35,8 +34,7 @@
                         [lib/sum :sum]
                         [lib/stddev :stddev]
                         [lib/distinct :distinct]]]
-        (testing "without query/stage-number, return a function for later resolution"
-          (is-fn? op tag [venues-category-id-metadata] [venue-field-check]))))))
+        (is-fn? op tag [venues-category-id-metadata] [venue-field-check])))))
 
 (defn- aggregation-display-name [aggregation-clause]
   (lib.metadata.calculation/display-name lib.tu/venues-query -1 aggregation-clause))
@@ -173,14 +171,27 @@
               [:expression {:base-type :type/Integer, :lib/uuid (str (random-uuid))} "double-price"]])))))
 
 (deftest ^:parallel aggregate-test
-  (is (=? {:lib/type :mbql/query,
-           :database (meta/id) ,
-           :type :pipeline,
-           :stages [{:lib/type :mbql.stage/mbql,
-                     :source-table (meta/id :venues) ,
-                     :lib/options {:lib/uuid string?},
-                     :aggregation [[:sum {:lib/uuid string?}
-                                    [:field {:base-type :type/Integer, :lib/uuid string?} (meta/id :venues :category-id)]]]}]}
-          (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
-              (lib/aggregate (lib/sum (lib/field "VENUES" "CATEGORY_ID")))
-              (dissoc :lib/metadata)))))
+  (let [q (lib/query-for-table-name meta/metadata-provider "VENUES")
+        result-query
+        {:lib/type :mbql/query,
+         :database (meta/id) ,
+         :type :pipeline,
+         :stages [{:lib/type :mbql.stage/mbql,
+                   :source-table (meta/id :venues) ,
+                   :lib/options {:lib/uuid string?},
+                   :aggregation [[:sum {:lib/uuid string?}
+                                  [:field
+                                   {:base-type :type/Integer, :lib/uuid string?}
+                                   (meta/id :venues :category-id)]]]}]}]
+
+    (testing "with helper function"
+      (is (=? result-query
+              (-> q
+                  (lib/aggregate (lib/sum (lib/field "VENUES" "CATEGORY_ID")))
+                  (dissoc :lib/metadata)))))
+    (testing "with external format"
+      (is (=? result-query
+              (-> q
+                  (lib/aggregate {:operator :sum
+                                  :args [(lib.field/field q (lib.metadata/field q nil "VENUES" "CATEGORY_ID"))]})
+                  (dissoc :lib/metadata)))))))
diff --git a/test/metabase/lib/expression_test.cljc b/test/metabase/lib/expression_test.cljc
index 1f32dd4a46d9f46d6522abf9b70840bc42e6a2dc..17fb916f3c6314f194da5319324ae200563843bb 100644
--- a/test/metabase/lib/expression_test.cljc
+++ b/test/metabase/lib/expression_test.cljc
@@ -7,6 +7,7 @@
    [metabase.lib.expression :as lib.expression]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
    [metabase.lib.schema :as lib.schema]
+   [metabase.lib.schema.common :as schema.common]
    [metabase.lib.schema.expression :as schema.expression]
    [metabase.lib.test-metadata :as meta]
    [metabase.lib.test-util :as lib.tu]))
@@ -79,9 +80,11 @@
                           (lib/lower string-field) :type/Text])]
       (testing (str "expression: " (pr-str expr))
         (let [query (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
-                        (lib/expression "myexpr" expr))]
+                        (lib/expression "myexpr" expr))
+              resolved (lib.expression/resolve-expression query 0 "myexpr")]
           (is (mc/validate ::lib.schema/query query))
-          (is (= typ (schema.expression/type-of (lib.expression/resolve-expression query 0 "myexpr")))))))))
+          (is (mc/validate ::schema.common/external-op resolved))
+          (is (= typ (schema.expression/type-of resolved))))))))
 
 (deftest ^:parallel col-info-expression-ref-test
   (is (=? {:base_type    :type/Integer
diff --git a/test/metabase/lib/filter_test.cljc b/test/metabase/lib/filter_test.cljc
index 08fb8925b85af78df51fd9a86d6d325115dd4395..69d3c1e98b459d6dff881de35470bdb991a4371c 100644
--- a/test/metabase/lib/filter_test.cljc
+++ b/test/metabase/lib/filter_test.cljc
@@ -2,17 +2,14 @@
   (:require
    [clojure.test :refer [deftest is testing]]
    [metabase.lib.core :as lib]
+   [metabase.lib.field :as lib.field]
    [metabase.lib.metadata :as lib.metadata]
-   [metabase.lib.options :as lib.options]
    [metabase.lib.test-metadata :as meta]
    #?@(:cljs ([metabase.test-runner.assert-exprs.approximately-equal]))))
 
-(defn- test-clause [result-filter f ->f & args]
-  (testing "with query/stage-number, return clause right away"
-    (is (=? result-filter
-            (apply f {:lib/metadata meta/metadata} -1 args))))
-  (testing "without query/stage-number, return a function for later resolution"
-    (let [f' (apply ->f args)]
+(defn- test-clause [result-filter f & args]
+  (testing "return a function for later resolution"
+    (let [f' (apply f args)]
       (is (fn? f'))
       (is (=? result-filter
               (f' {:lib/metadata meta/metadata} -1))))))
@@ -28,79 +25,73 @@
         categories-id-metadata      (lib.metadata/stage-column q2 -1 "ID")
         checkins-date-metadata      (lib.metadata/field q3 nil "CHECKINS" "DATE")]
     (testing "comparisons"
-      (doseq [[op f ->f] [[:= lib/= lib/->=]
-                          [:!= lib/!= lib/->!=]
-                          [:< lib/< lib/-><]
-                          [:<= lib/<= lib/-><=]
-                          [:> lib/> lib/->>]
-                          [:>= lib/>=  lib/->>=]]]
+      (doseq [[op f] [[:=  lib/=]
+                      [:!= lib/!=]
+                      [:<  lib/<]
+                      [:<= lib/<=]
+                      [:>  lib/>]
+                      [:>= lib/>=]]]
         (test-clause
-         [op
-          {:lib/uuid string?}
-          [:field {:lib/uuid string?} (meta/id :venues :category-id)]
-          [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]]
-         f ->f
+         {:operator op
+          :args [[:field {:lib/uuid string?} (meta/id :venues :category-id)]
+                 [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]]}
+         f
          venues-category-id-metadata
          categories-id-metadata)))
 
     (testing "between"
       (test-clause
-       [:between
-        {:lib/uuid string?}
-        [:field {:lib/uuid string?} (meta/id :venues :category-id)]
-        42
-        [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]]
-       lib/between lib/->between
+       {:operator :between
+        :args [[:field {:lib/uuid string?} (meta/id :venues :category-id)]
+               42
+               [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]]}
+       lib/between
        venues-category-id-metadata
        42
        categories-id-metadata))
 
     (testing "inside"
       (test-clause
-       [:inside
-        {:lib/uuid string?}
-        [:field {:base-type :type/Float, :lib/uuid string?} (meta/id :venues :latitude)]
-        [:field {:base-type :type/Float, :lib/uuid string?} (meta/id :venues :longitude)]
-        42.7 13 4 27.3]
-       lib/inside lib/->inside
+       {:operator :inside
+        :args [[:field {:base-type :type/Float, :lib/uuid string?} (meta/id :venues :latitude)]
+               [:field {:base-type :type/Float, :lib/uuid string?} (meta/id :venues :longitude)]
+               42.7 13 4 27.3]}
+       lib/inside
        venues-latitude-metadata
        venues-longitude-metadata
        42.7 13 4 27.3))
 
     (testing "emptiness"
-      (doseq [[op f ->f] [[:is-null lib/is-null  lib/->is-null]
-                          [:not-null lib/not-null lib/->not-null]
-                          [:is-empty lib/is-empty lib/->is-empty]
-                          [:not-empty lib/not-empty lib/->not-empty]]]
+      (doseq [[op f] [[:is-null   lib/is-null]
+                      [:not-null  lib/not-null]
+                      [:is-empty  lib/is-empty]
+                      [:not-empty lib/not-empty]]]
         (test-clause
-         [op
-          {:lib/uuid string?}
-          [:field {:lib/uuid string?} (meta/id :venues :name)]]
-         f ->f
+         {:operator op
+          :args [[:field {:lib/uuid string?} (meta/id :venues :name)]]}
+         f
          venues-name-metadata)))
 
     (testing "string tests"
-      (doseq [[op f ->f] [[:starts-with lib/starts-with  lib/->starts-with]
-                          [:ends-with lib/ends-with lib/->ends-with]
-                          [:contains lib/contains lib/->contains]
-                          [:does-not-contain lib/does-not-contain lib/->does-not-contain]]]
+      (doseq [[op f] [[:starts-with      lib/starts-with]
+                      [:ends-with        lib/ends-with]
+                      [:contains         lib/contains]
+                      [:does-not-contain lib/does-not-contain]]]
         (test-clause
-         [op
-          {:lib/uuid string?}
-          [:field {:lib/uuid string?} (meta/id :venues :name)]
-          "part"]
-         f ->f
+         {:operator op
+          :args [[:field {:lib/uuid string?} (meta/id :venues :name)]
+                 "part"]}
+         f
          venues-name-metadata
          "part")))
 
     (testing "time-interval"
       (test-clause
-       [:time-interval
-        {:lib/uuid string?}
-        [:field {:base-type :type/Date, :lib/uuid string?} (meta/id :checkins :date)]
-        3
-        :day]
-       lib/time-interval lib/->time-interval
+       {:operator :time-interval
+        :args [[:field {:base-type :type/Date, :lib/uuid string?} (meta/id :checkins :date)]
+               3
+               :day]}
+       lib/time-interval
        checkins-date-metadata
        3
        :day))
@@ -108,16 +99,15 @@
     (testing "segment"
       (doseq [id [7 "6"]]
         (test-clause
-         [:segment {:lib/uuid string?} id]
-         lib/segment lib/->segment
+         {:operator :segment
+          :args [id]}
+         lib/segment
          id)))))
 
 (deftest ^:parallel filter-test
   (let [q1                          (lib/query-for-table-name meta/metadata-provider "CATEGORIES")
         q2                          (lib/saved-question-query meta/metadata-provider meta/saved-question)
         venues-category-id-metadata (lib.metadata/field q1 nil "VENUES" "CATEGORY_ID")
-        venues-name-metadata        (lib.metadata/field q1 nil "VENUES" "NAME")
-        categories-id-metadata      (lib.metadata/stage-column q2 -1 "ID")
         original-filter
         [:between
          {:lib/uuid string?}
@@ -135,62 +125,26 @@
     (testing "no filter"
       (is (nil? (lib/current-filter q1)))
       (is (= [] (lib/current-filters q2))))
-    (testing "setting a simple filter"
-      (let [result-query
-            (lib/filter q1 (lib/between {:lib/metadata meta/metadata} -1 venues-category-id-metadata 42 100))]
-        (is (=? simple-filtered-query
-                (dissoc result-query :lib/metadata)))
-        (testing "and getting the current filter"
-          (is (=? original-filter
-                  (lib/current-filter result-query)))
-          (is (=? [original-filter]
-                  (lib/current-filters result-query))))))
 
-    (testing "setting a simple filter thunk"
-      (is (=? simple-filtered-query
-              (-> q1
-                  (lib/filter (lib/->between venues-category-id-metadata 42 100))
-                  (dissoc :lib/metadata)))))
+    (testing "setting a simple filter via the helper function"
+      (let [result-query
+            (lib/filter q1 (lib/between venues-category-id-metadata 42 100))
+            result-filter {:operator (-> original-filter first name)
+                          :options (second original-filter)
+                          :args (subvec original-filter 2)}]
+       (is (=? simple-filtered-query
+               (dissoc result-query :lib/metadata)))
+       (testing "and getting the current filter"
+         (is (=? result-filter
+                 (lib/current-filter result-query)))
+         (is (=? [result-filter]
+                 (lib/current-filters result-query))))))
 
     (testing "setting a simple filter expression"
       (is (=? simple-filtered-query
               (-> q1
-                  (lib/filter [:between venues-category-id-metadata 42 100])
-                  (dissoc :lib/metadata)))))
-
-    (testing "setting a nested filter expression"
-      (is (=? {:lib/type :mbql/query,
-               :database (meta/id),
-               :type :pipeline,
-               :stages
-               [{:lib/type :mbql.stage/mbql,
-                 :source-table (meta/id :categories)
-                 :lib/options #:lib{:uuid string?}
-                 :filter
-                 [:or
-                  #:lib{:uuid string?}
-                  [:between
-                   #:lib{:uuid string?}
-                   [:field {:base-type :type/Integer, :lib/uuid string?} (meta/id :venues :category-id)]
-                   42
-                   100]
-                  [:and
-                   #:lib{:uuid string?}
-                   [:=
-                    #:lib{:uuid string?}
-                    [:field {:base-type :type/Integer, :lib/uuid string?} (meta/id :venues :category-id)]
-                    242
-                    [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]]
-                   [:contains
-                    #:lib{:uuid string?}
-                    [:field {:base-type :type/Text, :lib/uuid string?} (meta/id :venues :name)]
-                    "part"]]]}]}
-              (-> q1
-                  (lib/filter [:or
-                               [:between venues-category-id-metadata 42 100]
-                               [:and
-                                [:= venues-category-id-metadata 242 categories-id-metadata]
-                                [:contains venues-name-metadata "part"]]])
+                  (lib/filter {:operator :between
+                               :args [(lib.field/field q1 venues-category-id-metadata) 42 100]})
                   (dissoc :lib/metadata)))))))
 
 (deftest ^:parallel add-filter-test
@@ -204,41 +158,46 @@
           (meta/id :venues :category-id)]
          42
          100]
+        first-result-filter
+        {:operator (-> first-filter first name)
+         :options (second first-filter)
+         :args (subvec first-filter 2)}
         second-filter
         [:starts-with
          {:lib/uuid string?}
          [:field {:base-type :type/Text, :lib/uuid string?} (meta/id :venues :name)]
          "prefix"]
+        second-result-filter
+        {:operator (-> second-filter first name)
+         :options (second second-filter)
+         :args (subvec second-filter 2)}
         third-filter
         [:contains
          {:lib/uuid string?}
          [:field {:base-type :type/Text, :lib/uuid string?} (meta/id :venues :name)]
          "part"]
+        third-result-filter
+        {:operator (-> third-filter first name)
+         :options (second third-filter)
+         :args (subvec third-filter 2)}
         first-add
         (lib/add-filter simple-query
-                        (lib/->between
-                         (lib.metadata/field simple-query nil "VENUES" "CATEGORY_ID")
+                        (lib/between
+                         (lib/field "VENUES" "CATEGORY_ID")
                          42
                          100))
         filtered-query
         (assoc-in simple-query [:stages 0 :filter] first-filter)
         second-add
-        (lib/add-filter first-add
-                        (lib/starts-with
-                         {:lib/metadata meta/metadata}
-                         0
-                         venues-name-metadata
-                         "prefix"))
+        (lib/add-filter first-add {:operator "starts-with"
+                                   :args [(lib.field/field simple-query venues-name-metadata) "prefix"]})
         and-query
         (assoc-in filtered-query
                   [:stages 0 :filter]
-                  [:and
-                   {:lib/uuid string?}
-                   first-filter
-                   second-filter])
+                  [:and {:lib/uuid string?} first-filter second-filter])
         third-add
-        (lib/add-filter second-add
-                        (lib/->contains venues-name-metadata "part"))
+        (lib/add-filter second-add {:operator :contains
+                                    :args [(lib.field/field simple-query venues-name-metadata) "part"]})
         extended-and-query
         (assoc-in filtered-query
                   [:stages 0 :filter]
@@ -252,15 +211,15 @@
                     "part"]])]
     (testing "adding an initial filter"
       (is (=? filtered-query first-add))
-      (is (=? [first-filter]
+      (is (=? [first-result-filter]
               (lib/current-filters first-add))))
     (testing "conjoining to filter"
       (is (=? and-query second-add))
-      (is (=? [first-filter second-filter]
+      (is (=? [first-result-filter second-result-filter]
               (lib/current-filters second-add))))
     (testing "conjoining to conjunction filter"
       (is (=? extended-and-query third-add))
-      (is (=? [first-filter second-filter third-filter]
+      (is (=? [first-result-filter second-result-filter third-result-filter]
               (lib/current-filters third-add))))))
 
 (deftest ^:parallel replace-filter-test
@@ -269,9 +228,9 @@
         venues-category-id-metadata (lib.metadata/field q1 nil "VENUES" "CATEGORY_ID")
         simple-filtered-query
         (-> q1
-            (lib/filter (lib/->between venues-category-id-metadata 42 100)))
+            (lib/filter (lib/between venues-category-id-metadata 42 100)))
         between-uuid (-> (lib/current-filter simple-filtered-query)
-                         lib.options/options
+                         :options
                          :lib/uuid)
         result-query
         (assoc-in simple-filtered-query
@@ -286,57 +245,16 @@
       (is (=? result-query
               (lib/replace-filter simple-filtered-query
                                   between-uuid
-                                  (lib/starts-with
-                                   {:lib/metadata meta/metadata}
-                                   0
-                                   venues-name-metadata
-                                   "part")))))
+                                  (lib/starts-with venues-name-metadata "part")))))
     (testing "setting a simple filter thunk"
       (is (=? result-query
               (lib/replace-filter simple-filtered-query
                                   between-uuid
-                                  (lib/->starts-with venues-name-metadata "part")))))
+                                  {:operator "starts-with"
+                                   :args [(lib.field/field q1 venues-name-metadata) "part"]}))))
     (testing "setting a simple filter expression"
       (is (=? result-query
               (lib/replace-filter simple-filtered-query
                                   between-uuid
-                                  [:starts-with venues-name-metadata "part"]))))
-
-    (let [contains-uuid (str (random-uuid))
-          nested-filtered-query
-          {:lib/type :mbql/query,
-           :database (meta/id),
-           :type :pipeline,
-           :stages
-           [{:lib/type :mbql.stage/mbql,
-             :source-table (meta/id :categories)
-             :lib/options {:lib/uuid (str (random-uuid))}
-             :filter
-             [:or
-              {:lib/uuid (str (random-uuid))}
-              [:between
-               {:lib/uuid (str (random-uuid))}
-               [:field {:base-type :type/Integer, :lib/uuid (str (random-uuid))} (meta/id :venues :category-id)]
-               42
-               100]
-              [:and
-               {:lib/uuid (str (random-uuid))}
-               [:contains
-                {:lib/uuid contains-uuid}
-                [:field {:base-type :type/Text, :lib/uuid (str (random-uuid))} (meta/id :venues :name)]
-                "part"]
-               [:=
-                {:lib/uuid (str (random-uuid))}
-                [:field {:base-type :type/Integer, :lib/uuid (str (random-uuid))} (meta/id :venues :category-id)]
-                242
-                [:field {:base-type :type/BigInteger, :lib/uuid (str (random-uuid))} "ID"]]]]}]}]
-      (testing "setting a nested filter expression"
-        (is (=? (assoc-in nested-filtered-query
-                          [:stages 0 :filter 3 2]
-                          [:starts-with
-                           {:lib/uuid string?}
-                           [:field {:base-type :type/Text, :lib/uuid string?} (meta/id :venues :name)]
-                           "part"])
-                (lib/replace-filter nested-filtered-query
-                                    contains-uuid
-                                    [:starts-with venues-name-metadata "part"])))))))
+                                  {:operator :starts-with
+                                   :args [(lib.field/field q1 venues-name-metadata) "part"]}))))))
diff --git a/test/metabase/lib/join_test.cljc b/test/metabase/lib/join_test.cljc
index 793567922a2cdb2265d9b1c4614dfb26e9043fd9..c88c71f16c6fcf5a46d43d060620d77e86b40b6b 100644
--- a/test/metabase/lib/join_test.cljc
+++ b/test/metabase/lib/join_test.cljc
@@ -2,6 +2,7 @@
   (:require
    [clojure.test :refer [deftest is testing]]
    [metabase.lib.core :as lib]
+   [metabase.lib.field :as lib.field]
    [metabase.lib.join :as lib.join]
    [metabase.lib.metadata :as lib.metadata]
    [metabase.lib.metadata.calculation :as lib.metadata.calculation]
@@ -15,7 +16,7 @@
   (let [query       (lib/query meta/metadata-provider (meta/table-metadata :venues))
         join-clause (-> ((lib/join-clause
                           (meta/table-metadata :categories)
-                          (lib/->=
+                          (lib/=
                            (lib/field (meta/id :venues :category-id))
                            (lib/with-join-alias (lib/field (meta/id :categories :id)) "CATEGORIES__via__CATEGORY_ID")))
                          query -1)
@@ -41,11 +42,13 @@
                                                      {:lib/uuid string?}
                                                      [:field {:lib/uuid string?} (meta/id :venues :category-id)]
                                                      [:field {:lib/uuid string?} (meta/id :categories :id)]]}]}]}
-          (-> (lib/query-for-table-name meta/metadata-provider "VENUES")
-              (lib/join (lib/query-for-table-name meta/metadata-provider "CATEGORIES")
-                        (lib/->= (lib/field "VENUES" "CATEGORY_ID")
-                                 (lib/field "CATEGORIES" "ID")))
-              (dissoc :lib/metadata)))))
+          (let [q (lib/query-for-table-name meta/metadata-provider "VENUES")]
+            (-> q
+                (lib/join (lib/query-for-table-name meta/metadata-provider "CATEGORIES")
+                          {:operator :=
+                           :args [(lib.field/field q (lib.metadata/field q nil "VENUES" "CATEGORY_ID"))
+                                  (lib.field/field q (lib.metadata/field q nil "CATEGORIES" "ID"))]})
+                (dissoc :lib/metadata))))))
 
 (deftest ^:parallel join-saved-question-test
   (is (=? {:lib/type :mbql/query
@@ -65,8 +68,8 @@
                                                      [:field {:lib/uuid string?} (meta/id :categories :id)]]}]}]}
           (-> (lib/query-for-table-name meta/metadata-provider "CATEGORIES")
               (lib/join (lib/saved-question-query meta/metadata-provider meta/saved-question)
-                        (lib/->= (lib/field "VENUES" "CATEGORY_ID")
-                                 (lib/field "CATEGORIES" "ID")))
+                        (lib/= (lib/field "VENUES" "CATEGORY_ID")
+                               (lib/field "CATEGORIES" "ID")))
               (dissoc :lib/metadata)))))
 
 (deftest ^:parallel join-condition-field-metadata-test
@@ -76,7 +79,7 @@
           venues-category-id-metadata (lib.metadata/field q1 nil "VENUES" "CATEGORY_ID")
           categories-id-metadata      (lib.metadata/stage-column q2 "ID")]
       (testing "lib/join-clause: return a function that can be resolved later"
-        (let [f (lib/join-clause q2 (lib/->= venues-category-id-metadata categories-id-metadata))]
+        (let [f (lib/join-clause q2 (lib/= venues-category-id-metadata categories-id-metadata))]
           (is (fn? f))
           (is (=? {:lib/type    :mbql/join
                    :lib/options {:lib/uuid string?}
@@ -98,7 +101,7 @@
                                                          [:field {:lib/uuid string?} (meta/id :venues :category-id)]
                                                          [:field {:base-type :type/BigInteger, :lib/uuid string?} "ID"]]}]}]}
               (-> q1
-                  (lib/join q2 (lib/->= venues-category-id-metadata categories-id-metadata))
+                  (lib/join q2 (lib/= venues-category-id-metadata categories-id-metadata))
                   (dissoc :lib/metadata)))))))
 
 ;;; FIXME
diff --git a/test/metabase/lib/query_test.cljc b/test/metabase/lib/query_test.cljc
index f2d430a357409e8d9688a3494fd357068401cab9..642940c94b43dbc6d834131f4f08550ccdb86466 100644
--- a/test/metabase/lib/query_test.cljc
+++ b/test/metabase/lib/query_test.cljc
@@ -15,7 +15,7 @@
                   (lib/aggregate (lib/sum (lib/field (meta/id :venues :price)))))
         ;; wrong arity: there's a bug in our Kondo config, see https://metaboat.slack.com/archives/C04DN5VRQM6/p1679022185079739?thread_ts=1679022025.317059&cid=C04DN5VRQM6
         query (-> #_{:clj-kondo/ignore [:invalid-arity]}
-                  (lib/filter query (lib/= query -1 (lib/field (meta/id :venues :name)) "Toucannery"))
+                  (lib/filter query (lib/= (lib/field (meta/id :venues :name)) "Toucannery"))
                   (lib/breakout (lib/field (meta/id :venues :category-id)))
                   (lib/order-by (lib/field (meta/id :venues :id)))
                   (lib/limit 100))]
diff --git a/test/metabase/lib/util_test.cljc b/test/metabase/lib/util_test.cljc
index 6ef3efb1c45de9bc54367218d6b767e5fa065472..aeb3da851b74d484f0946aff9a7554d90e63b0ca 100644
--- a/test/metabase/lib/util_test.cljc
+++ b/test/metabase/lib/util_test.cljc
@@ -220,7 +220,7 @@
 
 (deftest ^:parallel replace-clause-test
   (checking "can be called with anything"
-    [x gen/any]
+    [x gen/any-equatable]
     (is (identical? x (lib.util/replace-clause x
                                                (str (random-uuid))
                                                (random-uuid)))))