03. HITL Approval Workflow — Review, Modify, and Execute Proposals
Learning Objectives
By the end of this tutorial, you will be able to:
- Understand the purpose and structure of HITL (Human-In-The-Loop) approval
- Analyze the JSON structure of an approval record
- Accept, modify, or reject individual approvals
- Modify tool parameters for safer execution
- Use tool filters and run ID filters for batch approvals
- Apply handling policies based on risk level (low/medium/high)
- Understand MCP source activation approvals (
kind: source_enable) and override sources - Read the approval audit trail
Prerequisites
- Workspace initialization complete (
euleragent init) - At least one agent created:
euleragent new my-assistant --template personal-assistant
euleragent new code-agent --template code-assistant
What Is HITL Approval?
HITL (Human-In-The-Loop) is a mechanism where the agent obtains human review and approval before performing dangerous operations.
euleragent follows the deny-all principle, and the following tools always require human approval:
| Tool | Risk Level | Reason |
|---|---|---|
file.write |
medium | File system modification |
file.delete |
high | Permanent data deletion |
shell.exec |
high | Arbitrary code execution |
web.search |
medium | External network communication |
web.fetch |
medium | External URL access |
git.commit |
medium | Codebase modification |
git.push |
high | Remote repository modification |
email.send |
high | External email dispatch |
payment.charge |
high | Financial transaction |
llm.external_call |
high | External LLM API cost |
In addition to tool calls, euleragent also supports system-level approvals:
| Approval Kind | Risk Level | Reason |
|---|---|---|
source_enable |
medium | MCP source activation — connecting to external search services |
llm_profile_enable |
high | External LLM profile activation — when using external LLMs via --llm-plan / --llm-final |
Note:
kind: source_enableapprovals are automatically generated when SearchRouter tries to activate a new MCP source.kind: llm_profile_enableapprovals are auto-generated by the Runner when you specify anis_external: trueprofile via--llm-plan/--llm-final. The run continues even without approval (falling back to the local default provider), and after approval, the external profile takes effect from the next run. For detailed hands-on practice with MCP source approval, see 08_mcp_provider_and_tools.md. For the complete external LLM profile approval cycle, see 09_scoped_llm_profile.md.
Step-by-Step Guide
Step 1: Generate Pending Approvals
Run a task that includes multiple tool calls for approval practice:
euleragent run my-assistant \
--task "report.md 파일을 만들고, AI 에이전트 프레임워크를 웹에서 검색한 후, 결과를 파일에 저장해줘" \
--mode plan \
--max-loops 3
Expected output:
Run a1b2c3d4e5f6 started (agent: my-assistant, mode: plan)
[loop 1/3] Generating plan...
→ Proposed: web.search (risk: medium)
query: "AI agent framework 2025 comparison"
[loop 2/3] Continuing...
→ Proposed: file.write (risk: medium)
path: report.md
Run a1b2c3d4e5f6 completed (state: PENDING_APPROVAL)
2 approval(s) pending.
Use: euleragent approve list --run-id a1b2c3d4e5f6
Step 2: Check the Approval List
euleragent approve list
Expected output:
Pending approvals (2):
ID TOOL RISK RUN STATUS CREATED
apv_w1x2y3 web.search medium a1b2c3d4e5f6 pending 10:30:00
apv_p4q5r6 file.write medium a1b2c3d4e5f6 pending 10:30:02
To see approvals for a specific run only:
euleragent approve list --run-id a1b2c3d4e5f6
To see approvals for a specific tool only:
euleragent approve list --tool web.search
Step 3: View Approval Record Details
Review all fields of an approval record:
euleragent approve show apv_w1x2y3
Expected output:
{
"id": "apv_w1x2y3",
"run_id": "a1b2c3d4e5f6",
"tool_name": "web.search",
"params": {
"query": "AI agent framework 2025 comparison",
"top_k": 5
},
"risk_level": "medium",
"side_effects": ["external_network"],
"status": "pending",
"created_at": "2026-02-23T10:30:00Z",
"agent": "my-assistant",
"loop": 1,
"context": "Task: report.md 파일을 만들고 AI 에이전트 프레임워크를 웹에서 검색..."
}
Meaning of each field:
| Field | Description |
|---|---|
id |
Unique identifier for the approval record |
run_id |
The run ID that generated this approval |
tool_name |
Name of the tool to be executed |
params |
Parameters to be passed to the tool |
risk_level |
Risk level (low/medium/high) |
side_effects |
List of expected side effects |
status |
Current status (pending/accepted/rejected/executed) |
created_at |
Approval record creation time |
agent |
The agent that generated this approval |
loop |
Agentic loop number |
context |
Context in which this approval was generated |
Step 4: Accept an Individual Approval
Accept after reviewing the content:
euleragent approve accept apv_w1x2y3 --actor "user:you"
Expected output:
Accepted: apv_w1x2y3 (web.search)
Status: accepted (not yet executed)
Use --execute flag to execute immediately.
Accepting without --execute changes the status to accepted but does not actually execute yet. You can batch execute later or execute explicitly.
To execute immediately:
euleragent approve accept apv_w1x2y3 --actor "user:you" --execute
Expected output:
Accepted and executed: apv_w1x2y3 (web.search)
Result: 5 search results retrieved
Stored in: .euleragent/runs/a1b2c3d4e5f6/tool_calls.jsonl
Required: You must provide approver information via the
--actorflag or theEULERAGENT_ACTORenvironment variable (deny-all-default policy):bash euleragent approve accept apv_w1x2y3 --actor "user:alice" --executeSetting theEULERAGENT_ACTORenvironment variable eliminates the need to type--actorevery time.
Step 5: Modify Parameters Before Approving
You can modify tool parameters before approving. For example, if a search query is too broad, make it more specific:
euleragent approve accept apv_w1x2y3 \
--actor "user:you" \
--edit-params '{"query": "open source AI agent framework Python 2025", "top_k": 3}'
Expected output:
Accepted with modified params: apv_w1x2y3 (web.search)
Original query: "AI agent framework 2025 comparison"
Modified query: "open source AI agent framework Python 2025"
top_k: 5 → 3
Modify parameters and execute immediately:
euleragent approve accept apv_w1x2y3 \
--actor "user:you" \
--edit-params '{"query": "open source AI agent framework Python 2025", "top_k": 3}' \
--execute
When to use --edit-params:
- When the search query contains sensitive information
- When changing the file save path to a safer location
- When parameters are too broad and need to be narrowed
- When adjusting LLM-generated parameters for better accuracy
Step 6: Reject an Approval
Dangerous or inappropriate tool calls can be rejected:
euleragent approve reject apv_p4q5r6 --actor "user:you" --reason "File path is not safe. Must save under /tmp/."
Expected output:
Rejected: apv_p4q5r6 (file.write)
Reason: File path is not safe. Must save under /tmp/.
Status: rejected
The rejection reason is recorded in the audit log for later tracking. Rejected approvals are not executed, and the agent can check this fact in subsequent runs.
Step 7: Batch Accept — Run ID Filter
Accept all approvals from the same run at once:
euleragent approve accept-all --run-id a1b2c3d4e5f6 --actor "user:you"
Expected output:
Accepted 2 approval(s) for run a1b2c3d4e5f6.
[accepted] web.search (apv_w1x2y3)
[accepted] file.write (apv_p4q5r6)
Status: accepted (not yet executed)
Including immediate execution:
euleragent approve accept-all --run-id a1b2c3d4e5f6 --actor "user:you" --execute
# Batch accept with actor information
euleragent approve accept-all --run-id a1b2c3d4e5f6 --actor "ops:batch-review" --execute
Expected output:
Accepted and executed 2 approval(s) for run a1b2c3d4e5f6.
[OK] web.search (apv_w1x2y3) — 5 results retrieved
[OK] file.write (apv_p4q5r6) — report.md created (1.2KB)
Executed 2/2 successfully.
Run a1b2c3d4e5f6 state: RUN_FINALIZED
Step 8: Batch Accept — Tool Filter
Process only approvals for a specific tool type:
# Accept only web.search approvals
euleragent approve accept-all --tool web.search --actor "user:you" --execute
# Accept only file.write approvals (across all runs)
euleragent approve accept-all --tool file.write --actor "user:you"
# Accept only web.search approvals for a specific run
euleragent approve accept-all --run-id a1b2c3d4e5f6 --tool web.search --actor "user:you" --execute
Expected output:
Accepted 1 approval(s) matching tool=web.search.
[OK] web.search (apv_w1x2y3) — executed
1 approval(s) remain pending (different tool type).
Step 9: Batch Reject
Reject all approvals for a risky run:
euleragent approve reject-all --run-id a1b2c3d4e5f6 --actor "user:you" --reason "Security review required"
Expected output:
Rejected 2 approval(s) for run a1b2c3d4e5f6.
[rejected] web.search (apv_w1x2y3)
[rejected] file.write (apv_p4q5r6)
Reason: Security review required
Step 10: Handling Policies by Risk Level
Adjust review intensity based on risk level.
Low Risk Tools
Tools like file.read, memory.search, and git.status are not included in require_approval by default and are auto-executed. No separate approval is needed.
Medium Risk Tools
Tools like file.write, web.search, and git.commit are included in require_approval and require approval. In most cases, review the content and accept.
Recommended review items:
- file.write: Verify the save path is within the workspace
- web.search: Check that the query contains no sensitive information
- git.commit: Verify the commit message and changed files are as intended
High Risk Tools
Tools like shell.exec, file.delete, email.send, payment.charge, and git.push should always be reviewed carefully.
# Example: view shell.exec approval details
euleragent approve show apv_s1h2e3
{
"id": "apv_s1h2e3",
"tool_name": "shell.exec",
"params": {
"command": "rm -rf ./old_data/",
"working_dir": "/home/user/my-project"
},
"risk_level": "high",
"side_effects": ["filesystem_modification", "irreversible"],
"status": "pending"
}
High risk tool review checklist: - [ ] Is the command/action what was expected? - [ ] Is the scope limited to what was intended? - [ ] Is this an irreversible operation? (Is there a backup?) - [ ] Does the data being sent externally contain sensitive information?
Step 10-1: MCP Source Activation Approval (kind: source_enable)
When the agent calls web.search, the internal SearchRouter evaluates candidate sources from mcp.sources in workspace.yaml. When activating a new source for the first time, a kind: source_enable approval is generated.
Viewing a Source Activation Approval Record
euleragent approve show apv_src_t1v2
Expected output:
{
"id": "apv_src_t1v2",
"run_id": "a1b2c3d4e5f6",
"kind": "source_enable",
"tool_name": "web.search",
"params": {
"query": "AI agent framework 2025 comparison"
},
"resolved_source_id": "brave",
"candidate_sources": ["brave", "tavily", "local_kb"],
"routing_reason": "brave scored highest for broad web queries (score: 0.91)",
"risk_level": "medium",
"side_effects": ["external_network", "api_key_usage"],
"status": "pending",
"created_at": "2026-02-23T10:30:00Z",
"agent": "my-assistant",
"loop": 1,
"context": "SearchRouter selected 'brave' from source-set 'default'"
}
Additional fields compared to regular tool approval records:
| Field | Description |
|---|---|
kind |
Approval type — source_enable is an MCP source activation approval |
resolved_source_id |
The final source ID selected by SearchRouter |
candidate_sources |
List of evaluated candidate sources |
routing_reason |
Reason why SearchRouter selected this source |
Source Override — Switch to a Different Source
If the source selected by SearchRouter is not appropriate, you can specify a different one with --edit-params:
euleragent approve accept apv_src_t1v2 \
--actor "user:you" \
--edit-params '{"resolved_source_id": "tavily"}' \
--execute
Expected output:
Accepted with modified params: apv_src_t1v2 (source_enable)
Original source: brave
Override source: tavily
Executed: web.search via tavily — 5 results retrieved
This way, humans maintain ultimate control over which external service is used. Only sources included in candidate_sources can be specified as override targets.
Tip: To always prioritize a specific source, combine
--source-setwith the source priority settings inworkspace.yaml. This reduces the need for manual overrides each time.
Step 11: Verify the Audit Trail
All approval activities are recorded in the audit log:
euleragent logs a1b2c3d4e5f6
Expected output:
Run: a1b2c3d4e5f6
State: RUN_FINALIZED
Approvals:
apv_w1x2y3 web.search accepted 2026-02-23 10:30:30 by=user:alice
apv_p4q5r6 file.write accepted 2026-02-23 10:30:32 by=user:alice
Tool Executions:
web.search apv_w1x2y3 OK 3 results
file.write apv_p4q5r6 OK report.md (1.2KB)
Raw approval log file:
cat .euleragent/runs/a1b2c3d4e5f6/approvals.jsonl
{"id": "apv_w1x2y3", "tool_name": "web.search", "status": "accepted", "executed": true, "executed_at": "2026-02-23T10:30:30Z"}
{"id": "apv_p4q5r6", "tool_name": "file.write", "status": "accepted", "executed": true, "executed_at": "2026-02-23T10:30:32Z"}
Complete Approval Workflow Diagram
euleragent run <agent> --task "..." --mode plan
│
▼
[LLM proposes tool calls]
│
├─ Regular tool call ─────────────────┐
│ │
├─ web.search call ──┐ │
│ ▼ │
│ [SearchRouter] │
│ Evaluate & select │
│ source │
│ │ │
│ New source activation?│
│ ├─ Yes ──▶ [kind: source_enable approval generated]
│ └─ No │
│ │ │
▼ ▼ ▼
[Approval record created] → saved to .euleragent/approvals/
{
"id": "apv_xxx",
"kind": "tool_call | source_enable",
"tool_name": "web.search",
"params": {...},
"resolved_source_id": "brave", ← for source_enable
"candidate_sources": ["brave", ...], ← for source_enable
"routing_reason": "...", ← for source_enable
"risk_level": "medium",
"status": "pending"
}
│
▼
[Human reviews] euleragent approve list / show
│
┌──────┴──────┐
│ │
Accept Reject
│ │
--edit-params --reason
(optional) │
│ ▼
▼ [rejected] → recorded in audit log
--execute
(optional)
│
▼
[Tool executed] → recorded in tool_calls.jsonl
│
▼
[Result added to context]
│
▼
[Next loop or complete]
Expected Output Summary
# Approval list
$ euleragent approve list
Pending approvals (2):
apv_w1x2y3 web.search medium a1b2c3d4e5f6 pending
apv_p4q5r6 file.write medium a1b2c3d4e5f6 pending
# Individual accept + execute
$ euleragent approve accept apv_w1x2y3 --actor "user:you" --execute
Accepted and executed: apv_w1x2y3 (web.search)
Result: 5 search results retrieved
# Parameter modification + accept
$ euleragent approve accept apv_w1x2y3 --actor "user:you" --edit-params '{"query": "..."}' --execute
Accepted with modified params: apv_w1x2y3 (web.search)
# Reject
$ euleragent approve reject apv_p4q5r6 --actor "user:you" --reason "Path not safe"
Rejected: apv_p4q5r6 (file.write)
# Batch accept + execute
$ euleragent approve accept-all --run-id a1b2c3d4e5f6 --actor "user:you" --execute
Accepted and executed 2/2 successfully.
FAQ / Common Errors
Q: Where are approval records stored as JSONL files?
Approval records are stored in two locations:
1. .euleragent/approvals/ — The unified approval queue (managed regardless of run ID)
2. .euleragent/runs/<run-id>/approvals.jsonl — Approval records for a specific run
# Full approval queue
ls .euleragent/approvals/
# Approval records for a specific run
cat .euleragent/runs/a1b2c3d4e5f6/approvals.jsonl
Q: What happens if an approved tool fails during execution?
On execution failure, the error is recorded in tool_calls.jsonl and the approval status changes to executed_failed. Execution continues, but the agent's subsequent output may be incomplete since the tool's result is missing.
# Check failed tools
cat .euleragent/runs/<run-id>/tool_calls.jsonl | grep "status.*failed"
Q: How do I handle quotes when writing JSON in --edit-params?
When passing JSON in the shell, wrap the entire argument in single quotes and use double quotes inside:
euleragent approve accept apv_xxx \
--actor "user:you" \
--edit-params '{"query": "LangChain vs CrewAI 2025", "top_k": 3}'
Escaping is different on Windows PowerShell:
euleragent approve accept apv_xxx `
--actor "user:you" `
--edit-params '{\"query\": \"LangChain vs CrewAI 2025\", \"top_k\": 3}'
Q: I accidentally approved and executed with the wrong parameters. Can I undo it?
The results of executed tools cannot be undone (especially file.write or shell.exec). This is why Plan mode and --edit-params are recommended. For file system changes, the change history is recorded in the diffs/ directory:
ls .euleragent/runs/<run-id>/diffs/
cat .euleragent/runs/<run-id>/diffs/report.md.diff
Q: I ran euleragent approve accept-all but some approvals were not processed.
A --run-id or --tool filter may be applied, processing only approvals matching those conditions. Also, approvals already in another state (accepted/rejected) are excluded from processing.
# Check all pending approvals (no filter)
euleragent approve list
# Check all pending approvals for a specific run
euleragent approve list --run-id a1b2c3d4e5f6
Q: How do I completely block high-risk tools for a specific agent?
Add them to the denylist in the agent's tools.yaml:
# .euleragent/agents/my-assistant/tools.yaml
policy: deny-all
denylist:
- shell.exec
- file.delete
- email.send
- payment.charge
Or in agent.yaml:
# .euleragent/agents/my-assistant/agent.yaml
tools_denylist:
- shell.exec
- file.delete
Q: Typing --actor every time is tedious.
Set the EULERAGENT_ACTOR environment variable to have actor information recorded automatically without the --actor flag:
# Add to shell profile (~/.bashrc, ~/.zshrc, etc.)
export EULERAGENT_ACTOR="user:$(whoami)"
# Or in CI/CD pipelines
export EULERAGENT_ACTOR="ci:${CI_PIPELINE_ID}"
If the --actor flag is explicitly provided, it takes precedence over the environment variable.
Common Mistakes (Out-of-Order Steps)
| Symptom | Cause | Fix |
|---|---|---|
Error: Approval 'X' not found. |
Incorrect approval ID | euleragent approve list |
Error: Approval 'X' is already accepted. |
Attempted to re-accept an already accepted approval | euleragent approve list --run-id <id> |
Error: Actor identity is required |
--actor missing |
--actor "user:name" or export EULERAGENT_ACTOR="..." |
Next: euleragent workflow resume <id> |
Auto-hint after accept-all | Run the command shown in the hint |
Next step: 04_task_file_and_batch.md — Learn about task files, variable substitution, and batch research.