A Simple Async Task in Clojure
Commenting on this section:

; test-data mocks fetching a url by returning the string of a random int
(defn test-data [_] (str (rand-int 100)))

(def pages&gt; (chan 1))
(pipeline 4
          pages&gt;
          ; add the fetched page to the object getting passed around
          (map (fn [{:keys [url] :as obj}] 
                 (conj obj {:page (test-data url)})))
          db-objs&gt;)


In real-life, test-data will either do blocking I/O or be asynchronous. 
Therefore, you should use pipeline-blocking or pipeline-async instead, otherwise you'll exhaust the "go thread pool".

Commenting on this part:

(go-loop []
  (let [{:keys [id scraped]} (&lt;! selections&gt;)]
    (test-append [id :vals] scraped))
  (recur))


You should account for the fact that the selections&gt; channel may close, in which case the object will be nil and you should stop the loop. 

(go-loop []
  (when-let [{:keys [id scraped]} (&lt;! selections&gt;)]
    (test-append [id :vals] scraped)
    (recur)))


Commenting on this part:

(doseq [[id data] @test-db] (go (&gt;! db-objs&gt; (conj data {:id id}))))


Because go is asynchronous, this consists of putting all the objects in test-db at once into the processing pipeline, which potentially consumes a lot of memory. Core.async channels are designed to give you back-pressure, you should use it.

How about instead:

(go (doseq [[id data] @test-db] (&gt;! db-objs&gt; (conj data {:id id}))))


Which you can express at a higher-level using onto-chan

(onto-chan db-objs (map (fn [data] (conj data {:id id})) @test-db) false)