FlowRunner
Pricing
Theme
Start Free

Use CrewAI with FlowRunner

Code for wiring a CrewAI crew to FlowRunner: send human feedback to Slack or email and resume the Flow, start crews from business apps, and give agents flows as tools.

2026-11-02 Updated November 3, 2026 10 min

Your crews stay in Python, where they run today. This page wires them to FlowRunner in four ways: a person reviews the crew’s output in Slack or by email and the Flow resumes, AMP’s human input webhook lands in a flow, a business event kicks off a crew, and an agent uses a flow as a tool.

You don’t need to learn the flow editor for any of it. FlowRunner Prompt to Flow builds the FlowRunner side from a prompt, and the code below is the CrewAI side. Every sample reads the workspace id and key from environment variables (FLOWRUNNER_WORKSPACE_ID, FLOWRUNNER_KEY); both are in Workspace Settings, under Credentials.

1. Connect FlowRunner Prompt to Flow

FlowRunner Prompt to Flow is an MCP server you add to the AI tool you already use. You connect it once and sign in with your FlowRunner account. The server URL is the same for every tool: https://app.flowrunner.ai/mcp.

  • Claude (web and desktop): open Settings > Connectors, click + Add, then Add custom connector. Enter FlowRunner and the server URL, click Add, then Connect, and approve the FlowRunner sign-in. On a Team or Enterprise plan, an Owner adds it once under Organization settings > Connectors.
  • Claude Code: run these two commands in your project folder, and approve the sign-in page that opens:
claude mcp add --transport http flowrunner https://app.flowrunner.ai/mcp
claude mcp login flowrunner
  • ChatGPT (on the web): open ChatGPT Plugins, click Add, then Add custom MCP server. Enter FlowRunner, put the server URL under Connection, choose OAuth, and approve the sign-in. In a chat, type @ and pick FlowRunner.
  • Codex: add the server to ~/.codex/config.toml, then run codex mcp login flowrunner:
[mcp_servers.flowrunner]
url = "https://app.flowrunner.ai/mcp"
  • Cursor: add the server to ~/.cursor/mcp.json, then open Customize > MCPs and follow the sign-in prompt:
{
  "mcpServers": {
    "flowrunner": { "url": "https://app.flowrunner.ai/mcp" }
  }
}

If you don't have a FlowRunner account yet, the Free plan needs no card, and every new account starts with 14 days of Professional. The setup guide in the docs has each tool step by step.

Pattern: your Flow asks a person, FlowRunner gets the answer

CrewAI’s @human_feedback pauses a Flow and hands the request to a provider. The default provider reads the console. A custom one can hand it to FlowRunner, which reaches the reviewer and calls you back with the answer.

Build the review flow. Paste a request like this into your AI tool:

Using the FlowRunner tools, build a flow called “Crew Review”. My Python service calls it through the API without waiting for an answer. It sends flowId, step, message, and draft. Post the message and the draft to #content-review in Slack with Approve and Reject buttons and a box for a comment. Wait for the answer, however long it takes. Then POST JSON to https://agents.example.com/crew-feedback with flowId, approved (true or false), comment, and who answered. Send the header X-Review-Secret on that request, and leave its value empty: I’ll paste it into the HTTP Request block myself in the flow editor. Retry that call on 429 and 5XX up to 3 times with exponential backoff, and if it still fails, post the error in #content-review. Ask me before you make it live, and tell me exactly what my service sends and what my endpoint receives.

The AI builds the flow, gives you a link to connect Slack if it isn’t connected yet, and asks before making it live. Read the field names from its answer before you write the code, because the AI picks them where your prompt doesn’t.

Hand the request to the flow. The provider starts the flow with the non-blocking Call Flow API, which answers at once with an executionId, then raises HumanFeedbackPending so CrewAI saves the Flow’s state:

import os
import requests
from crewai.flow import (
    Flow, start, listen, human_feedback,
    HumanFeedbackProvider, HumanFeedbackPending, PendingFeedbackContext,
)

FR_ACTIVATE = (
    "https://api.flowrunner.ai/api/v1/"
    f"{os.environ['FLOWRUNNER_WORKSPACE_ID']}/flows/<flow-id>/activate"
)


