Summary
Enterprises want AI assistants that can act on their systems, not only answer questions about them, and that only works if a human stays genuinely in control of what changes. This project builds that pattern for an organization’s access – control system – an agent that reads freely and writes only once a specific, authenticated person approves the exact change, because the tools reviewed either skip human approval or bolt it on as an afterthought rather than building it in.
The approach started by separating steps that are mechanical from the ones needing judgment, then wrapped that in a supervisor directing focused specialists and a pause – and – approve step built into the agent’s own execution using LangGraph’s interrupt(), chosen over CrewAI and the Microsoft Agent Framework specifically for that pause/resume control. Building in stages (shell first, then visibility, then the real agent, then the approval gate) surfaced genuine technical problems worth solving: an incompatible database check, an identity endpoint split, a nested pause that had to resume in exactly the right place.
The benefit reaches everyone touching the workflow – a simpler request experience for the requester, real visibility for the approver, a trail for compliance, and a repeatable pattern for whoever extends it next. The next step is fine – tuning – a durable store for paused approvals, a verified internal credential, a complete audit trail – and then testing the same approval – first pattern against a second real system once one’s ready.
1. Introduction
1.1 What DatabaseWorker Is
Ashwath Kumaran is three years into Psiog, working primarily in enterprise application development and technology strategy. As a member of the Technology Advisory Council, he has a direct role in where Psiog places its technology bets. He came up through Psiog’s IMPACT mentorship program, and DatabaseWorker is exactly the kind of project that program pushes toward: a real problem, not a fictional one, built far enough that the toy version’s assumptions stop holding up.
DatabaseWorker is a personal journey that Ashwath took on to answer a question that kept surfacing in conversations about agentic AI : everyone wants an assistant that can act, but most don’t have a good answer for what happens when that assistant is wrong, or malicious input tricks it, and it has already changed something. DatabaseWorker is the smallest complete version of the answer he believes in, applied to a domain concrete enough to stress – test it: user access management inside an EPC (Engineering, Procurement & Construction) organization.
The result is called DatabaseWorker, a nod to the Oracle schema the entire reference implementation is built against.
1.2 Why an Access-Control System Is
While the use case is relevant to most industries, Ashwath has imagined this use case in the EPC context corelating it with his project experiences. Who can approve a purchase order, release an engineering drawing, or sign off on a site inspection is governed by role and discipline, and getting that wrong has real safety and compliance consequences, not just an inconvenience.
Viewing this use case from the lens of four stakeholder personas –
- Requester – the employee or HR admin who wants to get this done via conversation instead of making a request via an app
- Approver – the manager who has to actually authorize it and needs to see the real change before saying yes.
- Compliance reviewer – needs an audit trail that names a person, not a service account.
- Engineer maintaining the system – needs a pattern to extend rather than a one – off hack to reverse – engineer.
2. The Problem
Most tools handle read – only queries against company data adequately. Write operation requirements (revoking a permission, reassigning a role, updating a record) expose a gap that the industry attempts to address –
Camp one keeps the assistant strictly read – only, which is safe but limits it to being a fancy search bar.
Camp two gives the assistant write access and either asks it nicely to be careful, or wraps the write in a generic “human review” step bolted on after the fact, disconnected from the actual moment of decision – this is most of the times in a separate ticketing tool the approver has to go check.
Neither of those is a real answer. The reason why this gap exists is more that the tools available to solve it properly are new and opinionated enough that reaching for one means accepting its constraints. A review of available commercial and open – source agent platforms ahead of this build found that most treat human approval as an afterthought wired up via webhooks, a side database, or operate as closed platforms that assume full control over hosting, model selection, and data model (Oracle – backed EPC role – based access control, in this case).
The actual gap: nothing Ashwath found treated “pause here, wait for a specific human, then continue exactly where you left off” as a first – class thing the agent itself does, rather than a workflow stapled on top of it. That gap is why DatabaseWorker was built.
3. Approach
3.1 Separating the Mechanical Steps from the Judgment Calls
Before touching a framework, Ashwath mapped the workflow as a sequence of steps and identified which of those steps actually need a language model. Looking up a user, checking a role, writing an update – these have exactly one correct path and zero ambiguity. Running that through an LLM adds latency, cost, and a small but real chance of the model getting creative where creativity isn’t wanted. The part that actually needed AI was one layer up – understanding a request phrased in plain English, deciding which part of the system it belongs to, and recognizing when a step is sensitive enough that a human has to be the one who says yes. That split – deterministic code for the mechanical steps, an agent for interpretation and routing, a human for anything irreversible – is the core idea everything else in this build follows from.3.2 A Supervisor and Focused Specialists
Instead of one agent trying to do everything, a supervisor hands each request to a focused specialist and waits for it to finish before deciding what’s next. A single do – everything agent is faster to stand up, but a prompt carrying five unrelated responsibilities becomes harder to change safely, and every new feature risks breaking something adjacent. A supervisor model keeps each specialist narrow, so adding a capability means adding a specialist, not re – negotiating one enormous prompt.3.3 Human-in-the-Loop as a First-Class Step
Every write pauses for a specific person’s approval as part of the agent’s own execution, not as a step added around it afterward. When the agent decides to change something, it stops at that exact point, and only continues once a named, authenticated person has said yes or no.// tools/oracleWrites.js – every write tool follows this shape
function requestApproval(action, summary, details) {
return interrupt({ type: “approval”, action, summary, details });
}
export const removeRolePermission = tool(async ({ roleId, permissionId }) => {
const decision = requestApproval(
“remove_role_permission”,
`Remove permission ${permissionId} from role ${roleId}`,
{ roleId, permissionId }
);
if (!decision?.approved) {
return `Cancelled: ${decision?.reason ?? “not approved”}`;
}
await oracleClient.revokePermissionFromRole(roleId, permissionId);
return `Done. Approved by ${decision.approver}.`;
});
3.4 Why LangGraph
The shortlist came down to LangGraph, CrewAI, and the Microsoft Agent Framework. CrewAI reads naturally if you’re thinking in terms of roles collaborating on a task – a “researcher” and a “writer” style setup – and it gets you to a working demo fast. The Microsoft Agent Framework leans hard into enterprise identity and Azure – native tooling, which is a real strength if that’s already your stack. LangGraph asks more upfront – you define the exact states a conversation can be in and where it’s allowed to pause – but that’s precisely the control this problem needed.3.5 Trade-offs, Stated Upfront
This is a personal, reference – scale build. The trade – offs below are therefore deliberate, and not oversights.- The paused, awaiting – approval state currently lives in memory, not a durable store – fine for proving the pattern out, but wouldn’t survive a restart in anything running for real.
- The internal hop between the web gateway and the agent service currently relies on network isolation rather than its own verified credential.
4. Tool and Technology Choices
Each layer has real alternatives. The table below documents what was chosen, what was considered, and the specific variable that decided it.Layer | Chosen | Alternative Considered | The Variable That Decided It |
Orchestration | LangGraph | CrewAI, Microsoft Agent Framework | Explicit pause/resume control over a running agent, not just fast time - to - first - demo |
Model access | OpenRouter | Direct provider API (OpenAI, Anthropic, etc.) | Swapping the underlying model is a config change, not a code change - matters for something meant to be adapted, not locked to one vendor |
Observability | Langfuse (self - hosted) + LangSmith (managed), switchable | Pick one | Where the trace data should live changes by stage: self - hosted for local dev and any data - residency constraint, managed once a team doesn't want to operate that infrastructure |
Identity | Keycloak + a backend - for - frontend holding the token server - side | Custom auth, or a token issued straight to the browser | A stolen session reference is far less useful to an attacker than a stolen token - worth the extra setup cost |
5. Architecture, Request Flow, and Build Journey
The diagrams below cover the container layout, the exact sequence a write goes through (pause included), the order the build followed, and the three problems that required real thought rather than routine setup.
5.1 System Layout
Six services, one internal – only. The agent service is never reached directly by the browser – only the gateway can reach it, and only the gateway holds real identity tokens.
5.2 The Write Path: Where the Pause Happens
Reads never stop. A write always does, and it remains paused until a real person reviews and decides.
Why this matters: the approving identity is read from the authenticated session at resume time, and not from anything the request itself claims. So the record of who signed off can’t be spoofed by the client.
5.3 How It Actually Got Built
There was no fixed calendar, but a clear order – each stage had to hold up before the next one built on it. The friction points that took real thought to get past, rather than routine setup, are covered in Section 6.
On tooling: most of the mechanical code, including the first pass at this document, was written with AI – assisted tools.
6. Three Problems That Needed a Person
The order of the build was deliberate – get the chat UI and login working first with nothing behind them, wire in tracing before adding any agent logic (to avoid debugging blind), then build the specialist that touches the database, and only once reads were solid, add the approval gate on writes. Three problems in this build needed a person paying close attention, not a tool summarizing the situation:- Oracle’s missing boolean type. Oracle 21c, the database version this targets, has no native true/false type, and a common EF Core existence – check pattern emits SQL Oracle flatly rejects. Every “does this already exist” check in the codebase had to be hand – written around that – something that only shows up once you’ve actually hit the error, not by reading documentation upfront.
- Keycloak’s split identity. Keycloak needed to hand out a different address for its own endpoints depending on who was asking – a browser versus a service inside the same Docker network – and reconciling that took real untangling rather than a config flag.
- Nested pause and resume. Getting interrupt() to correctly pause and resume from inside a nested specialist agent, not just the top – level supervisor, took care. The state has to propagate up through the right configuration object, or the resume silently starts the conversation over instead of continuing it.
7. Impact and Benefit
DatabaseWorker has not run in a live environment yet, so no production metrics are available. What is clear is what changes for each person touching the workflow:- Requester – asks in plain language instead of navigating an admin console they might open twice a year.
- Approver – sees the exact change before it happens, in one place, instead of a separate ticket to chase down.
- Compliance reviewer – gets a decision trail tied to a named person, in principle; in the current build this holds for role assignment specifically, and closing that gap for every write type is on the list below.
- Engineer extending the system – adds a new capability by following an established pattern (new tool, new specialist, wire it into the router) instead of re – deciding how approval should work each time.
8. What’s Next
In order of priority:- Make the pause durable. Move the paused – approval state out of memory and into Postgres or Redis, so a restart mid – approval doesn’t lose it.
- Close the audit gap. Right now the audit log always records the literal string “API” as the actor, regardless of who actually approved a change. The real approver only survives today for one specific write path. Every write should carry it.
- Authenticate the internal hop. The call from the web gateway to the agent service should carry its own verified credential instead of relying on the fact that nothing else can reach it on the network.
- Move past docker – compose. This runs as a local multi – container stack today. A managed deployment target, and a managed observability service instead of a self – hosted one, are the natural next step once this needs to stay up on its own.
- Generalize past this one database. The pattern (agent reads freely, writes pause for a named human) was built to extend to other systems of record.