The brief is always some version of "the agent should answer customer questions about their orders."
Before I can write a single line about orders, I need an agent loop that calls a model and runs the tools it asks for, a registry of those tools, somewhere to keep the customer and order data, a rule for which agent answers first, and a panel so the client can change the wording without calling me.
That is a month of work, and it is invisible in the invoice. So I built it once.
I open-sourced it as CerebrumKit. Here is the design decision I would want to argue about first.
The decision: agents are data
The version of this I had built before kept the topology in source code. Which agents exist, what each one is told, which tools it can call, who answers first - all of it in Python, so every change was a deploy.
The new version keeps all of it in the database:
| Thing | What it is | Where it lives |
|---|---|---|
| Tool | OpenAI function spec + Python body |
tools table |
| Skill | Instructions + the tools they need |
skills table + skill_tool
|
| Agent | Description (its system prompt) + skills |
agents table + agent_skill
|
| Workflow | start / group / stop graph |
projects.workflow JSON |
| Storage | Your own tables + column descriptions | real tables in the same database |
| Context tool | A tool run before the agent reads the message | agent_context_tools |
Editing a tool body in the panel takes effect on the next message. The compiled body is cached by (tool id, body hash), so the cost of that is a dictionary miss.
What it buys
The domain expert can change the agent. The valuable sentence in a support agent is not def lookup_order(...). It is "never quote a delivery date you have not read from the order". That belongs to the person who owns the refund policy, and it should be editable on a Tuesday afternoon.
Multi-agent without a framework in your imports. A workflow graph decides which agent or group answers and in what order; a second group can review what the first one produced. One agent can delegate a self-contained task to another, with depth capped at 3.
The model-facing contract and the implementation are one row. When the tool description and the Python body can drift apart, they do. Here they are edited on the same screen.
What it costs
I would not trust a post about an architecture that only lists the good parts.
You give up code. Complex conditional routing becomes a canvas with groups, not an if. If your agent control flow is genuinely a program, this is the wrong tool and a library is the right one.
Tool bodies are code execution. They run with full builtins and are not sandboxed - deliberately, because the shipped tools import requests, SQLAlchemy and app internals, and one passes model-authored SQL to the database. So tool authoring is admin-only, and a tool body is reviewed like a commit.
No transcript in the prompt. Earlier chat messages are not replayed. Memory (a note keyed by agent and user) and tools carry the facts. Runs get cheaper and more predictable, and the tools have to be good enough to compensate.
One process. The websocket registry and in-flight tasks live in process memory, so the documented deploy is one uvicorn worker behind nginx.
The detail I did not expect to matter: column descriptions
When the agent decides which tool to call, what it reads is the text you wrote. The description on a database column - "the delivery date quoted to the customer at checkout" - is a prompt.
Schema design for an agent is prompt engineering with a type system. The panel makes you write a description on every table and every column for that reason, and the tools read them.
Two agents, one catches the other
Here is the run that convinced me the workflow was worth building.
A customer asks why order AC-10477 has not arrived. The first agent finds the customer, reads the order, searches the help articles, and answers: the order is two days past its promised date, so the late-delivery credit applies.
The second agent - a different brief, same tables - reads the same order and points out that the credit only applies while the status is shipped or packed. This order is delayed, so the first agent promise was wrong.
I did not write that check as an if. I wrote it as a second agent with a different instruction, and put it in the next group of the workflow.
How to try it
Postgres via docker compose up -d, then seed_all.py, then npm run dev, using the admin and client accounts the seeder creates from your .env. Two commands and a seed, roughly two minutes.
Repo: https://github.com/islomkhon/CerebrumKit
Over to you
If you are building agents for businesses, I would like to hear where you draw the line between data and code - and whether you moved it after your first production incident. That is the question I am still chewing on.


Top comments (2)
The "two agents, one catches the other" example is what makes this click — you turned a conditional business rule into a second agent with a different instruction, and let the workflow graph enforce the review rather than writing the check as code. That's the real unlock here. The column descriptions as prompt engineering framing is also a sharp insight; we've seen schema design decisions ripple into model behavior in ways that feel surprising until you realize the description IS the contract. The honest "what it costs" section is refreshing — the no-transcript constraint is a real trade-off that forces discipline in tool design. Excited to dig into CerebrumKit.
Thank you - that is exactly the trade I was trying to make visible.
On the scale question: there is no cap on the number of agents. You write them as separate briefs over the same tables, then stack them in the workflow to match how the business actually runs - one agent answering, a group of agents working in parallel, a second group reviewing what the first produced. An agent can also hand a self-contained task to another, so the graph composes instead of collapsing into one enormous prompt.
That is why I would call the potential here effectively unlimited: the ceiling is not the framework, it is how clearly you can describe each piece of the work.
Welcome to explore it - github.com/islomkhon/CerebrumKit - and I would genuinely like to hear where it breaks first.