Project: Prefect (Orchestration & Management / Scheduling & Orchestration)
File: creating-interactive-workflows.md

description: Learn how to create interactive workflows with Prefect. tags: flow run pause suspend input human-in-the-loop workflows interactive workflows search: boost: 2 Learn how to create interactive workflows with Prefect. Flows can pause or suspend execution and automatically resume when they receive type-checked input in Prefect's UI. Flows can also send and receive type-checked input at any time while runningwithout pausing or suspending. This guide explains how to use these features to build interactive workflows. !!! note "A note on async Python syntax" Most of the example code in this section uses async Python functions and `await`. However, as with other Prefect features, you can call these functions with or without `await`. You can pause or suspend a flow until it receives input from a user in Prefect's UI. This is useful when you need to ask for additional information or feedback before resuming a flow. These workflows are often called (HITL) systems. !!! note "What is human-in-the-loop interactivity used for?" Approval workflows that pause to ask a human to confirm whether a workflow should continue are very common in the business world. Certain types of and artificial intelligence workflows benefit from incorporating HITL design. To receive input while paused or suspended use the `waitforinput` parameter in the `pauseflowrun` or `suspendflowrun` functions. This parameter accepts one of the following: A built-in type like `int` or `str`, or a built-in collection like `List[int]` A `pydantic.BaseModel` subclass A subclass of `prefect.input.RunInput` !!! tip "When to use a `RunModel` or `BaseModel` instead of a built-in type" There are a few reasons to use a `RunModel` or `BaseModel`. The first is that when you let Prefect automatically create one of these classes for your input type, the field that users will see in Prefect's UI when they click "Resume" on a flow run is named `value` and has no help text to suggest what the field is. If you create a `RunInput` or `BaseModel`, you can change details like the field name, help text, and default value, and users will see those reflected in the "Resume" form. The simplest way to pause or suspend and wait for input is to pass a built-in type: ```python from prefect import flow, pauseflowrun, getrunlogger @flow def greet_user(): logger = getrunlogger() user = pauseflowrun(waitforinput=str) logger.info(f"Hello, {user}!") ``` In this example, the flow run will pause until a user clicks the Resume button in the Prefect UI, enters a name, and submits the form. !!! note "What types can you pass for `waitforinput`?" When you pass a built-in type such as `int` as an argument for the `waitforinput` parameter to `pauseflowrun` or `suspendflowrun`, Prefect automatically creates a Pydantic model containing one field annotated with the type you specified. This means you can use with these functions. Instead of a built-in type, you can pass in a `pydantic.BaseModel` class. This is useful if you already have a `BaseModel` you want to use: ```python from prefect import flow, pauseflowrun, getrunlogger from pydantic import BaseModel class User(BaseModel): name: str age: int @flow async def greet_user(): logger = getrunlogger() user = await pauseflowrun(waitforinput=User) logger.info(f"Hello, {user.name}!") ``` !!! note "`BaseModel` classes are upgraded to `RunInput` classes automatically" When you pass a `pydantic.BaseModel` class as the `waitforinput` argument to `pauseflowrun` or `suspendflowrun`, Prefect automatically creates a `RunInput` class with the same behavior as your `BaseModel` and uses that

