> ## Documentation Index
> Fetch the complete documentation index at: https://docs.poly.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Tutorial: Flow fundamentals

> PolyAcademy Level 3 – Use flows to enforce multi-step sequences where order matters.

export const LessonMeta = ({level, difficulty, time}) => {
  const levelConfig = {
    1: {
      badge: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
      label: 'Level 1'
    },
    2: {
      badge: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
      label: 'Level 2'
    },
    3: {
      badge: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
      label: 'Level 3'
    }
  };
  const difficultyConfig = {
    Beginner: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
    Intermediate: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200',
    Advanced: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
  };
  const lvl = levelConfig[level] || levelConfig[1];
  const diffColor = difficultyConfig[difficulty] || difficultyConfig['Beginner'];
  return <div className="flex flex-wrap items-center gap-2 my-4 not-prose">
      <span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${lvl.badge}`}>
        {lvl.label}
      </span>
      <span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${diffColor}`}>
        {difficulty}
      </span>
      {time && <span className="inline-flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400">
          <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
          </svg>
          {time}
        </span>}
    </div>;
};

export const FillBlank = ({prompt, answer, hint, explanation}) => {
  const [value, setValue] = useState('');
  const [submitted, setSubmitted] = useState(false);
  const normalize = s => s.trim().toLowerCase().replace(/[^a-z0-9_]/g, '');
  const answers = Array.isArray(answer) ? answer : [answer];
  const isCorrect = answers.some(a => normalize(value) === normalize(a));
  const handleSubmit = e => {
    e.preventDefault();
    if (value.trim()) setSubmitted(true);
  };
  const handleReset = () => {
    setValue('');
    setSubmitted(false);
  };
  return <div className="my-6">
      <p className="mt-0 mb-3 text-sm font-semibold leading-relaxed text-gray-900 dark:text-gray-100">
        {prompt}
      </p>
      <form onSubmit={handleSubmit} className="flex flex-col gap-2.5">
        <div className="flex gap-2">
          <input type="text" value={value} onChange={e => {
    setValue(e.target.value);
    setSubmitted(false);
  }} placeholder={hint || "Type your answer…"} className="flex-1 rounded-xl border py-2.5 px-4 text-sm font-mono border-gray-200 bg-white text-gray-900 placeholder-gray-400 outline-none focus:border-gray-400 focus:ring-2 focus:ring-gray-200 transition-all duration-150 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-600 dark:focus:border-gray-500 dark:focus:ring-gray-700" />
          <button type="submit" className="rounded-xl border py-2.5 px-5 text-sm font-medium transition-all duration-150 border-gray-800 bg-gray-800 text-white hover:bg-gray-700 hover:border-gray-700 dark:border-gray-200 dark:bg-gray-200 dark:text-gray-900 dark:hover:bg-white">
            Check
          </button>
        </div>
        {submitted ? <div className={`py-3 pl-4 pr-3.5 rounded-r-xl text-sm leading-relaxed border-l-4 ${isCorrect ? 'border-green-500 bg-green-50 dark:bg-green-900 dark:border-green-500' : 'border-red-500 bg-red-50 dark:bg-red-900 dark:border-red-500'}`}>
            {isCorrect ? <>
                <span className={`font-semibold !text-green-800 dark:!text-green-200`}>Correct.</span>{' '}
                <span className="!text-gray-700 dark:!text-gray-300">{explanation}</span>
              </> : <>
                <span className="font-semibold !text-red-800 dark:!text-red-200">Not quite.</span>{' '}
                <span className="!text-gray-700 dark:!text-gray-300">The answer is <code className="!text-gray-800 dark:!text-gray-200">{answers[0]}</code>. {explanation}</span>
              </>}
          </div> : null}
      </form>
      {submitted ? <button type="button" onClick={handleReset} className="mt-2 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 underline underline-offset-2 cursor-pointer transition-colors duration-150">
          Try again
        </button> : null}
    </div>;
};

