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.