diff --git a/docs/en/concepts/flows.mdx b/docs/en/concepts/flows.mdx
index defbd3e01..ea482f199 100644
--- a/docs/en/concepts/flows.mdx
+++ b/docs/en/concepts/flows.mdx
@@ -572,6 +572,176 @@ The `third_method` and `fourth_method` listen to the output of the `second_metho
When you run this Flow, the output will change based on the random boolean value generated by the `start_method`.
+### Conversational Flows (User Input)
+
+The `self.ask()` method pauses flow execution to request input from a user inline, then returns their response as a string. This enables conversational, interactive flows where the AI can gather information, ask clarifying questions, or request approvals during execution.
+
+#### Basic Usage
+
+```python Code
+from crewai.flow.flow import Flow, start, listen
+
+class GreetingFlow(Flow):
+ @start()
+ def greet(self):
+ name = self.ask("What's your name?")
+ self.state["name"] = name
+
+ @listen(greet)
+ def welcome(self):
+ print(f"Welcome, {self.state['name']}!")
+
+flow = GreetingFlow()
+flow.kickoff()
+```
+
+By default, `self.ask()` uses a `ConsoleProvider` that prompts via Python's built-in `input()`.
+
+#### Multiple Asks in One Method
+
+You can call `self.ask()` multiple times within a single method to gather several inputs:
+
+```python Code
+from crewai.flow.flow import Flow, start
+
+class OnboardingFlow(Flow):
+ @start()
+ def collect_info(self):
+ name = self.ask("What's your name?")
+ role = self.ask("What's your role?")
+ team = self.ask("Which team are you joining?")
+ self.state["profile"] = {"name": name, "role": role, "team": team}
+ print(f"Welcome {name}, {role} on {team}!")
+
+flow = OnboardingFlow()
+flow.kickoff()
+```
+
+#### Timeout Support
+
+Pass `timeout=` (in seconds) to avoid blocking indefinitely. If the user doesn't respond in time, `self.ask()` returns `None`:
+
+```python Code
+from crewai.flow.flow import Flow, start
+
+class ApprovalFlow(Flow):
+ @start()
+ def request_approval(self):
+ response = self.ask("Approve deployment? (yes/no)", timeout=120)
+
+ if response is None:
+ print("No response received — timed out.")
+ self.state["approved"] = False
+ return
+
+ self.state["approved"] = response.strip().lower() == "yes"
+```
+
+Use a `while` loop to retry on timeout:
+
+```python Code
+from crewai.flow.flow import Flow, start
+
+class RetryFlow(Flow):
+ @start()
+ def ask_with_retry(self):
+ answer = None
+ while answer is None:
+ answer = self.ask("Please confirm (yes/no):", timeout=60)
+ if answer is None:
+ print("Timed out, asking again...")
+ self.state["confirmed"] = answer.strip().lower() == "yes"
+```
+
+#### Metadata Support
+
+The `metadata` parameter enables bidirectional context passing between the flow and the input provider. Send context to the provider, and receive structured context back:
+
+```python Code
+from crewai.flow.flow import Flow, start
+
+class ContextualFlow(Flow):
+ @start()
+ def gather_feedback(self):
+ response = self.ask(
+ "Rate this output (1-5):",
+ metadata={
+ "step": "quality_review",
+ "output_id": "abc-123",
+ "options": ["1", "2", "3", "4", "5"],
+ },
+ )
+ self.state["rating"] = int(response) if response else None
+```
+
+When a custom provider returns an `InputResponse`, it can include its own metadata (e.g., user identity, timestamp, channel info) that your flow can process.
+
+#### Custom InputProvider
+
+For production use cases (Slack bots, web UIs, webhooks), implement the `InputProvider` protocol:
+
+```python Code
+from crewai.flow.flow import Flow, start
+from crewai.flow.input_provider import InputProvider, InputResponse
+import requests
+
+class SlackInputProvider(InputProvider):
+ def __init__(self, channel_id: str, bot_token: str):
+ self.channel_id = channel_id
+ self.bot_token = bot_token
+
+ def request_input(self, message, flow, metadata=None):
+ # Post the question to Slack
+ requests.post(
+ "https://slack.com/api/chat.postMessage",
+ headers={"Authorization": f"Bearer {self.bot_token}"},
+ json={"channel": self.channel_id, "text": message},
+ )
+ # Wait for and return the user's reply (simplified)
+ reply = self.poll_for_reply()
+ return InputResponse(
+ value=reply["text"],
+ metadata={"user": reply["user"], "ts": reply["ts"]},
+ )
+
+ def poll_for_reply(self):
+ # Your implementation to wait for a Slack reply
+ ...
+
+# Use the custom provider
+flow = Flow(input_provider=SlackInputProvider(
+ channel_id="C01ABC123",
+ bot_token="xoxb-...",
+))
+flow.kickoff()
+```
+
+The `request_input` method can return:
+- A **string** — used directly as the user's response
+- An **`InputResponse`** — includes `value` (the response string) and optional `metadata`
+- **`None`** — treated as a timeout / no response
+
+#### Auto-Checkpoint Behavior
+
+
+When [persistence](/concepts/flows-persistence) is configured, the flow state is automatically saved **before** each `self.ask()` call. If the process restarts while waiting for input, the flow can resume from the checkpoint without losing progress.
+
+
+#### `self.ask()` vs `@human_feedback`
+
+| | `self.ask()` | `@human_feedback` |
+|---|---|---|
+| **Purpose** | Inline user input during execution | Approval gates and review feedback |
+| **Returns** | `str \| None` | `HumanFeedbackResult` with structured fields |
+| **Timeout** | Built-in `timeout=` parameter | Not built-in |
+| **Provider** | Pluggable `InputProvider` protocol | Console-based |
+| **Use when** | Gathering data, clarifications, confirmations | Review/approval workflows with structured feedback |
+| **Decorator** | None — call `self.ask()` anywhere | `@human_feedback` on the method |
+
+
+Both features coexist — you can use `self.ask()` and `@human_feedback` in the same flow. Use `self.ask()` for inline data gathering and `@human_feedback` for structured review gates.
+
+
### Human in the Loop (human feedback)