Output Review
Pause after step execution to review, edit, or reject output before it flows downstream.
Output review pauses the workflow after a step executes, letting a human inspect the output before it continues to the next step. This complements pre-execution confirmation, which pauses before a step runs.
Configure it with human_review=HumanReview(...) on the primitive. Flat parameters on Step (for example requires_output_review=True) still work for backward compatibility.
Supported on Step, Router, and Loop (via requires_iteration_review on HumanReview).
1from kern.workflow import Workflow, OnReject2from kern.workflow.step import Step3from kern.workflow.types import HumanReview4from kern.db.sqlite import SqliteDb56workflow = Workflow(7 name="email_workflow",8 db=SqliteDb(db_file="workflow.db"),9 steps=[10 Step(11 name="draft_email",12 agent=draft_agent,13 human_review=HumanReview(14 requires_output_review=True,15 output_review_message="Review the email draft before sending",16 on_reject=OnReject.cancel,17 ),18 ),19 Step(name="send_email", agent=send_agent),20 ],21)2223run_output = workflow.run("Draft an email about the Friday standup")2425if run_output.is_paused:26 for req in run_output.steps_requiring_output_review:27 print(req.step_output.content)2829 if user_approves():30 req.confirm()31 else:32 req.reject()3334 run_output = workflow.continue_run(run_output)The step executes, then the workflow pauses with the full output available in req.step_output. The reviewer calls confirm(), reject(), or edit() before resuming.
Parameters
Pass these fields inside HumanReview. See HumanReview Config for the full field list, defaults, and validation rules.
| Field | Type | Default | Description |
|---|---|---|---|
requires_output_review | bool | Callable[[StepOutput], bool] | False | Pause after execution for review. Pass a callable for conditional review |
output_review_message | str | None | Message shown to the reviewer |
on_reject | OnReject | OnReject.skip | Action on rejection: skip, cancel, retry, else_branch |
max_retries | int | 3 | Maximum retry attempts when on_reject=OnReject.retry |
timeout | int | None | Seconds before auto-resolving. See Timeout |
on_timeout | OnTimeout | None | Action when timeout expires. See Timeout |
Reviewer Actions
| Method | Effect |
|---|---|
req.confirm() | Accept output as-is. Continues to next step |
req.reject() | Reject output. Behavior depends on on_reject |
req.reject(feedback="...") | Reject with feedback. Feedback is sent to the agent on retry |
req.edit("new output") | Accept with modifications. Edited output replaces original |
Reject with Retry
Set on_reject=OnReject.retry to re-execute the step when a reviewer rejects. Pair with reject(feedback=...) to send the reviewer's feedback to the agent on the next attempt.
1from kern.workflow import Workflow, OnReject2from kern.workflow.step import Step3from kern.workflow.types import HumanReview4from kern.db.sqlite import SqliteDb56workflow = Workflow(7 name="email_review_workflow",8 db=SqliteDb(db_file="workflow.db"),9 steps=[10 Step(11 name="draft_email",12 agent=draft_agent,13 human_review=HumanReview(14 requires_output_review=True,15 output_review_message="Review the email draft",16 on_reject=OnReject.retry,17 max_retries=3,18 ),19 ),20 Step(name="send_email", agent=send_agent),21 ],22)2324run_output = workflow.run("Draft an email about the Friday standup")2526while run_output.is_paused:27 for req in run_output.steps_requiring_output_review:28 print(f"Attempt {req.retry_count + 1}:")29 print(req.step_output.content)3031 if user_approves():32 req.confirm()33 else:34 feedback = input("What should change? ")35 req.reject(feedback=feedback)3637 run_output = workflow.continue_run(run_output)The feedback string is injected into the agent's message as "Feedback from reviewer: ..." on the next execution. Without feedback, the step simply re-runs with the same input.
Retry Behavior
| Scenario | Result |
|---|---|
Reject with on_reject=OnReject.retry | Step re-executes. Previous output is removed from collected outputs |
| Reject with feedback | Feedback is passed to the agent. retry_count increments |
max_retries exhausted | Step is skipped (treated as final rejection) |
Edit Output
Accept with modifications. The edited content replaces the original step output before it flows to the next step. Use this when the fix is minor and a full retry would be wasteful.
1run_output = workflow.run("Draft an email about the Friday standup")23if run_output.is_paused:4 for req in run_output.steps_requiring_output_review:5 print(req.step_output.content)67 choice = input("[a]pprove / [e]dit / [r]eject: ").strip().lower()89 if choice == "a":10 req.confirm()11 elif choice == "e":12 edited = input("Enter corrected output: ")13 req.edit(edited)14 else:15 req.reject()1617 run_output = workflow.continue_run(run_output)edit() sets confirmed=True and stores the edited content. On resume, the edited output replaces the original in collected outputs.
Conditional Review
Pass a callable to requires_output_review inside HumanReview to evaluate at runtime whether review is needed. The predicate receives the StepOutput and returns True to pause or False to auto-approve.
1from kern.workflow import Workflow, OnReject2from kern.workflow.step import Step3from kern.workflow.types import HumanReview, StepOutput4from kern.db.sqlite import SqliteDb56def needs_review(step_output: StepOutput) -> bool:7 """Only review outputs longer than 200 characters."""8 content = str(step_output.content) if step_output.content else ""9 return len(content) > 2001011workflow = Workflow(12 name="conditional_review_workflow",13 db=SqliteDb(db_file="workflow.db"),14 steps=[15 Step(16 name="draft_email",17 agent=draft_agent,18 human_review=HumanReview(19 requires_output_review=needs_review,20 output_review_message="Long email detected. Review before sending",21 on_reject=OnReject.retry,22 max_retries=2,23 ),24 ),25 Step(name="send_email", agent=send_agent),26 ],27)This avoids the all-or-nothing problem: review every output (expensive) or review none (risky). Common predicates:
| Condition | Example |
|---|---|
| Output length | len(str(output.content)) > 200 |
| Contains sensitive keywords | "password" in str(output.content).lower() |
| Confidence score | output.metrics.get("confidence", 1.0) < 0.8 |
| Random sampling | random.random() < 0.1 for 10% review rate |
Full Review Loop
The complete pattern with all three reviewer actions:
1from kern.workflow import Workflow, OnReject2from kern.workflow.step import Step3from kern.workflow.types import HumanReview4from kern.db.sqlite import SqliteDb56workflow = Workflow(7 name="review_workflow",8 db=SqliteDb(db_file="workflow.db"),9 steps=[10 Step(11 name="draft",12 agent=draft_agent,13 human_review=HumanReview(14 requires_output_review=True,15 output_review_message="Review the draft",16 on_reject=OnReject.retry,17 max_retries=3,18 ),19 ),20 Step(name="publish", agent=publish_agent),21 ],22)2324run_output = workflow.run("Write a client email")2526while run_output.is_paused:27 for req in run_output.steps_requiring_output_review:28 print(req.step_output.content)2930 choice = input("[a]pprove / [r]eject / [e]dit: ")31 if choice == "a":32 req.confirm()33 elif choice == "r":34 feedback = input("What should change? ")35 req.reject(feedback=feedback)36 elif choice == "e":37 edited = input("Enter corrected output: ")38 req.edit(edited)3940 run_output = workflow.continue_run(run_output)StepRequirement Properties
When a step pauses for output review, the StepRequirement includes:
| Property | Type | Description |
|---|---|---|
step_name | str | Name of the paused step |
step_output | StepOutput | The step's output for review |
output_review_message | str | Message from the step configuration |
retry_count | int | Number of retry attempts so far |
rejection_feedback | str | Feedback from the last rejection |
timeout_at | datetime | When the timeout expires (if set) |