export const Quiz = ({questions = []}) => {
  const [selected, setSelected] = useState({});
  const [resetCount, setResetCount] = useState(0);
  const letters = ['A', 'B', 'C', 'D'];
  const handleSelect = (qIdx, optIdx) => {
    if (selected[qIdx] !== undefined) return;
    setSelected(prev => ({
      ...prev,
      [qIdx]: optIdx
    }));
  };
  const handleReset = () => {
    setSelected({});
    setResetCount(c => c + 1);
  };
  if (!questions?.length) return null;
  const getOptionClasses = ({hasAnswered, isThisCorrect, isThisSelected}) => {
    if (!hasAnswered) {
      return {
        btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-pointer border-gray-200 bg-white text-gray-700 hover:border-gray-300 hover:bg-gray-50 hover:shadow-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:border-gray-500 dark:hover:bg-gray-700',
        badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-300',
        icon: null
      };
    }
    if (isThisCorrect) {
      return {
        btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-default border-green-400 bg-green-50 text-green-900 font-medium dark:border-green-500 dark:bg-green-950 dark:text-green-100',
        badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-green-500 text-white dark:bg-green-500',
        icon: <svg className="shrink-0 w-4 h-4 text-green-500 dark:text-green-400 ml-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
          </svg>
      };
    }
    if (isThisSelected) {
      return {
        btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-default border-red-400 bg-red-50 text-red-900 dark:border-red-500 dark:bg-red-950 dark:text-red-100',
        badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-red-500 text-white dark:bg-red-500',
        icon: <svg className="shrink-0 w-4 h-4 text-red-400 dark:text-red-400 ml-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
            <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
          </svg>
      };
    }
    return {
      btn: 'flex w-full items-center gap-3 py-2.5 px-4 rounded-xl text-sm leading-normal transition-all duration-150 text-left border cursor-default border-gray-100 bg-white text-gray-400 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-500',
      badge: 'w-6 h-6 rounded-full text-xs font-bold flex items-center justify-center shrink-0 leading-none transition-all duration-150 bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-500',
      icon: null
    };
  };
  return <div key={resetCount} className="my-6">
      {questions.map((q, qIdx) => {
    const answer = selected[qIdx];
    const hasAnswered = answer !== undefined;
    const isCorrect = answer === q.correct;
    return <div key={String(qIdx)} className="mb-8">
            <p className="flex items-start gap-2.5 font-semibold text-sm mb-3 mt-0 leading-relaxed text-gray-900 dark:text-gray-100">
              <span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-800 dark:bg-gray-200 text-white dark:text-gray-900 text-xs font-bold shrink-0 mt-px leading-none">
                {qIdx + 1}
              </span>
              {q.q}
            </p>

            <div className="flex flex-col gap-2">
              {q.options.map((opt, i) => {
      const isThisCorrect = i === q.correct;
      const isThisSelected = i === answer;
      const {btn, badge, icon} = getOptionClasses({
        hasAnswered,
        isThisCorrect,
        isThisSelected
      });
      return <button key={String(i)} type="button" onClick={() => handleSelect(qIdx, i)} className={btn}>
                    <span className={badge}>{letters[i]}</span>
                    <span className="flex-1">{opt}</span>
                    {icon}
                  </button>;
    })}
            </div>

            {hasAnswered ? <div className={`mt-3 py-3 pl-4 pr-3.5 rounded-r-xl text-sm leading-relaxed border-l-4 ${isCorrect ? 'border-green-500 bg-green-50 dark:bg-green-950 dark:border-green-500' : 'border-red-500 bg-red-50 dark:bg-red-950 dark:border-red-500'}`}>
                <span className={`font-semibold ${isCorrect ? '!text-green-800 dark:!text-green-200' : '!text-red-800 dark:!text-red-200'}`}>
                  {isCorrect ? 'Correct.' : 'Not quite.'}
                </span>{' '}
                <span className="!text-gray-700 dark:!text-gray-300">{q.explanation}</span>
              </div> : null}
          </div>;
  })}

      <button type="button" onClick={handleReset} className="mt-1 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 underline underline-offset-2 cursor-pointer transition-colors duration-150">
        Reset quiz
      </button>
    </div>;
};

