---
title: CrewAI Flows
description: Learn how to implement Human-in-the-Loop (HITL) using CrewAI Flows.
icon: lucide/Share2
---

<video
  src="https://cdn.copilotkit.ai/docs/copilotkit/images/coagents/node-hitl.mp4"
  className="rounded-lg shadow-xl"
  loop
  playsInline
  controls
  autoPlay
  muted
/>

<Callout type="info">
  Pictured above is the [coagent
  starter](https://github.com/copilotkit/copilotkit/tree/main/examples/coagents-starter-crewai-flows)
  with the implementation below applied!
</Callout>

## What is this?

[Flow based agents](https://docs.crewai.com/concepts/flows) are stateful agents that can be interrupted and resumed
to allow for user input.

CopilotKit lets you to add custom UI to take user input and then pass it back to the agent upon completion.

## Why should I use this?

Human-in-the-loop is a powerful way to implement complex workflows that are production ready. By having a human in the loop,
you can ensure that the agent is always making the right decisions and ultimately is being steered in the right direction.

Flow based agents are a great way to implement HITL for more complex workflows where you want to ensure the agent is aware
of everything that has happened during a HITL interaction.

## Implementation

<Steps>
    <Step>
        ### Run and connect your agent

        You'll need to run your agent and connect it to CopilotKit before proceeding. If you haven't done so already,
        you can follow the instructions in the [Getting Started](/crewai-flows/quickstart) guide.

        If you don't already have an agent, you can use the [coagent starter](https://github.com/CopilotKit/CopilotKit/tree/main/examples/coagents-starter-crewai-flows) as a starting point
        as this guide uses it as a starting point.
    </Step>

    <Step>
      ### Install the CopilotKit SDK
      <InstallSDKSnippet/>
    </Step>

    <Step>
        ### Add a `useFrontendTool` to your Frontend
        First, we'll create a component that renders the agent's essay draft and waits for user approval.

        ```tsx title="ui/app/page.tsx"

        function YourMainContent() {
          // ...

          useFrontendTool({
            name: "writeEssay",
            available: "remote",
            description: "Writes an essay and takes the draft as an argument.",
            parameters: z.object({
              draft: z.string().describe("The draft of the essay"),
            }),
            // [!code highlight:25]
            renderAndWaitForResponse: ({ args, respond, status }) => {
              return (
                <div>
                  <Markdown content={args.draft || 'Preparing your draft...'} />

                  <div className={`flex gap-4 pt-4 ${status !== "executing" ? "hidden" : ""}`}>
                    <button
                      onClick={() => respond?.("CANCEL")}
                      disabled={status !== "executing"}
                      className="border p-2 rounded-xl w-full"
                    >
                      Try Again
                    </button>
                    <button
                      onClick={() => respond?.("SEND")}
                      disabled={status !== "executing"}
                      className="bg-blue-500 text-white p-2 rounded-xl w-full"
                    >
                      Approve Draft
                    </button>
                  </div>
                </div>
              );
            },
          });

          // ...
        }
        ```
    </Step>

    <Step>
    ### Setup the CrewAI Agent
    Now we'll setup the CrewAI agent. The flow is hard to understand without a complete example, so below
    is the complete implementation of the agent with explanations.

    Some main things to note:
    - The agent's state inherits from `CopilotKitState` to bring in the CopilotKit actions.
    - CopilotKit's actions are bound to the model as tools.
    - If the `writeEssay` action is found in the model's response, the agent will pass control back to the frontend
      to get user feedback.

    <Tabs groupId="language_crewai-flows_agent" items={["Python"]} persist>
      <Tab value="Python">
        ```python title="agent.py"
        from typing import Any, cast
        from crewai.flow.flow import Flow, start, listen
        from copilotkit import CopilotKitState
        from copilotkit.crewai import copilotkit_stream
        from litellm import completion


        class AgentState(CopilotKitState):
            pass


        class SampleAgentFlow(Flow[AgentState]):

            @start()
            async def check_for_user_feedback(self):
                if not self.state.get("messages"):
                    return

                last_message = cast(Any, self.state["messages"][-1])

                # Expecting the result of a CopilotKit tool call (SEND/CANCEL)
                if last_message["role"] == "tool":
                    user_response = last_message.get("content")

                    if user_response == "SEND":
                        self.state["messages"].append({
                            "role": "assistant",
                            "content": "✅ Great! Sending your essay via email.",
                        })
                        return

                    if user_response == "CANCEL":
                        self.state["messages"].append({
                            "role": "assistant",
                            "content": "❌ Okay, we can improve the draft. What would you like to change?",
                        })
                        return

                # If no tool result yet, or it's a user message, prompt next step
                if last_message.get("role") == "user":
                    self.state["messages"].append({
                        "role": "system",
                        "content": (
                            "You write essays. Use your tools to write an essay; "
                            "don’t just write it in plain text."
                        )
                    })

            @listen(check_for_user_feedback)
            async def chat(self):
                messages = self.state.get("messages", [])

                system_message = {
                    "role": "system",
                    "content": (
                        "You write essays. Use your tools to write an essay; "
                        "don’t just write it in plain text."
                    )
                }

                response = await copilotkit_stream(
                    completion(
                        model="openai/gpt-5.4",
                        messages=[system_message, *messages],
                        tools=self.state["copilotkit"]["actions"],
                        stream=True
                    )
                )

                self.state["messages"].append(response.choices[0].message)
        ```
      </Tab>
    </Tabs>
    </Step>
    <Step>
        ### Give it a try!
        Try asking your agent to write an essay about the benefits of AI. You'll see that it will generate an essay,
        stream the progress and eventually ask you to review it.
    </Step>

</Steps>