`RunInput` classes contain extra logic that allows flows to send and receive them at runtime. You shouldn't notice any difference. For advanced use cases such as overriding how Prefect stores flow run inputs, create a `RunInput` class: ```python from prefect import getrunlogger from prefect.input import RunInput class UserInput(RunInput): name: str age: int def override_something(self, args, *kwargs): super().override_something(args, *kwargs) @flow async def greet_user(): logger = getrunlogger() user = await pauseflowrun(waitforinput=UserInput) logger.info(f"Hello, {user.name}!") ``` Set default values for fields in your model with the `withinitialdata` method. This is useful for providing default values for the fields in your own `RunInput` class. Expanding on the example above, you can make the `name` field default to "anonymous": ```python from prefect import getrunlogger from prefect.input import RunInput class UserInput(RunInput): name: str age: int @flow async def greet_user(): logger = getrunlogger() userinput = await pauseflow_run( waitforinput=UserInput.withinitialdata(name="anonymous") ) if user_input.name == "anonymous": logger.info("Hello, stranger!") else: logger.info(f"Hello, {user_input.name}!") ``` When a user sees the form for this input, the name field contains "anonymous" as the default. You can provide a dynamic, markdown description that appears in the Prefect UI when the flow run pauses. This feature enables context-specific prompts, enhancing clarity and user interaction. Building on the example above: ```python from datetime import datetime from prefect import flow, pauseflowrun, getrunlogger from prefect.input import RunInput class UserInput(RunInput): name: str age: int @flow async def greet_user(): logger = getrunlogger() current_date = datetime.now().strftime("%B %d, %Y") description_md = f""" Welcome to the User Greeting Flow! Today's Date: {current_date} Please enter your details below: Name: What should we call you? Age: Just a number, nothing more. """ userinput = await pauseflow_run( waitforinput=UserInput.withinitialdata( description=description_md, name="anonymous" ) ) if user_input.name == "anonymous": logger.info("Hello, stranger!") else: logger.info(f"Hello, {user_input.name}!") ``` When a user sees the form for this input, the given markdown will appear above the input fields. Prefect uses the fields and type hints on your `RunInput` or `BaseModel` class to validate the general structure of input your flow receives. If you require more complex validation, use Pydantic . !!! warning "Custom validation runs after the flow resumes" Prefect transforms the type annotations in your `RunInput` or `BaseModel` class to a JSON schema and uses that schema in the UI for client-side validation. However, custom validation requires running Python logic defined in your `RunInput` class. Because of this, validation happens after the flow resumes, so you should handle it explicitly in your flow. Continue reading for an example best practice. The following is an example `RunInput` class that uses a custom field validator: ```python import pydantic from prefect.input import RunInput class ShirtOrder(RunInput): size: Literal["small", "medium", "large", "xlarge"] color: Literal["red", "green", "black"] @pydantic.validator("color") def validate_age(cls, value, values, kwargs): if value == "green" and values["size"] == "small": raise ValueError( "Green is only in-stock for medium, large, and XL sizes." ) return value ``` In the example, we use Pydantic's `validator` decorator to define a custom validation method for the `color` field. You can use it in a flow like this: ```python import pydantic from prefect import flow, pauseflowrun from prefect.input import RunInput class ShirtOrder(RunInput): size: Literal["small", "medium", "large", "xlarge"] color: Literal["red", "green", "black"] @pydantic.validator("color") def validate_age(cls, value, values, kwargs): if value == "green" and values["size"] == "small": raise ValueError( "Green is only in-stock for medium, large, and XL

) return value @flow def getshirtorder(): shirtorder = pauseflowrun(waitfor_input=ShirtOrder) ``` If a user chooses any size and color combination other than `small` and `green`, the flow run will resume successfully. However, if the user chooses size `small` and color `green`, the flow run will resume, and `pauseflowrun` will raise a `ValidationError` exception. This will cause the flow run to fail and log the error. To avoid a flow run failure, use a `while` loop and pause again if the `ValidationError` exception is raised: ```python from typing import Literal import pydantic from prefect import flow, getrunlogger, pauseflowrun from prefect.input import RunInput class ShirtOrder(RunInput): size: Literal["small", "medium", "large", "xlarge"] color: Literal["red", "green", "black"] @pydantic.validator("color") def validate_age(cls, value, values, kwargs): if value == "green" and values["size"] == "small": raise ValueError( "Green is only in-stock for medium, large, and XL sizes." ) return value @flow def getshirtorder(): logger = getrunlogger() shirt_order = None while shirt_order is None: try: shirtorder = pauseflowrun(waitfor_input=ShirtOrder) except pydantic.ValidationError as exc: logger.error(f"Invalid size and color combination: {exc}") logger.info( f"Shirt order: {shirtorder.size}, {shirtorder.color}" ) ``` This code causes the flow run to continually pause until the user enters a valid age. As an additional step, you can use an or to alert the user to the error. Use the `sendinput` and `receiveinput` functions to send input to a flow or receive input from a flow at runtime. You don't need to pause or suspend the flow to send or receive input. !!! note "Reasons to send or receive input without pausing or suspending" You might want to send or receive input without pausing or suspending in scenarios where the flow run is designed to handle real-time data. For example, in a live monitoring system, you might need to update certain parameters based on the incoming data without interrupting the flow. Another use is having a long-running flow that continually responds to runtime input with low latency. For example, if you're building a chatbot, you could have a flow that starts a GPT Assistant and manages a conversation thread. The most important parameter to the `sendinput` and `receiveinput` functions is `run_type`, which should be one of the following: A built-in type such as `int` or `str` A `pydantic.BaseModel` class A `prefect.input.RunInput` class !!! type "When to use a `BaseModel` or `RunInput` instead of a built-in type" Most built-in types and collections of built-in types should work with `sendinput` and `receiveinput`, but there is a caveat with nested collection types, such as lists of tuples. For example, `List[Tuple[str, float]])`. In this case, validation may happen after your flow receives the data, so calling `receive_input` may raise a `ValidationError`. You can plan to catch this exception, and consider placing the field in an explicit `BaseModel` or `RunInput` so your flow only receives exact type matches. See examples below of `receiveinput`, `sendinput`, and the two functions working together. The following flow uses `receive_input` to continually receive names and print a personalized greeting for each name it receives: ```python from prefect import flow from prefect.input.runinput import receiveinput @flow async def greeter_flow(): async for nameinput in receiveinput(str, timeout=None): print(f"Hello, {name_input}!") ``` When you pass a type such as `str` into `receiveinput`, Prefect creates a `RunInput` class to manage your input automatically. When a flow sends input of this type, Prefect uses the `RunInput` class to validate the input. If the validation succeeds, your flow receives the input in the type you

In this example, if the flow received a valid string as input, the variable `nameinput` contains the string value. If, instead, you pass a `BaseModel`, Prefect upgrades your `BaseModel` to a `RunInput` class, and the variable your flow sees &mdash; in this case, `name_input` &mdash; is a `RunInput` instance that behaves like a `BaseModel`. If you pass in a `RunInput` class, no upgrade is needed and you'll get a `RunInput` instance. A simpler approach is to pass types such as `str` into `receiveinput` . If you need access to the generated `RunInput` that contains the received value, pass `withmetadata=True` to `receive_input`: ```python from prefect import flow from prefect.input.runinput import receiveinput @flow async def greeter_flow(): async for nameinput in receiveinput( str, timeout=None, with_metadata=True ): print(f"Hello, {name_input.value}!") ``` !!! tip "When to use `with_metadata=True`" The primary uses of accessing the `RunInput` object for a receive input are to respond to the sender with the `RunInput.respond()` function or to access the unique key for an input. These uses are discussed in more detail later in this document. Notice that we are now printing `nameinput.value`. When Prefect generates a `RunInput` for you from a built-in type, the `RunInput` class has a single field, `value`, that uses a type annotation matching the type you specified. So if you call `receiveinput` like this: `receiveinput(str, withmetadata=True)`, that's equivalent to manually creating the following `RunInput` class and `receive_input` call: ```python from prefect import flow from prefect.input.run_input import RunInput class GreeterInput(RunInput): value: str @flow async def greeter_flow(): async for nameinput in receiveinput(GreeterInput, timeout=None): print(f"Hello, {name_input.value}!") ``` !!! warning "The type used in `receiveinput` and `sendinput` must match" For a flow to receive input, the sender must use the same type that the receiver is receiving. This means that if the receiver is receiving `GreeterInput`, the sender must send `GreeterInput`. If the receiver is receiving `GreeterInput` and the sender sends `str` input that Prefect automatically upgrades to a `RunInput` class, the types won't match, so the receiving flow run won't receive the input. However, the input will be waiting if the flow ever calls `receive_input(str)`. By default, each time you call `receive_input`, you get an iterator that iterates over all known inputs to a specific flow run, starting with the first received. The iterator will keep track of your current position as you iterate over it, or you can call `next()` to explicitly get the next input. If you're using the iterator in a loop, you should assign it to a variable: ```python from prefect import flow, get_client from prefect.deployments.deployments import run_deployment from prefect.input.runinput import receiveinput, send_input EXITSIGNAL = "EXIT_" @flow async def sender(): greeterflowrun = await run_deployment( "greeter/send-receive", timeout=0, as_subflow=False ) client = get_client() receiver = receive_input( str, with_metadata=True, timeout=None, poll_interval=0.1 ) while True: name = input("What is your name? ") if not name: continue if name == "q" or name == "quit": await send_input( EXIT_SIGNAL, flowrunid=greeterflowrun.id ) print("Goodbye!") break await sendinput(name, flowrunid=greeterflow_run.id) greeting = await receiver.next() print(greeting) ``` An iterator helps keep track of the inputs your flow has already received. If you want your flow to suspend and then resume later, save the keys of the inputs you've seen so the flow can read them back out when it resumes. You might use a , such as a

The following flow receives input for 30 seconds then suspends itself, which exits the flow and tears down infrastructure: ```python from prefect import flow, getrunlogger, suspendflowrun from prefect.blocks.system import JSON from prefect.context import getruncontext from prefect.input.runinput import receiveinput EXITSIGNAL = "EXIT_" @flow async def greeter(): logger = getrunlogger() runcontext = getrun_context() assert runcontext.flowrun, "Could not see my flow run ID" blockname = f"{runcontext.flow_run.id}-seen-ids" try: seenkeysblock = await JSON.load(block_name) except ValueError: seenkeysblock = JSON( value=[], ) try: async for nameinput in receiveinput( str, with_metadata=True, poll_interval=0.1, timeout=30, excludekeys=seenkeys_block.value ): if nameinput.value == EXITSIGNAL: print("Goodbye!") return await nameinput.respond(f"Hello, {nameinput.value}!") seenkeysblock.value.append(name_input.metadata.key) await seenkeysblock.save( name=block_name, overwrite=True ) except TimeoutError: logger.info("Suspending greeter after 30 seconds of idle time") await suspendflowrun(timeout=10000) ``` As this flow processes name input, it adds the key of the flow run input to the `seenkeysblock`. When the flow later suspends and then resumes, it reads the keys it has already seen out of the JSON Block and passes them as the `exludekeys` parameter to `receiveinput`. When your flow receives input from another flow, Prefect knows the sending flow run ID, so the receiving flow can respond by calling the `respond` method on the `RunInput` instance the flow received. There are a couple of requirements: Pass in a `BaseModel` or `RunInput`, or use `with_metadata=True`. The flow you are responding to must receive the same type of input you send in order to see it. The `respond` method is equivalent to calling `sendinput(..., flowrunid=sendingflow_run.id)`, but with `respond`, your flow doesn't need to know the sending flow run's ID. Next, make the `greeter_flow` respond to name inputs instead of printing them: ```python from prefect import flow from prefect.input.runinput import receiveinput @flow async def greeter(): async for nameinput in receiveinput( str, with_metadata=True, timeout=None ): await nameinput.respond(f"Hello, {nameinput.value}!") ``` There's one problem left: this flow runs forever unless there's a signal that it should exit. Here's how to teach it to look for a special string: ```python from prefect import flow from prefect.input.runinput import receiveinput EXITSIGNAL = "EXIT_" @flow async def greeter(): async for nameinput in receiveinput( str, with_metadata=True, poll_interval=0.1, timeout=None ): if nameinput.value == EXITSIGNAL: print("Goodbye!") return await nameinput.respond(f"Hello, {nameinput.value}!") ``` With a `greeter` flow in place, create the flow that sends `greeter` names. Send input to a flow with the `sendinput` function. This works similarly to `receiveinput` and, like that function, accepts the same `run_input` argument, which can be a built-in type such as `str`, or else a `BaseModel` or `RunInput` subclass. !!! note "When to send input to a flow run" Send input to a flow run as soon as you have the flow run's ID. The flow does not have to be receiving input for you to send input. If you send a flow input before it is receiving, it will see your input when it calls `receiveinput` (as long as the types in the `sendinput` and `receive_input` calls match). Next, create a `sender` flow that starts a `greeter` flow run and then enters a loopcontinuously getting input from the terminal and sending it to the greeter flow: ```python @flow async def sender(): greeterflowrun = await run_deployment( "greeter/send-receive", timeout=0, as_subflow=False ) receiver = receiveinput(str, timeout=None, pollinterval=0.1) client = get_client() while True: flowrun = await client.readflowrun(greeterflow_run.id) if not flowrun.state or not flowrun.state.is_running(): continue name = input("What is your name? ") if not name: continue if name == "q" or name == "quit": await send_input( EXIT_SIGNAL, flowrunid=greeterflowrun.id ) print("Goodbye!") break await sendinput(name, flowrunid=greeterflow_run.id) greeting = await

print(greeting) ``` There's more going on here than in `greeter`, so here's a closer look at the pieces. First, `rundeployment` starts a `greeter` flow run. This requires a deployed flow running in a process. That process begins running `greeter` while `sender` continues to execute. Calling `rundeployment(..., timeout=0)` ensures that `sender` won't wait for the `greeter` flow run to complete, because it's running a loop and will only exit when sending `EXIT_SIGNAL`. Next, the iterator returned by `receive_input` as `receiver` is captured. This flow works by entering a loop, and on each iteration of the loop, the flow asks for terminal input, sends that to the `greeter` flow, and then runs `receiver.next()` to wait until it receives the response from `greeter`. Next, the terminal user who ran this flow is allowed to exit by entering the string `q` or `quit`. When that happens, the `greeter` flow is sent an exit signal so it will shut down too. Finally, the new name is sent to `greeter`. `greeter` sends back a greeting as a string. When you receive the greeting, print it and continue the loop that gets terminal input. For a complete example of using `sendinput` and `receiveinput`, here is what the `greeter` and `sender` flows look like together: ```python import asyncio import sys from prefect import flow, get_client from prefect.blocks.system import JSON from prefect.context import getruncontext from prefect.deployments.deployments import run_deployment from prefect.input.runinput import receiveinput, send_input EXITSIGNAL = "EXIT_" @flow async def greeter(): runcontext = getrun_context() assert runcontext.flowrun, "Could not see my flow run ID" blockname = f"{runcontext.flow_run.id}-seen-ids" try: seenkeysblock = await JSON.load(block_name) except ValueError: seenkeysblock = JSON( value=[], ) async for nameinput in receiveinput( str, with_metadata=True, poll_interval=0.1, timeout=None ): if nameinput.value == EXITSIGNAL: print("Goodbye!") return await nameinput.respond(f"Hello, {nameinput.value}!") seenkeysblock.value.append(name_input.metadata.key) await seenkeysblock.save( name=block_name, overwrite=True ) @flow async def sender(): greeterflowrun = await run_deployment( "greeter/send-receive", timeout=0, as_subflow=False ) receiver = receiveinput(str, timeout=None, pollinterval=0.1) client = get_client() while True: flowrun = await client.readflowrun(greeterflow_run.id) if not flowrun.state or not flowrun.state.is_running(): continue name = input("What is your name? ") if not name: continue if name == "q" or name == "quit": await send_input( EXIT_SIGNAL, flowrunid=greeterflowrun.id ) print("Goodbye!") break await sendinput(name, flowrunid=greeterflow_run.id) greeting = await receiver.next() print(greeting) if name == "main": if sys.argv[1] == "greeter": asyncio.run(greeter.serve(name="send-receive")) elif sys.argv[1] == "sender": asyncio.run(sender()) ``` To run the example, you need a Python environment with Prefect installed, pointed at either a self-hosted Prefect server instance or Prefect Cloud. With your environment set up, start a flow runner in one terminal with the following command: <div class="terminal"> ```bash python myfilename greeter ``` </div> For example, with Prefect Cloud, you should see output like this: <div class="terminal"> ```bash Your flow 'greeter' is being served and polling for scheduled runs! To trigger a run for this flow, use the following command: $ prefect deployment run 'greeter/send-receive' You can also run your flow via the Prefect UI: https://app.prefect.cloud/account/...(a URL for your account) ``` </div> Then start the greeter process in another terminal: <div class="terminal"> ```bash python myfilename sender ``` </div> You should see output like this: <div class="terminal"> ```bash 11:38:41.800 | INFO | prefect.engine - Created flow run 'gregarious-owl' for flow 'sender' 11:38:41.802 | INFO | Flow run 'gregarious-owl' - View at https://app.prefect.cloud/account/... What is your name? ``` </div> Type a name and press the enter key to see a greeting, and you'll see sending and receiving in action: <div class="terminal"> ```bash What is your name? andrew Hello, andrew! ``` </div>