export const ProgressTracker = ({lessonNum, totalLessons, level}) => {
  const [checked, setChecked] = useState(false);
  return <div onClick={() => setChecked(prev => !prev)} className={checked ? 'flex items-center gap-3 p-4 rounded-lg border-2 border-green-600 bg-green-50 dark:bg-green-950 cursor-pointer select-none transition-all' : 'flex items-center gap-3 p-4 rounded-lg border-2 border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 cursor-pointer select-none transition-all'}>
      <div className={checked ? 'w-5 h-5 rounded border-2 border-green-600 bg-green-600 flex items-center justify-center shrink-0 transition-all' : 'w-5 h-5 rounded border-2 border-gray-400 dark:border-gray-500 bg-white dark:bg-gray-800 flex items-center justify-center shrink-0 transition-all'}>
        {checked ? <svg width="10" height="8" viewBox="0 0 10 8" fill="none">
            <path d="M1 4L3.5 6.5L9 1" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
          </svg> : null}
      </div>
      <div>
        <div className={checked ? 'font-semibold text-sm text-green-700 dark:text-green-300' : 'font-semibold text-sm text-gray-700 dark:text-gray-200'}>
          {checked ? 'Lesson complete' : 'Mark lesson complete'}
        </div>
        {lessonNum && totalLessons ? <div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
            {level ? level + ' - ' : ''}Lesson {lessonNum} of {totalLessons}
          </div> : null}
      </div>
    </div>;
};

<Info>
  **Level 3 – Lesson 2 of 5** – Understand flows, steps, and transitions for structured multi-turn interactions.
</Info>

<LessonMeta level={3} difficulty="Advanced" time="20 min" />

Flows enforce a specific sequence of steps – the step prompt stays anchored at the end of conversation history, preventing the model from improvising and keeping it focused on collecting, validating, and acting on information reliably.

Use flows when order matters: collecting a booking reference, verifying identity, routing based on input. Without flows, the LLM guesses the sequence, which leads to inconsistent outcomes.

For reference documentation on flows, see the [Flows overview](/flows/introduction). This lesson focuses on how flows work and when to use them.

## When to use flows vs other tools

<CardGroup cols={3}>
  <Card title="FAQs" icon="message-question">
    Simple question-and-answer. No sequencing needed.
  </Card>

  <Card title="Functions" icon="code">
    Single actions: fetch data, call an API, save a value.
  </Card>

  <Card title="Flows" icon="route">
    Multi-step sequences where order matters and the model must stay on track.
  </Card>
</CardGroup>

If the interaction takes more than 2–3 turns and the steps must happen in a specific order, you probably need a flow.

## How flows work

A flow is a sequence of **steps**. Each step contains:

* **A prompt** – instructions the model follows for this step
* **Functions** – scoped to this step only, used for transitions and actions
* **A transition** – how the flow moves to the next step (or exits)

<Steps>
  <Step title="Enter the flow">
    A start function calls `conv.goto_flow("flow_name")`. This can be triggered from agent behavior, a knowledge topic, or another function.
  </Step>

  <Step title="Step activates">
    The step prompt is injected as a system message at the end of the conversation history. The model follows these instructions.
  </Step>

  <Step title="Model interacts with the user">
    The model asks for information, confirms details, or takes actions according to the step prompt.
  </Step>

  <Step title="Flow function transitions">
    When the model has what it needs, it calls a flow function. That function can save data, call an API, and then move to the next step or exit the flow.
  </Step>
</Steps>

## The persistent prompt

The step prompt is always the **last message** in the conversation history. This is the most important thing to understand about flows.

