Giving a human the final say sounds simple.
An AI system proposes an action:
Refund customer €2,500
The application shows:
Approve? [Yes] [No]
Someone clicks Yes. Done.
That model works well in demos.
It becomes much harder when the approval arrives two hours later, the application has restarted, three workers are processing the same queue, the original model request no longer exists in memory, and retrying the wrong piece of code could execute the action twice.
At that point, human-in-the-loop is no longer a UI feature. It is a workflow durability problem.
A production approval system must suspend execution as data, persist enough state to continue safely, and resume exactly the operation that was approved.
For Java and Spring Boot teams building consequential AI workflows, that distinction matters.
The Demo Version of Human Approval
A simple implementation might look like this:
if (decision.requiresApproval()) {
boolean approved = askHuman();
if (approved) {
paymentService.schedulePayment(payment);
}
}
There is nothing inherently wrong with this code for a synchronous toy example. But it hides several assumptions:
- The human must respond immediately.
- The application process must remain alive.
- The workflow state must remain in memory.
- Only one execution path must exist.
- The approval must still refer to the exact action originally proposed.
- And the application must somehow guarantee that the side effect is not executed twice.
Real systems rarely give us all of those guarantees.
Approval Changes the Workflow Lifecycle
Consider an AI system reviewing an invoice. The model extracts the invoice, classifies the risk, and proposes:
schedulePayment(invoice = "INV-2026-1842", amount = 18400, currency = "EUR")
Runtime policy decides that this operation cannot execute automatically:
Tool request ──► Runtime policy ──► REQUIRE_APPROVAL
At that moment, normal execution must stop.
But stopping is not enough. The system must preserve enough information to answer:
- What was being executed?
- What action was proposed?
- Which workflow requested it?
- Which policy required approval?
- What exactly is the human approving?
- How should execution continue afterwards?
The workflow enters a durable state: RUNNING ──► REQUIRE_APPROVAL ──► SUSPENDED.
The important word is suspended: not sleeping, not polling inside the request, and not holding an application thread open.
Do Not Wait for Humans on Application Threads
A human may approve an operation in ten seconds or tomorrow morning. An application thread should not care.
Blocking an application thread (waitForHumanApproval()) for 4 hours is obviously undesirable.
A durable model is:
AI workflow ──► Approval required ──► Persist suspension ──► Return
│
Time passes │
▼
Approval arrives ──► Load state ──► Resume workflow
The workflow is waiting conceptually. The application process is not.
What Must Be Persisted?
Saving approved = false is not sufficient. A durable approval workflow needs enough state to establish a secure relationship between the original execution and the future resume operation.
Conceptually, that means preserving:
- Approval Request: workflowRunId, subject, required approver role, recommendation, pending decision.
- Suspended Invocation: invocation identity, operation reference, replay identity.
- Continuation: resume metadata, arguments required to continue.
Approval must be bound to the execution it authorizes.
Approval Is Not Just Yes or No
A useful workflow model needs more than approved = true.
requestApproval()
│
├── SUSPENDED ──► stop execution
│
├── APPROVED ──► continue
│
├── DENIED ──► terminate safely
│
└── EXPIRED ──► terminate safely
Explicit states prevent dangerous code such as if (!denied) { execute(); } where an expired or unresolved approval could accidentally behave like an approval.
Fail-closed state machines are much safer than ambiguous booleans.
Approval Must Describe What the Human Is Approving
If a reviewer sees “Approve payment?” and clicks yes, what exactly was approved? A payment to whom? For how much? Using which account? Was the operation changed after the approval request was created?
A useful approval request should carry a meaningful summary:
Action: Schedule invoice payment
Invoice: INV-2026-1842
Supplier: Example Components BV
Amount: €18,400
Risk: HIGH
Reason for approval: Payment exceeds automatic authority
The approval decision must remain tied to the execution context the reviewer actually evaluated.
The Hard Problem Is Resume
Creating an approval request is easy. Safely continuing afterwards is harder.
Suppose an approval is granted at 16:30. The original server process died at 15:50 and a new deployment is running.
A robust architecture reconstructs the suspended execution from durable state:
Human approves ──► Decision persisted ──► Resume request ──► Load suspended invocation ──► Revalidate binding ──► Continue
The runtime does not depend on the original Java call stack still existing in memory.
Exactly Once Is the Goal, Not the Default
If an approval endpoint gets called twice (double click, retried webhook, message broker redelivery), both paths executing paymentService.schedulePayment(...) could perform the side effect twice.
Approval workflows need idempotency and replay protection:
Approval granted ──┬──► Resume attempt A ──┐
│ ├──► Durable state (1 wins) ──► Action executes ONCE
└──► Resume attempt B ──┘
Denial Must Be Terminal
Suppose a reviewer denies Transfer €18,400. A retry must not rediscover the old proposed action and execute it later.
The denial must become an explicit terminal workflow outcome (PENDING ──► DENIED ──► TERMINAL).
Likewise, an expired approval must not quietly become eligible for execution.
Approval Should Survive Restarts
A useful test for any human-approval architecture is simple:
Can I kill the application after suspension, restart it, and still make the correct approval decision and resume safely?
If the answer is no, the workflow is not truly durable.
Concurrency and Race Conditions
Two reviewers might attempt to decide the same approval concurrently, or two background workers might attempt to consume an approved continuation.
Durable state needs clear ownership semantics: exactly one decision transitions state, and exactly one worker continuation proceeds.
Human Approval Is Also an Audit Event
For consequential workflows, audit evidence must reconstruct the full timeline:
Model proposed payment ──► Policy required approval ──► Request created ──► Suspended ──► Reviewer approved ──► Resumed ──► Executed
Logging only the final tool call is not enough to explain how authority was granted.
Policy and Approval Should Remain Separate
Policy decides: Does this operation require approval? The approval system decides: Has the required human decision actually been granted?
Keeping them separate avoids hardcoding human-workflow mechanics into every business rule:
Policy evaluation ──► REQUIRE_APPROVAL ──► Approval workflow ──► APPROVED / DENIED / EXPIRED
The Model Should Not Approve Itself
If an LLM evaluates its own proposed payment and returns { "risk": "LOW", "approved": true }, that is a recommendation, not an authorization decision.
The authority boundary must remain outside the model:
Model ──► Recommendation ──► Runtime Policy ──► Human Authority ──► Side Effect
How TramAI Models Durable Approval
This lifecycle is one of the core problems TramAI is designed around:
Governed workflow ──► ApprovalGateway.requestApproval(...) ──► Persist state & continuation ──► SUSPENDED
Suspension does not mean keeping a thread alive. It is represented through persisted workflow state. When JDBC-backed sovereign stores are used, TramAI’s transactional approval path commits suspension records atomically.
(TramAI remains under active development. Its core sovereign approval golden path has a stronger stability boundary than surrounding control-plane features.)
What to Test
A production approval test suite should verify:
- Workflow suspends before side effects occur.
- Approval resumes the correct suspended execution.
- Denial never executes the protected action.
- Expiration remains fail-closed.
- Workflow survives process restarts while suspended.
- Duplicate approval/resume requests execute at most once.
- Concurrent decisions have one durable winner.
- Audit evidence reflects request, decision, suspension, resume, and execution.
None of these invariants require a live LLM to verify.
Human-in-the-Loop Should Be Rare and Intentional
The objective is not to put a human in every loop, but to place explicit human authority at boundaries where autonomous execution would exceed acceptable risk.
Low-risk operations remain automatic (ALLOW), forbidden operations are denied (DENY), and human attention is reserved for decisions where judgment is required (REQUIRE_APPROVAL).
Conclusion
Human-in-the-loop is not a button; it is a durable state transition in the execution lifecycle:
Probabilistic Recommendation ──► Deterministic Policy ──► REQUIRE_APPROVAL ──► Persisted Suspension ──► Human Decision ──► Replay-Safe Continuation ──► Side Effect
Once AI systems are allowed to trigger consequential actions, that distinction separates a demo from an enterprise architecture.
Building AI Workflows With Consequential Actions?
Constant Labs helps Java and Kotlin teams design governed AI workflows around real business processes, including tool authorization, durable approval, model routing, sensitive-data boundaries, recovery, and execution evidence.
- Explore our AI Software Integration Services.
- Learn about Private & Sovereign AI Deployments.
- Explore TramAI, our open-source governed AI runtime for the JVM.
- Discuss your architecture with Constant Labs →

