Human-in-the-Loop Review
Require a person to approve a task's output before the next task consumes it
Require a person to sign off on a task’s output before the next task can read it.
Quick Start
- Mark a task for review
Set
humanInput: trueon the task, then callreviewTaskOutputafter the task produces its output.typescript import { Task, reviewTaskOutput } from 'praisonai'; const task = new Task({ description: 'Draft a customer refund reply', expected_output: 'A one-paragraph reply', humanInput: true, humanReviewPrompt: 'Is this legally safe to send?', }); const output = '...draft reply...'; const outcome = await reviewTaskOutput(task, output); if (!outcome.approved) { // Re-run with outcome.reason folded back into the prompt } - Plug in your own approver
Pass an
approvalManagerto route the review through Slack, a webhook, or any UI instead of the built-in terminal prompt.typescript import { Task, reviewTaskOutput } from 'praisonai'; const task = new Task({ description: 'Draft a customer refund reply', expected_output: 'A one-paragraph reply', humanInput: true, }); const output = '...draft reply...'; const outcome = await reviewTaskOutput(task, output, { approvalManager: { async requestApproval({ toolName, input, reason }) { return await askHumanAndReturnTrueOrFalse(reason, input); }, }, timeout: 300, // seconds — forwarded to the approval manager });
How It Works
reviewTaskOutput asks the approval manager to approve the complete output, then returns a verdict.
| Rule | Behaviour |
|---|---|
| Opt-in | When humanInput is falsy, reviewTaskOutput returns { approved: true } and never calls the manager. Callers need no if of their own. |
| No silent skip | With humanInput: true and no manager available, the function throws — a task that was supposed to be signed off and simply was not is the exact failure this prevents. |
| Complete output | The reviewer always sees the full output, never a prefix, so no unreviewed trailing content reaches the next task. |
| Strict approval | Only a literal true counts as approval. A truthy non-boolean (e.g. { approved: false }) is treated as a rejection. |
| Rejection reason | A rejection returns reason: "a reviewer rejected this output" so you can feed it back into a re-run. |
| Custom prompt | humanReviewPrompt is passed to the manager as reason; otherwise a default question is built from the task name. |
| Audit label | The toolName sent to the manager is task_output:<task.name ?? 'task'>, so approval rules and audit trails tell task-output reviews apart from tool-call reviews. |
Approval vs. Human Review vs. Guardrails
Three features gate agent work at different points — pick by what you are protecting.
The approval manager gates a TOOL CALL and guardrails validate automatically; neither let an orchestrator require a person to approve an output before the next task consumes it.
| Feature | Gates | Who decides |
|---|---|---|
| Approval | A tool call before it runs | A person |
| Human Review | A task’s output before the next task reads it | A person |
| Guardrails | A task’s output | Automatic validation |
Task Options
Two Task fields turn on review.
| Option | Type | Default | Description |
|---|---|---|---|
humanInput | boolean | false | Require a person to approve this task’s output before the next task consumes it. |
humanReviewPrompt | string | undefined | Custom question the reviewer is asked. Falls back to Approve the output of task <name>?. |
reviewTaskOutput Reference
`reviewTaskOutput(task, output, options?) → Promise
`
| Parameter | Type | Description |
|---|---|---|
task | ReviewableTask | { name?, humanInput?, humanReviewPrompt? } — usually your Task. |
output | unknown | The complete output to review. |
options.approvalManager | ReviewApprovalManager | Override the manager. Defaults to the global one from getApprovalManager(). |
options.timeout | number | Seconds to wait, forwarded to the manager. |
ReviewOutcome return shape:
| Field | Type | Description |
|---|---|---|
approved | boolean | true when the reviewer accepted the output. |
reason | string | Present on rejection, to fold back into a re-run. |
A ReviewApprovalManager is any object with a requestApproval that resolves to a boolean:
import { Task, reviewTaskOutput, type ReviewApprovalManager } from 'praisonai';
const manager: ReviewApprovalManager = {
async requestApproval({ toolName, input, reason, timeout }) {
// toolName === `task_output:<task name>`
return await askHuman(reason);
},
};
const task = new Task({
description: 'Draft a reply',
expected_output: 'A one-paragraph reply',
humanInput: true,
});
const outcome = await reviewTaskOutput(task, '...draft...', { approvalManager: manager });Best Practices
Feed the reason back into a re-run
A rejection returns reason: "a reviewer rejected this output". Add it to the task’s prompt and re-run so the next draft addresses the objection.
Register a manager before running headless
In non-TTY environments the default prompt cannot read stdin. Pass options.approvalManager (Slack, webhook, dashboard) so the throw never fires.
Return a strict boolean
Only === true approves. Make sure your requestApproval resolves an actual boolean — a truthy object is read as a rejection on purpose.
Reach for the right gate
Use Approval for tool calls, Human Review for outputs a person must sign off, and Guardrails for automatic output validation.