Unlike content returned from a function (which appears at a specific point in history and becomes less relevant as more turns happen), the step prompt is re-injected on every turn. It cannot be "forgotten" by the model.

<Tabs>
  <Tab title="Function content">
    ```
    assistant: "Hi, how can I help?"
    user: "I want to register"
    function: "Ask the user for their phone number"    ← fades over time
    assistant: "Sure, what's your phone number?"
    user: "Actually, what are your opening hours?"
    assistant: "We're open 9 to 5. Now, about that phone number..."  ← model may drift
    ```

    The function result moves further back in history with each turn.
  </Tab>

  <Tab title="Flow step">
    ```
    assistant: "Hi, how can I help?"
    user: "I want to register"
    assistant: "Sure, what's your phone number?"
    user: "Actually, what are your opening hours?"
    system: "Ask the user for their phone number"    ← always last, always fresh
    ```

    The step prompt stays anchored at the end, keeping the model on task.
  </Tab>
</Tabs>

This persistent control is why flows are more reliable than prompting alone for multi-turn interactions.

## Check your understanding

<Quiz
  questions={[
{
q: "Why is the flow step prompt more persistent than content returned from a function?",
options: [
  "It uses a special LLM mode that locks behavior",
  "It is always re-injected as the last message in conversation history",
  "It overrides all other prompting",
  "It is stored in a separate memory system",
],
correct: 1,
explanation: "The step prompt is always appended as the last system message. This means it stays relevant regardless of how many turns pass – unlike function content, which moves further back in history over time.",
}
]}
/>

## Creating and entering a flow

There are two ways to create a flow:

1. Go to **Flows** and create one directly
2. Create one from any behavior menu (e.g., inside a managed topic action)

When you create a flow, Agent Studio generates:

* The flow itself (with a start step)
* A corresponding **start function** that contains `conv.goto_flow("flow_name")`

The start function is what triggers entry into the flow. You must expose it somewhere – in agent behavior, a knowledge topic, or another function. If a flow has no entry point, it exists but cannot be reached.

<Warning>
  A common mistake: creating a flow but forgetting to expose the start function. The flow exists but is never entered because nothing triggers `conv.goto_flow`.
</Warning>

## Step transitions

Each step shows only its own functions to the model. When the model calls a flow function, that function can:

* Save data (to `conv.state`)
* Call an API
* Move to the next step using `flow.goto_step("step_name")`
* Exit the flow using `conv.exit_flow()`

When transitioning between steps:

* The previous step prompt is removed
* The new step prompt is injected
* Only the new step's functions are visible

This means the model cannot skip ahead, look at future steps, or call functions from a previous step.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def save_phone_number(conv, flow, phone_number: str) -> str:
    conv.state["phone_number"] = phone_number
    flow.goto_step("collect_name")
    return f"Phone number {phone_number} saved. Now ask for their full name."
