Skip to navigation

Triggering flows

This page requires Python familiarity for the programmatic examples. Non-technical operators can trigger flows from FAQs actions without writing code – see Start a flow from a FAQs action below. All code-focused content is also available in the Developer tab.

A flow starts when something calls conv.goto_flow("Flow name"). This may happen through a FAQs action or programmatically inside a function.

This page explains how conv.goto_flow can be used:

You can call conv.goto_flow(...) from any function in Agent Studio, including:

Example:

def start_booking(conv: Conversation):
conv.goto_flow("Make a booking")
return

What happens:

  • The function runs during the current turn.
  • When the turn completes, the agent enters the named flow.
  • The flow begins at its configured start step.

Start a flow from a FAQs action

A Managed Topic can trigger a flow when it matches a user request. This is the primary way to collect entities from a topic, since FAQs do not support entity extraction directly.

Using the /Flow shortcut (no code)

example-main

Inside a FAQs Actions field, type /Flow and use the (+) option to create or attach a flow. When the topic matches, the agent enters the selected flow at its start step.

No custom function is required if you just need to enter the flow.

Using a tool call from a topic

If you need additional logic before entering the flow – for example, storing context so the agent can return to the topic afterward – use a tool call action instead:

def start_verification_flow(conv: Conversation, original_faq: str):
"""
Enter the verification flow and remember which topic we came from.
Args:
original_faq: the name of the topic that triggered this function
"""
conv.state.original_topic = original_faq
conv.goto_flow("Verify Identity")
return

You can also trigger a flow from a global function by calling conv.goto_flow() directly.

Returning to a topic after a flow

When a topic triggers a flow (e.g. to collect a date or verify identity), the agent can return to the original topic content after the flow exits. To do this:

  1. Before entering the flow, store the topic name in state (e.g. conv.state.original_topic).
  2. In the flow’s exit function, check the stored topic and return a prompt that directs the LLM to the relevant topic content:

What is an “exit function”? There is no special “exit function” step type. An exit function is just a transition function on the final step of the flow that calls conv.exit_flow() and returns a payload (typically {"content": ...} or {"utterance": ...}). It runs because the step runs — not because the flow registers an “on-exit hook” — and its return value becomes the function output the agent uses next. Mutating conv.state from inside it is the same as from any other function: conv.state["is_verified"] = True (or conv.state.is_verified = True) takes effect immediately and persists across subsequent turns.

def exit_and_resume(conv: Conversation, flow: Flow):
conv.exit_flow()
if conv.state.original_topic:
return {
"content": f"Look for the topic '{conv.state.original_topic}' in your context. "
f"Continue answering the caller's original question."
}
return {"utterance": "Is there anything else I can help with?"}

This pattern avoids a generic “Is there anything else?” when the caller’s original question has not yet been answered.

Common mistakes

conv.goto_flow() only queues a transition — it does not pause your function, run the target flow inline, and resume. The flow runs on subsequent turns. The following anti-patterns all stem from forgetting that.

Anti-pattern: checking the verification result inline after goto_flow().

# Don't do this
def identify_and_verify_user(conv: Conversation, account_type: str):
if conv.state.is_verified == False:
conv.state.original_topic = "check_account_balance"
conv.goto_flow("Identify & Verify User")
# ⚠️ Nothing below runs as you'd expect:
# - The verification flow hasn't run yet (it runs next turn).
# - `verification_successful` is undefined.
# - The branch reading `conv.state.is_verified` runs against
# the value from BEFORE the flow.
if verification_successful:
conv.state.is_verified = True
return check_account_balance(conv, conv.state.pending_account_type)

Why it fails: conv.goto_flow(...) returns immediately and the rest of the function executes on the same turn — before the verification flow has had a chance to run. Any code that reads the result of the flow on the same call will see stale or undefined values.

Correct pattern: store context, call conv.goto_flow(...), return — and resume in the flow’s exit function:

def identify_and_verify_user(conv: Conversation, account_type: str):
if not conv.state.get("is_verified"):
conv.state.pending_account_type = account_type
conv.state.original_topic = "check_account_balance"
conv.goto_flow("Identify & Verify User")
return "Tell the user we need to verify their identity first."
# Already verified — proceed with the original task
return check_account_balance(conv, account_type)

Inside the verification flow, its steps set conv.state["is_verified"] (and any other context). The flow’s exit function reads that state and routes back:

def exit_and_resume(conv: Conversation, flow: Flow):
conv.exit_flow()
if conv.state.get("is_verified") and conv.state.get("original_topic") == "check_account_balance":
return {
"content": (
f"The caller has been verified. "
f"Resume the '{conv.state.original_topic}' topic "
f"with account_type='{conv.state.pending_account_type}'."
)
}
return {"utterance": "Tell the user we could not verify their identity."}

Anti-pattern: if conv.state.is_verified == False:

When is_verified has never been written, conv.state.is_verified returns None, not False. None == False is False, so the guard is silently skipped and the unverified user falls through to the protected code.

# Don't do this — silently lets unverified callers through
if conv.state.is_verified == False:
...
# Do this — treats missing and falsy the same way
if not conv.state.get("is_verified"):
...