class FlowRunnerReview(HumanFeedbackProvider):
    def request_feedback(self, context: PendingFeedbackContext, flow) -> str:
        r = requests.post(
            FR_ACTIVATE,
            headers={"FR-Access-Key": os.environ["FLOWRUNNER_KEY"]},
            json={
                "flowId": context.flow_id,
                "step": context.method_name,
                "message": context.message,
                "draft": str(context.method_output),
            },
            timeout=10,
        )
        r.raise_for_status()
        raise HumanFeedbackPending(
            context=context,
            callback_info={"flowrunner_execution": r.json()["executionId"]},
        )


class ReplyFlow(Flow):
    @start()
    @human_feedback(
        message="Approve this customer reply, or say what to change.",
        emit=["approved", "rejected"],
        llm=os.environ["FEEDBACK_MODEL"],
        provider=FlowRunnerReview(),
    )
    def draft_reply(self):
        return "Hi Dana, your replacement ships today..."

    @listen("approved")
    def send_reply(self):
        ...

    @listen("rejected")
    def revise_reply(self):
        ...

kickoff() returns the HumanFeedbackPending object instead of raising it, so the caller can tell the Flow is waiting.

Resume when the answer arrives. The flow posts the decision to your endpoint. Check the shared secret, make sure each Flow resumes only once, then hand CrewAI the feedback in words so emit can route it:

import hmac
import os
from fastapi import FastAPI, Header, HTTPException

app = FastAPI()


@app.post("/crew-feedback")
async def crew_feedback(body: dict, x_review_secret: str = Header(...)):
    if not hmac.compare_digest(x_review_secret, os.environ["REVIEW_SECRET"]):
        raise HTTPException(status_code=401)
    if not claim_once(body["flowId"]):  # your store: returns False if already resumed
        return {"ok": True}  # a retried delivery

    words = "Approved." if body["approved"] else "Rejected."
    feedback = f"{words} {body.get('comment') or ''}".strip()

    flow = ReplyFlow.from_pending(body["flowId"])
    await flow.resume_async(feedback)
    return {"ok": True}

Use resume() instead of resume_async() outside an event loop. The FlowRunner run ends when your endpoint answers, so the record on the flow’s Instances tab shows the request, the reviewer’s answer, and the call back to you. It can wait up to 30 days on Free, Starter, and Growth, and up to a year on Professional and above, and it counts as one execution either way.

Pattern: AMP’s human input webhook lands in a flow

On CrewAI AMP, the pause is already there: pass humanInputWebhook at kickoff and the execution waits for human input. Point that webhook at a FlowRunner flow, and the flow does the reaching and the resuming.

Build the flow. Ask for one that starts from a webhook:

Using the FlowRunner tools, build a flow called “AMP Review”. It starts with a webhook: CrewAI AMP posts to it when a crew task needs human input, with the execution ID, the task ID, and the task output. Post the task output to #crew-review in Slack with Approve and Reject buttons and a box for a comment. When someone answers, POST to my crew’s resume endpoint, <crew-base-url>/resume, with an Authorization header that I’ll fill in with my bearer token myself in the flow editor, and a JSON body with execution_id, task_id, human_feedback (the comment, or “Approved” when it’s empty), and is_approve. Retry on 429 and 5XX up to 3 times. Ask me before making it live, and tell me which block holds the webhook address.

Before the first real run, open the External Callback block in the flow editor and turn on Learning Mode, then kick off one crew. FlowRunner records the payload AMP sends, so the flow reads AMP’s real field names instead of guessed ones.

Kick off with the flow as the webhook. Copy the block’s Callback URL into your server’s config. It contains your workspace key, so treat it like one:

import os
import requests

r = requests.post(
    f"{os.environ['CREW_BASE_URL']}/kickoff",
    headers={"Authorization": f"Bearer {os.environ['CREW_TOKEN']}"},
    json={
        "inputs": {"topic": "Q3 renewal risk"},
        "humanInputWebhook": {"url": os.environ["FLOWRUNNER_AMP_REVIEW_URL"]},
    },
    timeout=30,
)
r.raise_for_status()

Add the authentication object to humanInputWebhook if your AMP setup asks for it; the Callback URL itself is what FlowRunner checks. If you also use taskWebhookUrl, stepWebhookUrl, or crewWebhookUrl, add them to the flow’s resume call too: CrewAI’s docs note that webhook settings aren’t carried over from kickoff. When the reviewer rejects, AMP has the crew retry with their comment.