```

<Quiz
  questions={[
{
q: "You create a flow with three steps but the model skips step 2 and jumps to step 3. What is the most likely cause?",
options: [
  "Step 2's prompt is too short for the model to act on",
  "Step 2 has no prompt defined",
  "A flow function in step 1 transitions directly to step 3 via goto_step",
  "The model can see all steps and chooses the most relevant one",
],
correct: 2,
explanation: "The model cannot see other steps – it only sees the current step prompt and its functions. If a step is skipped, the transition function must be routing directly past it. Open the flow function in step 1 and check whether its goto_step call jumps to step 3 instead of step 2.",
}
]}
/>

<FillBlank prompt="To move from one flow step to the next, call: flow.______(&#x22;step_name&#x22;)" answer={["goto_step"]} hint="What method transitions between steps?" explanation="flow.goto_step(&#x22;step_name&#x22;) removes the current step prompt and injects the new one. The model then only sees the new step and its functions." />

## What happens without transitions

If you tell the model to ask for a phone number but don't define what happens next, the model improvises. It may start asking for names, emails, or other details based on its training patterns.

This is not a model failure – it is a design failure. You did not give it the next step.

<Warning>
  Always define explicit transitions between steps. Never rely on the model to "know what to do next."
</Warning>

## Transition pitfalls

Neither `conv.goto_flow` nor `flow.goto_step` interrupt the execution of your function. If you have multiple conditions, be careful not to overwrite one transition with another.

<Tabs>
  <Tab title="Incorrect">
    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    # If condition_1 and condition_2 are both True,
    # we set step A, then overwrite with B, then C.
    # Final transition is always C.
    if condition_1:
        flow.goto_step("A")
    if condition_2:
        flow.goto_step("B")
    flow.goto_step("C")
    ```
  </Tab>

  <Tab title="Correct (return)">
    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    if condition_1:
        flow.goto_step("A")
        return
    if condition_2:
        flow.goto_step("B")
        return
    flow.goto_step("C")
    ```
  </Tab>

  <Tab title="Correct (elif)">
    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    if condition_1:
        flow.goto_step("A")
    elif condition_2:
        flow.goto_step("B")
    else:
        flow.goto_step("C")
    ```
  </Tab>
</Tabs>

<Warning>
  This is one of the most common bugs in flow functions. Always use explicit `return` statements or `if/elif/else` chains to prevent unintended transition overwrites.
</Warning>

## Exiting a flow

When `conv.exit_flow()` is called:

* The step prompt disappears from conversation history
* The model returns to normal behavior (behavioral rules, knowledge, global functions)
* Any state saved during the flow persists in `conv.state`

## Try it yourself

<Steps>
  <Step title="Challenge: Design a two-step registration flow">
    Design a flow that collects a phone number and then a full name.

    Plan:

    1. What is the start step prompt?
    2. What flow function transitions from step 1 to step 2?
    3. What does the second step prompt say?
    4. How does the flow exit?

    <Accordion title="Hint">
      Each step should have a clear prompt (one instruction) and a flow function that saves the collected value and transitions. The final function should call `conv.exit_flow()`.
    </Accordion>

    <Accordion title="Example solution">
      **Step 1 prompt:** "Ask the user for the phone number we can contact them on. Once they provide it, call `save_phone_number`."

      **Step 1 function (`save_phone_number`):**

      ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
      def save_phone_number(conv, flow, phone_number: str) -> str:
          conv.state["phone_number"] = phone_number
          flow.goto_step("collect_name")
          return "Phone number saved."
      ```

      **Step 2 prompt:** "Ask the user for their full name. Once they provide it, call `save_name`."

      **Step 2 function (`save_name`):**

      ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
      def save_name(conv, flow, full_name: str) -> str:
          conv.state["full_name"] = full_name
          conv.exit_flow()
          return f"Registration complete for {full_name}."
      ```
    </Accordion>
  </Step>
</Steps>

## Check your understanding

<Quiz
  questions={[
{
q: "After exiting a flow, what happens to values saved in conv.state during the flow?",
options: [
  "They are deleted when the flow exits",
  "They persist and remain accessible for the rest of the conversation",
  "They are only available if you re-enter the same flow",
  "They are moved to a separate flow-specific storage",
],
correct: 1,
explanation: "conv.state persists across the entire conversation – it is not scoped to the flow. Values saved during a flow (e.g., conv.state.phone_number) remain accessible after the flow exits, so later functions and topics can use them.",
}
]}
/>

<CardGroup cols={2}>
  <Card title="← Previous: Code organization" icon="arrow-left" href="/learn/guides/expert/code-organization">
    Lesson 1 of 5
  </Card>

  <Card title="Next: Flow patterns →" icon="arrow-right" href="/learn/guides/expert/flow-patterns">
    Lesson 3 of 5
  </Card>
</CardGroup>

<ProgressTracker lessonKey="l3-2-flow-fundamentals" lessonNum={2} totalLessons={5} level="Level 3" />