Anti-pattern: mutual recursion between a protected function and its guard.

# Don't do this
def check_account_balance(conv: Conversation, account_type: str):
if not conv.state.get("is_verified"):
return identify_and_verify_user(conv) # recurses; returns None on cold-start
...
def identify_and_verify_user(conv: Conversation):
if not conv.state.get("is_verified"):
conv.goto_flow("Identify & Verify User")
return "Tell the user we need to verify their identity first."
# Unreachable on the first call — the function above returned already
return check_account_balance(conv, conv.state.pending_account_type)

Why it fails: on the first (unverified) call, identify_and_verify_user queues goto_flow and returns a string — but execution never comes back into check_account_balance on this turn, so callers reading the return of check_account_balance get None or the verification string instead of a balance response.

Correct pattern: only the entry point calls goto_flow + return. Post-flow resumption is done in the target flow’s exit function, not by calling the original function recursively.

Preventing verification loops

A protected function that calls conv.goto_flow("Identify & Verify User") whenever conv.state.is_verified is falsy will keep sending the user back into verification — every turn, forever — unless two things are true:

  • The verification flow writes is_verified = True on the success path.
  • The protected function has a terminal branch for repeated failures.

Skip either one and the caller is trapped in the loop. The walkthrough below sets up both halves of the fix.

1

Hand off cleanly from the protected function

The entry point stores any context the flow will need on resume, calls conv.goto_flow(...), and returns. It must not try to read the verification result on the same turn — goto_flow only queues the transition.

def check_account_balance(conv: Conversation, account_type: str):
if not conv.state.get("is_verified"):
conv.state.original_topic = "check_account_balance"
conv.state.pending_account_type = account_type
conv.goto_flow("Identify & Verify User")
return "Tell the user we need to verify their identity first."
...
2

Set is_verified on success and escalate after repeated failures

Inside the verification flow, a transition function on the verification step compares user input to the expected value. On success it writes is_verified = True; on failure it increments an attempts counter and routes to escalation once the counter hits 3.

def check_verification(conv: Conversation, flow: Flow):
if conv.state.get("user_input") == conv.state.get("expected_value"):
conv.state.is_verified = True
flow.goto_step("Verification succeeded")
return
attempts = conv.state.get("verification_attempts", 0) + 1
conv.state.verification_attempts = attempts
if attempts >= 3:
conv.goto_flow("Escalation")
return
flow.goto_step("Retry verification")
return

Without the success-path write, the protected function’s is_verified check stays falsy and the loop repeats on every turn.

3

Add a terminal branch in the protected function

Even with the flow setting is_verified correctly, the caller still needs its own guard on the attempts counter. Otherwise a user who fails three times and then returns to the original topic re-triggers verification a fourth time.

def check_account_balance(conv: Conversation, account_type: str):
if conv.state.get("verification_attempts", 0) >= 3:
return "Tell the user we cannot continue without successful identity verification."
if not conv.state.get("is_verified"):
conv.state.original_topic = "check_account_balance"
conv.state.pending_account_type = account_type
conv.goto_flow("Identify & Verify User")
return "Tell the user we need to verify their identity first."
...

For the in-flow version of the same escalation pattern — when the failure happens inside a flow rather than at a protected entry point — see Escalating after repeated failed verification below.

Start or switch flows from inside another flow

Once a user is inside a flow, most movement should happen using flow.goto_step(...).

That keeps the user inside the same workflow and branches to another step.

However, sometimes you need to switch to an entirely different workflow.

Examples:

  • The user fails identity verification and must enter a verification flow.
  • The user requests a human agent and must enter an escalation flow.
  • A compliance rule requires a separate structured process.
  • The user changes intent entirely (e.g., from booking to cancellation).

In these cases, start a new flow instead of branching.

You do this by calling conv.goto_flow("Flow name") from a function inside the current flow.

Example: Escalating after repeated failed verification

Imagine a booking flow where the user must confirm their date of birth. If they fail verification too many times, you want to move them into a dedicated verification flow.

def check_verification(conv: Conversation, flow: Flow):
attempts = conv.state.verification_attempts or 0
if not conv.state.is_verified:
attempts += 1
conv.state.verification_attempts = attempts
if attempts >= 3:
conv.goto_flow("Identity Verification")
return
flow.goto_step("Retry verification")
return
flow.goto_step("Continue booking")
return
Inside a function, only the last flow.goto_step(...) call is executed

Routing based on API results

Main article: conv.api

A function may call an API using conv.api.

After receiving a response, explicitly choose one of two actions:

  • Stay in the current workflow using flow.goto_step(...)
  • Switch workflows using conv.goto_flow(...)

Example:

def check_customer_status(conv: Conversation, flow: Flow):
response = conv.api.crm.lookup_user(id=conv.state.user_id)
if response.status_code != 200:
flow.goto_step("API error handling")
return
data = response.json()
if data.get("status") == "requires_verification":
conv.goto_flow("Verification flow")
return
flow.goto_step("Continue booking")
return

Next steps