How One Clojure Function Destroyed Agent Framework Completely
Tired of complex agent frameworks? See how Clojure’s hidden gem, the `iteration` function, delivers transparent, efficient, and fully controllable agentic workflows—no YAML, no headaches, just clear code and real results.

Four months ago, I started building AI systems for Vade AI, an AI-native no-code platform that helps businesses create multilingual websites to reach wider audiences. Like any sensible developer diving into the AI space, I reached for the popular tools: CrewAI, Mastra, and other shiny agent frameworks that promised to make multi-agent orchestration a breeze.
Spoiler alert: they didn't.
Don't get me wrong, these frameworks work. But every time I tried to implement something slightly custom or debug a workflow that wasn't behaving as expected, I found myself drowning in configuration files, role definitions, and abstractions that seemed designed to hide rather than reveal what was actually happening.
I kept thinking, "There has to be a simpler way."
Turns out, there is. And it's been hiding in plain sight in Clojure 1.11, disguised as a humble function for consuming paginated APIs. That function is iteration
, and it's about to change how you think about agentic workflows.
The Agent Framework Complexity Trap

Let me paint you a picture of my typical Tuesday morning four months ago. I'd open my laptop, fire up the Vade AI codebase, and immediately face a wall of YAML configuration files for CrewAI. Each agent needed a role, a goal, a backstory (yes, really), and carefully defined tools. Then I'd have to orchestrate how these agents talk to each other, manage their shared memory, and handle the inevitable failures when one agent decides to go rogue.
CrewAI markets itself as "lean, standalone, high-performance," but the reality feels more like managing a small corporation. You need:
State Management because someone has to remember what Agent A told Agent B three turns ago
Task Orchestration so your research agent doesn't start writing before your planning agent finishes thinking
Error Handling for when your "expert analyst" agent starts hallucinating about fictional market trends
Resource Management because burning through your OpenAI credits in 10 minutes isn't a sustainable business model
Context Window Management for those moments when your agents get chatty and exceed token limits
And here's the kicker: despite all this complexity, you still don't have full control. The framework makes decisions for you, often in ways that are hard to debug or modify.
After weeks of wrestling with these abstractions, I started wondering: what if we're overthinking this whole agent thing?
Enter Clojure's iteration
Function
Here's where things get interesting. While digging through Clojure's documentation (as one does when procrastinating on actually fixing bugs), I stumbled upon a function introduced in version 1.11 that was supposedly designed for consuming paginated APIs.
But as I read the docs, something clicked. This wasn't just about API pagination—this was a general-purpose iteration pattern that could model any sequential, stateful process. Including agentic workflows.
Meet iteration
:
(iteration step & {:keys [somef vf kf initk],
:or {vf identity, kf identity, somef some?, initk nil}})
I know, I know. It looks like typical Clojure function signature soup. But stick with me—this is where it gets good.
Let me translate the parameters into human terms:
step
is your workhorse. It takes some state and does something with it—like calling an LLM, processing a response, or deciding what to do next.
initk
is where you start. Your initial prompt, conversation state, whatever gets the ball rolling.
vf
(value function) extracts the actual result you care about from each step. Think of it as "give me the good stuff from this API response."
kf
(continuation function) figures out what state to pass to the next iteration. It's like asking "what should I do next?"
somef
(some function) is your bouncer. It decides whether the party continues or if it's time to go home.
In agent terms, this maps beautifully:
step
is where you call your LLM or execute toolsinitk
is your starting prompt or taskvf
extracts the AI response from API noisekf
determines your next conversation statesomef
checks if you've achieved your goal
A Real-World Example: Building an Agentic Workflow
Let me show you how this looks in practice. Here's a simplified version of how we handle agentic workflows at Vade AI:
(defn simple-agent-workflow
"A basic agentic workflow using iteration"
[initial-prompt max-iterations]
(let [llm-instance (create-llm-instance)
step-fn (fn [{:keys [iteration prompt response]}]
(when (< iteration max-iterations)
(let [messages [(create-message :user prompt)]
new-response (generate llm-instance messages)
next-prompt (extract-next-task new-response)]
{:iteration iteration
:prompt prompt
:response new-response
:next-token {:iteration (inc iteration)
:prompt next-prompt
:response new-response}})))]
(iteration step-fn
:somef (fn [res] (some? res))
:vf identity
:kf :next-token
:initk {:iteration 0 :prompt initial-prompt :response {}})))
This it is. No YAML configs, no agent role definitions, no complex orchestration setup. Just a simple function that calls an LLM, processes the response, and decides what to do next.
But here's where it gets really interesting. Let me show you our actual production code:
(defn generate-operations
[{:keys [context] :as params} {:keys [push-operation-fn push-usage-fn max-iterations]}]
(let [tid->rid (atom {})
provider-config (->provider-config (:ai-provider context))
llm-instance (v.llm/create-provider provider-config)
system-prompt (load-system-prompt)
step-fn (fn [{:keys [iteration response]}]
(when (and (< iteration max-iterations) (not (completed? response)))
(let [params (if (zero? iteration)
params
(assoc params :message (extract-summary response)))
user-prompt (load-user-prompt params)
system-msg (v.llm/create-message :system system-prompt)
user-msg (v.llm/create-message :user user-prompt)
response (v.llm/generate-stream llm-instance [system-msg user-msg]
{:as (stream-json-operations
{:push-operation-fn push-operation-fn
:push-usage-fn push-usage-fn
:context context
:tid->rid tid->rid})})]
{:iteration iteration
:response response
:next-token {:iteration (inc iteration) :response response}})))]
(iteration step-fn
:somef (fn [res] (some? res))
:vf identity
:kf :next-token
:initk {:iteration 0 :response {}})))
This is the actual code that powers our multi-agent workflows at Vade AI. No frameworks, no complex abstractions, just pure logic that you can read and understand in five minutes.
The Elegant Simplicity

