Resumable and Human-Approvable Workflows in BotsOnRails
One of the key features of BotsOnRails is the ability to create workflows that can be paused, reviewed by a human, and then resumed. This is particularly useful when you need human judgment or approval at certain points in your workflow.
Marking a Node for Human Approval
To mark a node as requiring human approval before the workflow can proceed, use the wait_for_approval=True
argument to the @step
decorator:
from BotsOnRails import ExecutionPath, step_decorator_for_path
path = ExecutionPath()
step = step_decorator_for_path(path)
@step(wait_for_approval=True)
def human_review(text: str) -> bool:
print(f"Please review the following text: {text}")
return input("Approve? (y/n): ").lower() == "y"
When the workflow reaches this node, it will pause execution and wait for human interaction. The node function should prompt the user for input and return a value indicating whether to proceed or not.
Resuming a Workflow
When a workflow is paused at a human approval node, you can resume it by calling the run_from_step
method on the
ExecutionPath
:
result = path.run(initial_data)
if result == SpecialTypes.EXECUTION_HALTED:
print("Workflow paused for human approval")
approve = input("Proceed? (y/n): ").lower() == "y"
if approve:
path.run_from_step(
halted_node_name,
prev_execution_state=tree.model_dump(),
has_approval=True
)
else:
print("Workflow cancelled by user")
The run
method will return a special value SpecialTypes.EXECUTION_HALTED
if the workflow is paused at a human approval node.
To resume, you first need to get the name of the node where execution is halted. You can find this in the locked_at_step_name
attribute of the ExecutionPath
.
Then, call run_from_step
, passing:
- The name of the node to resume from
- The previous execution state, obtained by calling model_dump()
on the ExecutionPath
- has_approval=True
to indicate that the human has approved proceeding
The workflow will then resume from the approval node.
Detailed Example
Here's a more detailed example of a content moderation workflow that uses human approval:
from BotsOnRails import ExecutionPath, step_decorator_for_path, SpecialTypes
path = ExecutionPath()
step = step_decorator_for_path(path)
@step(path_start=True, next_step={"flagged": "human_review", "clean": "publish"})
def analyze_content(text: str, **kwargs) -> str:
if "spam" in text:
return "flagged"
else:
return "clean"
@step(wait_for_approval=True, next_step={"approve": "publish", "reject": "end"})
def human_review(text: str, **kwargs) -> str:
# You can access data from other nodes using the runtime_args property in the kwargs
orig_input = kwargs['runtime_args']['input'][0]
print(f"Please review the following content: {orig_input}")
# We set this is a default return type, but we can override this when we resume the workflow
return "reject"
@step()
def publish(text: str, **kwargs):
print(f"Publishing: {text}")
@step()
def end(**kwargs):
print("Content rejected")
path.compile()
while True:
text = input("Enter some text to moderate (or 'quit'): ")
if text == "quit":
break
result = path.run(text)
if result == SpecialTypes.EXECUTION_HALTED:
halted_node = path.locked_at_step_name
print(f"Workflow paused at {halted_node}")
approve = input("Proceed? (y/n): ").lower() == "y"
if approve:
path.run_from_step(
halted_node,
prev_execution_state=path.model_dump(),
has_approval=True,
override_output="approve"
)
else:
path.run_from_step(
halted_node,
prev_execution_state=path.model_dump(),
has_approval=True,
override_output="reject"
)
This workflow first analyzes the input text. If it's flagged as potentially inappropriate, it routes to a
human_review
node for approval. This node is marked with wait_for_approval=True
, so execution will pause here.
The main loop checks the result of tree.run()
. If it's EXECUTION_HALTED
, it prompts the user to approve or
reject the content.
If approved, it resumes the workflow from the human_review
node with override_output="approve"
, which will cause
the workflow to proceed to the publish
node.
If rejected, it resumes with override_output="reject"
, which will route to the end
node instead.
This demonstrates how you can build workflows that flexibly incorporate human judgment and easily resume after human
interaction. The wait_for_approval
mechanism allows you to designate any node as a pause point, and
run_from_step
allows you to resume from that point once human input is provided.
You can also see how the execution state could be easily persisted to disk and then resumed later... the ExecutionPath
obj instance is a Pydantic model, so we can dump the model out to disk, serialize it, store it, and return to
execution at any arbitrary point in time.