               output-names #js []
               output-fields #js []]
 
-          (doseq [[_ clause-type clause-id name] clauses
+          (doseq [[_ clause-type clause-id clause-name] clauses
                   :when (not= clause-type "when")]
             (let [clause-fields (get @clause-id->fields clause-id)
-                  fields (make-array num-vars)]
-              (doseq [[_ field-type key val] clause-fields]
-                (assert (= field-type "variable") [rule-id clause-id clause-fields])
+                  fields (make-array num-vars)
+                  aggregate-variable (first (for [[_ field-type key val] clause-fields
+                                                  :when (= key "aggregate-variable")]
+                                              val))
+
+                  aggregate-function (first (for [[_ field-type key val] clause-fields
+                                                  :when (= key "aggregate-function")]
+                                              val))
+                  output-name (if (nil? aggregate-variable)
+                                clause-name
+                                (str "aggregate-" rule-id))
+                  output-type (if (nil? aggregate-variable)
+                                clause-type
+                                "know")]
+
+              ;; add to rule outputs
+              (doseq [[_ field-type key val] clause-fields
+                      :when (= field-type "variable")]
                 (aset fields (var->ix val) key))
-              (apush output-kinds clause-type)
-              (apush output-names name)
+              (apush output-kinds output-type)
+              (apush output-names output-name)
               (apush output-fields fields)
-              (let [filtered-fields (into-array (filter #(not (nil? %)) fields))
+
+              ;; ensure indexes exist
+              (let [filtered-fields (into-array (filter #(not (or (nil? %) (= aggregate-variable %))) fields))
+                    ;; make sure aggregate-variable is last if it exists
+                    _ (when-not (nil? aggregate-variable)
+                        (apush filtered-fields aggregate-variable))
                     ;; TODO get transient? from schema instead
                     transient? (identical? clause-type "know")]
-                (assert (not= (not transient?) (get @name->transient? name)) name)
-                (swap! name->transient? assoc name transient?)
-                (swap! kind->name->rules update-in [clause-type name] #(or % #{}))
-                (.ensure-index kn "know" name filtered-fields)
+                (assert (not= (not transient?) (get @name->transient? clause-name)) clause-name)
+                (swap! name->transient? assoc clause-name transient?)
+                (swap! kind->name->rules update-in [clause-type clause-name] #(or % #{}))
+                (.ensure-index kn "know" clause-name filtered-fields)
                 (when (false? transient?)
-                  (.ensure-index kn "forget" name filtered-fields)
-                  (.ensure-index kn "remember" name filtered-fields)))))
-
-          (swap! rule->flow assoc rule-id (Flow. solver output-kinds output-names output-fields)))))
+                  (.ensure-index kn "forget" clause-name filtered-fields)
+                  (.ensure-index kn "remember" clause-name filtered-fields))
+
+                ;; create aggregate flow if needed
+                (when-not (nil? aggregate-variable)
+                  (let [input-index (.get-or-create-index kn "know" output-name filtered-fields)
+                        aggregate-function (aget js/aurora.aggregates aggregate-function)]
+                    (swap! name->transient? assoc output-name true)
+                    (swap! kind->name->rules update-in ["know" output-name] #(conj (or % #{}) output-name))
+                    (swap! rule->flow assoc output-name (AggregateFlow. input-index clause-type clause-name filtered-fields aggregate-function)))))))
+
+          (swap! rule->flow assoc rule-id (SolverFlow. solver output-kinds output-names output-fields)))))
 
     ;; TODO stratify
     (Flows. (clj->js (map first @rule->flow)) (clj->js @rule->flow) #js {} (clj->js @kind->name->rules) (clj->js @name->transient?))))


;; FLOWS

(deftype Flow [solver output-kinds output-names output-fields]
  Object
  (run [this kn rule->dirty? kind->name->rules]
       (.reset solver)

              :when (not (#{"=constant" "=variable" "=function" "filter"} name))]
        (let [fields (get @clause-id->fields clause-id)
              var->key (atom {})]
          (doseq [[_ field-type key val] fields
                  :when (not (#{"aggregate-variable" "aggregate-function"} key))]
            (if (= field-type "constant")
              ;; rewrite (foo 1) to (foo x) (constant= x 1)
              (let [new-var (new-id)
