Foundation~30 minHazelJS
Safe Writes, Approvals, and HITL
Add a refund tool the right way: side-effect classes, requiresApproval, PolicyEngine, and durableSuspend so humans can approve without hanging workers.
- Authors
- editorial-team
- Published
- Last reviewed
Direct answer
Reads can be autonomous. Irreversible writes need a human in the loop on HazelJS: requiresApproval: true, usually a PolicyEngine rule, and for production waits durableSuspend + approveAndResume.
Pattern: /patterns/human-approval-gate. Guide: /guides/add-human-approval-without-breaking-the-run.
Side-effect classes (memorize these)
| Class | Examples | Default posture |
|---|---|---|
| Read | lookupOrder, search | Autonomous, still validate args |
| Reversible write | create draft ticket | Optional approval; idempotent ids |
| Irreversible | refund, delete, prod deploy | Always approval + durable suspend |
Step 1Add a refund tool (do not auto-run it)
@Tool({
name: 'processRefund',
description: 'Refund an order — requires human approval',
requiresApproval: true,
parameters: [
{ name: 'orderId', type: 'string', required: true },
{ name: 'amount', type: 'number', required: true },
],
})
async processRefund(input: { orderId: string; amount: number }) {
// Adapter must be idempotent (same key → same outcome)
return { ok: true, ...input, status: 'refunded' };
}
Update the system prompt: “Propose refunds only when the user asks. Never invent amounts.”
Step 2Reinforce with PolicyEngine
import {
AgentRuntime,
PolicyEngine,
defaultPiiMaskPolicies,
} from '@hazeljs/agent';
const runtime = new AgentRuntime({
llmProvider,
policyEngine: new PolicyEngine([
...defaultPiiMaskPolicies(),
{
id: 'refund-needs-approval',
tool: 'processRefund',
effect: 'require_approval',
priority: 20,
},
]),
});
Prompts remind; policy enforces.
Step 3Durable wait (production shape)
Holding an HTTP worker open while a human decides is a bad idea. Prefer:
import { createDurableRunStore, AgentRuntime } from '@hazeljs/agent';
const store = createDurableRunStore('.hazel/runs');
const runtime = new AgentRuntime({
llmProvider,
durableSuspend: true,
runRepository: store.runRepository,
checkpointService: store.checkpointService,
humanTaskService: store.humanTaskService,
policyEngine, // from step 2
});
const waiting = await runtime.execute(
'ops-desk',
'Please refund ORD-1001 for 42.00'
);
// …process may restart; human reviews in your UI…
await runtime.approveAndResume(waiting.executionId, {
approved: true,
approvedBy: 'ops@example.com',
});
Glossary: /glossary/hitl, /glossary/durable-execution.
Step 4Idempotency (minimum bar)
Before enabling refunds in any shared environment:
- Pass an idempotency key (order id + amount + day) into your payments adapter
- Retries must not double-charge
- See /patterns/idempotent-tool-execution
Checkpoint
- Reads vs writes are explicit in tool design
-
processRefundhasrequiresApproval: true - You understand suspend → human →
approveAndResume
What to do next
Lock behavior with tests: /learn/agentic-development/golden-tests-with-describe-agent
Artifact: Ops Desk with lookupOrder + processRefund(requiresApproval)