Pattern: a business event starts your crew

CrewAI AMP has triggers for Gmail, Google Calendar, Google Drive, Outlook, OneDrive, Microsoft Teams, HubSpot, Salesforce, Slack, and Zapier. For anything else, or for a crew you host yourself, a FlowRunner flow can start it:

Using the FlowRunner tools, build a flow called “New Ticket to Crew”. When a new ticket is created in Zendesk, POST the ticket’s subject, description, requester email, and priority to https://agents.example.com/crews/triage with the header X-Crew-Key; leave its value for me to paste into the HTTP Request block in the flow editor. Retry on 429 and 5XX up to 4 times with exponential backoff. If it still fails, email me the ticket id and the error. Ask before making it live.

Swap Zendesk for any of FlowRunner’s 2,100+ integrations, or for a schedule (every weekday at 9 am, or a cron expression). On your side, the endpoint calls kickoff() with the fields it receives. If the crew runs on AMP, point the flow at AMP’s /kickoff endpoint with your bearer token instead.

Pattern: your crew uses a flow as a tool

When an agent needs something from a business app, a flow can do it and return the answer in the same request. Wrap the blocking Call Flow API in a CrewAI tool:

import os
import requests
from crewai.tools import tool

FR_BLOCKING = (
    "https://api.flowrunner.ai/api/v1/"
    f"{os.environ['FLOWRUNNER_WORKSPACE_ID']}/flows/<flow-id>/activate-blocking"
)


@tool("Look up customer")
def look_up_customer(email: str) -> str:
    """Return the customer's plan, open invoices, and account owner from the CRM."""
    r = requests.post(
        FR_BLOCKING,
        params={"waitResponseTimeoutSeconds": 30},
        headers={"FR-Access-Key": os.environ["FLOWRUNNER_KEY"]},
        json={"email": email},
        timeout=40,
    )
    r.raise_for_status()
    return r.text

The flow ends with a Return Result block, and that value is the response body. Ask for it with a prompt that names the inputs and the exact answer you want back, and that says your program calls it through the API and waits. The blocking call waits up to 300 seconds. If the timeout runs out, the API answers with an error and the run keeps going, so don’t retry blindly: a retry starts a second run. Anything that waits on a person belongs in the non-blocking pattern above.

Optional: turn a crew into a flow

FlowRunner Prompt to Flow can also read agent code and build a flow that does the same job with FlowRunner’s AI Agent blocks, on your own model keys. It has done this with the Agents SDK code that OpenAI Agent Builder exports (six real conversions). For a crew, paste the agents, tasks, and Flow, and ask it to list anything it can’t map one to one before it builds. This is an option for a crew the rest of the team needs to read and change on a canvas. For everything else, keep the crew in Python and use the patterns above.

Questions about wiring CrewAI to FlowRunner? Contact us.

Frequently asked questions

Where do the FlowRunner workspace id and key come from?

Both are in Workspace Settings, under General, in the Credentials section. Keep the key on your server, in an environment variable, and send it in the FR-Access-Key header. Never put it in client code.

What happens if nobody answers the review?

The FlowRunner run keeps waiting, up to 30 days on Free, Starter, and Growth, and up to a year on Professional and above. Your CrewAI Flow stays paused in its persistence layer until your resume endpoint is called. If you want a reminder or a default answer after a set time, say so in the prompt that builds the flow.

Does the review flow work with the open source framework and AMP?

Yes, with different wiring. On the open source framework, your HumanFeedbackProvider starts the flow and the flow calls your resume endpoint. On AMP, the humanInputWebhook points at the flow and the flow calls AMP's /resume endpoint.

How many executions does one review use?

One. The whole FlowRunner run, from the request to the answer being sent back, counts as one execution however long it waits.

Can FlowRunner Prompt to Flow convert my crew into a flow?

It can read crew code and build a flow that does the same work with FlowRunner's AI Agent blocks. That's an option, not a requirement. Most teams keep the crew in Python and let FlowRunner run the triggers, reviews, and record around it.

Editorial policy

Ready to try it?

Start on the Free plan. No card required, and every new account gets 14 days of Professional.