Build1 publisher3 min readPublished
Moving an agent definition into Postgres leaves seven lines of Python on disk
In apowerb the module the runtime imports is generated and disposable, and every instruction, tool and model string is read out of a database row at load time. The cost is a filesystem holding derived state.
The Engineer · Build desk

What happened
- Agent definitions in apowerb live as rows in Postgres, so a wording change is an UPDATE against a column instead of a commit and a deploy.
- The Python module the runtime imports is generated and disposable: seven lines, two of which do anything, the rest comments including a stale package name.
- The project is Apache-2.0, built on FastAPI and Google's Agent Development Kit, and it self-hosts with Docker Compose.
- On a stack booted from the published images, an agent created through the interface as article_demo showed up as row agent_id 1, type base, model string anthropic/claude-sonnet-4-5-20250929.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision With the editing surface an API and a form, a wording fix is in reach of anyone who can log into the UI, and it no longer passes through the release that used to gate it.
- exposure A stub can look healthy on disk while the database disagrees, and nothing surfaces the mismatch until someone opens that agent. That makes the first user the one who finds it.
- cost Postgres becomes the source of truth for behaviour. Backups, restores and environment promotion have to keep the database and the agents_pool directory in step, or the startup repair has to clean up after them.
- constraint Agent behaviour leaves version control. Reconstructing what an agent was instructed to say last Tuesday means querying a database instead of reading a diff.
The chain starts with a string literal. The generated module calls to_agent(agent_name='agent1'); to_agent strips the word "agent" off that argument, casts what is left to an integer, and uses it as the key for get_agent_details [6][5]. The rest follows from the record. The agent_tools column is parsed into a list of ids, and then load_agent_tools_functions, load_mcp_servers and load_agent_skills_toolset attach the callables [7]. Instruction, model, sub-agents, guardrails and output schema are all read at load time [8].
In the tested stack the row read article_demo while the folder and the module argument read agent1 [9][5]. The display name and the lookup key are separate. Renaming an agent in the interface rewrites a column and leaves the stub pointing at id 1 [11]. The generated file also carries a comment naming a package the project has since renamed [5]. A row can be incomplete too: the creation form accepts an empty API key, so a definition exists before any credential does and cannot answer until one is added [12].
Derived state on a filesystem drifts. The post lists three ways it happens: a volume restored without its database, one environment synced onto another so a folder goes missing, and a package rename that leaves a stub importing a module that is gone [13]. All three arrive as the same ModuleNotFoundError the first time a user talks to that agent [13].
The repair is the part I would copy. ensure_agent_modules runs inside the single bootstrap() behind the ASGI lifespan [19]. Its docstring states the contract: "Auto-repair: regenerate missing *or stale* agent.py files for every agent," called at startup "so an environment heals itself on its next restart rather than waiting for someone to save each agent by hand" [14]. Staleness is the half that takes thought, because a present-but-broken file passes an os.path.exists check [15]. What the repair tests for is the single import line every generated stub carries, and a stub somebody has genuinely customised that still imports the core is left alone [16].
The post's case for the whole design is a scale threshold. The committed file is a good default, it says, and it holds until a non-engineer needs to change a prompt, or until you are running forty agents for twelve teams and every wording fix has become a release [17]. Forty across twelve is a bit over three agents a team [20]. What pushes you over is the number of people waiting on a deploy, not the number of agents. For one team and a handful of agents, git already gives you the version history and the review. The post describes the editing surface as an API and a UI, and does not describe a version history for the rows [18][21].
What to watch
- Whether apowerb adds row-level history or an audit trail for agent edits.
- Whether the import-line staleness check survives the next package rename, since the last rename is what left stale stubs behind.
- Whether the UI gains field-level permissions, so someone editing a prompt cannot also change the agent_model column or the tools list.