The answer to use (apply str ...) is usually the best one. But here is an additional technique, and a "pro tip" about the three dots in (apply str ...).
If the string's content would most naturally be generated by the print functions (which is not the case with your specific examples!), then you can capture it with with-out-str:
(with-out-str
(doseq [i (range 1 4)]
(print "|")
(print i))
(println "|")) ;; => "|1|2|3|\n"
Usually, (apply str ...) is more idiomatic. You can use the whole rich tapestry of sequence functions (interleave, interpose, repeat, cycle, ...) and extract the result as a string with (apply str ...). But you face a challenge if the sequence contains nested sequences. We mention this challenge here because there are two solutions that are specific to building up strings.
To be clear, nested sequences "work fine" in every respect except that what str does to a sequence might not be what you want. For example, to build "1------2------3":
;; not quite right:
(apply str
(interpose
(repeat 2 "---")
(range 1 4))) ;; => "1(\"---\" \"---\")2(\"---\" \"---\")3"
The matter is that repeat produced a sequence, which interpose dutifully stuck between the numbers in a bigger sequence, and str when processing the bigger sequence dutifully wrote the nested sequences in Clojure syntax. To better control how nested sequences get stringified, you could replace (repeat 2 "---") with (apply str (repeat 2 "---")). But, if the pattern of apply str within apply str occurs over and over, it hurts the program's signal-to-noise ratio. An alternative that may be cleaner is the flatten function (maybe this is its only idiomatic use):
(apply str
(flatten
(interpose
(repeat 2 "---")
(range 1 4)))) ;; => "1------2------3"