Here's what hit me when I first got this working: look at what we didn't need to set up.
No complex agent role definitions where you pretend your AI is a "Senior Data Analyst with 10 years of experience in market research."
No task dependency graphs that look like subway maps.
No workflow orchestration engines that require a PhD in distributed systems to configure.
No state management frameworks that make Redux look simple.
No error recovery systems with retry logic, circuit breakers, and exponential backoff strategies.
No message passing protocols between agents that half the time end up talking past each other anyway.
What we do have is something beautiful:
Complete control over each step. Want to add logging? Just add it to your step function. Need to validate responses? Handle it right there in the code.
Transparent state flow. You can see exactly what's happening at each iteration. No hidden state, no mysterious framework magic.
Simple error handling. If something goes wrong, your step function returns nil
and the iteration stops. That's it.
Easy debugging. Want to see what's happening? Just print the state at each step. No special debugging tools required.
Flexible termination conditions. Want to stop when you've found the answer? When you've hit a cost limit? When it's Tuesday? Just encode it in your somef
function.
And the best part? It composes beautifully with the rest of Clojure. This isn't a framework that takes over your application—it's just a function that does exactly what you tell it to do.
Streaming and Real-Time Operations
Want to see something really cool? Our production system handles streaming responses from LLMs and processes operations in real-time. Here's how we do it with iteration
:
(defn stream-json-operations
[{:keys [context push-operation-fn push-usage-fn tid->rid]}]
(fn parse-streaming-json
([]
{:text (StringBuilder.)
:parsed (v.ds/ordered-set)
:seen-hashes #{}
:start-time (System/currentTimeMillis)})
([{:keys [text parsed start-time metrics]}]
(let [metrics (assoc metrics
:duration-ms (+ (- (System/currentTimeMillis) start-time)
(:request-time metrics))
:total-tokens (+ (:input-tokens metrics 0)
(:output-tokens metrics 0)))]
(push-usage-fn {:id "create-usage"
:type "create:usage"
:data {:id (u.id/uuid)
:quantity 1
:amount 0.10
:metadata metrics}
:context context})
{:role :assistant
:created-at start-time
:content (.toString text)
:parsed (vec parsed)
:metrics metrics}))
([{:keys [text seen-hashes] :as acc} {:keys [content] :as chunk}]
(if content
(let [_ (.append text content)
extracted (v.json/extract (.toString text))
new-objects (->> (resolve-tempids extracted tid->rid)
(remove #(contains? seen-hashes (hash (pr-str %))))
(map (fn [op]
(push-operation-fn (assoc op :context context))
op)))]
(-> acc
(assoc :text text)
(update :parsed into new-objects)
(update :seen-hashes into (map (comp hash pr-str) new-objects))))
acc))))
This transducer-style pattern lets us process LLM responses as they stream in. Each chunk gets processed incrementally, with operations extracted and dispatched the moment they become available. The user sees results in real-time, not after waiting for the entire response to complete.
Try implementing this elegantly in CrewAI. I'll wait.
Why This Actually Matters

Now, you might be thinking, "Okay, this is neat, but why should I care? Frameworks exist for a reason."
Here's the thing: traditional agent frameworks treat complexity as a selling point. The more features, abstractions, and configuration options, the better, right? CrewAI wants you to manage "agent roles, task dependencies, workflow orchestration, and complex state management" with specialized configurations for "autonomous AI agents that work together as a cohesive assembly."
But complexity is the enemy of everything we actually care about: understanding our code, debugging when things go wrong, and maintaining systems over time.
Clojure's iteration
function represents a completely different philosophy: simple primitives that compose. Instead of fighting framework abstractions, you write clear, debuggable code that does exactly what you need, nothing more, nothing less.
The function's design acknowledges something crucial that most frameworks miss: agentic workflows are fundamentally iterative processes with state transitions. Whether you're building a research assistant that progressively refines its search, creating a planning agent that breaks down complex tasks, implementing a validation system that iteratively improves outputs, or orchestrating multiple LLM calls with conversation history, you're dealing with the same core pattern.
It all boils down to: (current-state, input) → (new-state, output, should-continue?)
Once you see this pattern, everything else is just implementation details.
The Performance Benefits
Beyond the philosophical benefits, this approach delivers real, measurable performance improvements:
Memory Efficiency: No framework overhead means no hidden object graphs eating your RAM. Our workflows run with predictable memory usage that scales linearly with the actual work being done.
Predictable Resource Usage: You control exactly when and how API calls happen. No surprise batch requests or hidden retries burning through your AI credits.
Easy Optimization: Want to improve performance? Profile your step function. No need to understand framework internals or fight with black-box optimizations.
Transparent Costs: Every token, every API call, every operation is visible and controllable. We track resource usage with surgical precision because there's nowhere for costs to hide.
Our production system at Vade AI processes thousands of agentic operations daily. The transparency of the iteration
approach makes optimization straightforward, and our performance is predictable enough that we can actually provide meaningful SLAs to our customers.
When to Use This Approach

The iteration
pattern isn't right for every situation, but it shines when you need:
Complete control over agent behavior and state management. If you find yourself fighting framework conventions or working around limitations, iteration
might be your answer.
Transparent, debuggable workflows. When you need to understand exactly what's happening at each step, not guess what the framework is doing behind the scenes.
Custom termination logic or complex branching. Frameworks are great for happy path scenarios, but real-world logic is messy and conditional.
Performance optimization without framework overhead. When every millisecond and every token counts.
Integration with existing Clojure systems. If you're already in the Clojure ecosystem, why add framework dependencies?
It's particularly powerful for:
Research and analysis workflows where you need iterative refinement and the ability to change direction based on intermediate results.
Planning systems that break down complex tasks into manageable pieces, adapting the approach as new information becomes available.
Validation pipelines with multiple improvement cycles, where each iteration makes the output better.
Custom agent behaviors that don't fit the cookie-cutter patterns most frameworks provide.
The sweet spot is when you have complex logic that needs to be both reliable and adaptable. Standard frameworks excel at standard problems, but real business problems are rarely standard.
The Path Forward

We're barely scratching the surface of what becomes possible with this approach. The iteration
function enables patterns that would be complex or impossible in traditional agent frameworks:
Adaptive workflows that change their behavior based on intermediate results. Imagine an agent that switches from research mode to analysis mode based on what it discovers, without pre-defining all possible paths.
Resource-aware agents that optimize for cost and latency in real-time. Need to stay under budget? Your agent can switch to a cheaper model or reduce iterations based on current spending.
Hybrid systems that seamlessly combine symbolic reasoning with neural computation. Traditional frameworks struggle with non-neural components, but iteration
treats them all as just functions.
Domain-specific languages for agent behavior specification. You can build custom abstractions on top of iteration
that are perfectly suited to your specific problem domain.
The key insight is that simplicity enables complexity. By starting with a simple, composable primitive like iteration
, we can build sophisticated systems without drowning in accidental complexity.
At Vade AI, we're exploring some fascinating directions with this approach. Multi-language content generation where each iteration refines not just the content but the language strategy itself. Dynamic pricing models where agents iteratively optimize for conversion while staying within brand guidelines. Quality assurance systems that get progressively more thorough based on the confidence level of previous checks.
The possibilities multiply when you're not constrained by framework limitations.
The Bottom Line
Here's what I wish someone had told me four months ago when I was drowning in CrewAI configuration files: agent frameworks promise to simplify AI development, but they often deliver the opposite. They trade understanding for abstraction, control for convenience, and clarity for "features."
The dirty secret of the agent framework world is that most of the complexity they manage is complexity they created in the first place. You don't need elaborate role definitions, complex task orchestration, or sophisticated error recovery systems if your underlying approach is simple and transparent.
Clojure's iteration
function offers a radically different path: embrace the fundamental pattern underlying agentic workflows and build exactly what you need, nothing more, nothing less.
When you step back and look at what agents actually do—take state, process it, produce new state, and decide whether to continue—you realize that most framework complexity is solving problems that don't need to exist.
The Real Test
The next time you find yourself wrestling with an agent framework's abstractions, fighting configuration files, or trying to debug behavior that should be straightforward, ask yourself one simple question:
What would this look like with just iteration
?
You might be surprised by how much simpler the answer is.
At Vade AI, this shift in thinking has made our AI systems more reliable, more performant, and infinitely easier to understand and maintain. Our multilingual website generation workflows now handle complex business logic with code that our entire team can read and modify.
The beauty of this approach isn't just its technical elegance—it's the clarity of thought it enables. When your tools are simple and transparent, you can focus on solving actual problems instead of fighting with abstractions.
And in a world where AI systems are becoming increasingly critical to business operations, that clarity might be the most valuable feature of all.
The beauty of this approach is its transparency—every step is just code you can read, understand, and modify. No black box abstractions, no hidden complexity, just clear thinking expressed in clear code. Sometimes, the best solution isn't the most sophisticated one—it's the one that gets out of your way and lets you build what you actually need.