# Unexpected behavior with @listen and missing nodes in Flow graph

**URL:** <https://community.crewai.com/t/unexpected-behavior-with-listen-and-missing-nodes-in-flow-graph/5862>\
**Category:** General\
**Created:** [May 19, 2025, 5:01pm UTC](https://community.crewai.com/t/unexpected-behavior-with-listen-and-missing-nodes-in-flow-graph/5862 "2025-05-19T17:01:05Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![simone\_russo](https://sea1.discourse-cdn.com/flex025/user_avatar/community.crewai.com/simone_russo/32/3485_2.png) [@simone\_russo](https://community.crewai.com/u/simone_russo)\
**Post date:** [May 19, 2025, 5:01pm UTC](https://community.crewai.com/t/unexpected-behavior-with-listen-and-missing-nodes-in-flow-graph/5862/1 "2025-05-19T17:01:05Z")

</div>

Hi everyone,

I’m experimenting with `crewai` and trying to build a simple ticket handling flow using the `Flow` API. Here’s my code:

```
from crewai.flow.flow import Flow, listen, router, start, or_
from dotenv import load_dotenv
from litellm import completion
import os
import json

class CustomerSupportFlow(Flow):
    def __init__ (self):
        super(). __init__ ()
        load_dotenv()
        self.llm_model = os.getenv("MODEL")
        
    
    @start()
    def receive_ticket(self):
        # Inizializza il ticket
        self.ticket_customer = "Mario Rossi"
        self.ticket_issue = "Problema con l'ordine"
        self.ticket_status = "Aperto"
        print(f"Ticket ricevuto: {self.ticket_customer}, {self.ticket_issue}, {self.ticket_status}")
    
    @listen(receive_ticket)
    def categorize_ticket(self):
        print("Inizio categorizzazione del ticket...")
        self.ticket_status = "in attesa"
        
        with open('config/prompts.json', 'r') as f:
            prompts = json.load(f)

        messages = [
            {**p, "content": p["content"].format(issue=self.ticket_issue)}
            for p in prompts["classification"]
        ]
        
        response = completion(
            model=self.llm_model,
            messages=messages,
        )

        category = response["choices"][0]["message"]["content"]
        self.ticket_category = category
        
        print(f"Ticket categorizzato: {category}")
            
    @router(categorize_ticket)
    def route_ticket(self):
        print("Inidirizzamento del ticket al giusto team...")
        return self.ticket_category.lower().replace(" ", "_")
    
    @listen("accesso_account")
    def handle_account_access(self):
        print("Ticket indirizzato al team di gestione account...")
        self.ticket_status = "in lavorazione"
        self.ticket_resolution = "Gestione account completata"
        return "Gestione completata"
    
    @listen("fatturazione")
    def handle_billing(self):
        print("Ticket indirizzato al team di gestione fatturazione...")
        self.ticket_status = "in lavorazione"
        self.ticket_resolution = "Gestione fatturazione completata"
        return "Gestione completata"
    
    @listen(or_("problema_tecnico", "richiesta_di_funzionalità", "altro"))
    def handle_other_issues(self):
        print("Non esiste un team per gestire il problema...")
        self.ticket_status = "in lavorazione"
        self.ticket_resolution = "Problema in fase di risoluzione"
        return "Gestione completata"
    
    
    @listen("Gestione completata")
    def close_ticket(self):
        print("Chiusura del ticket...")
        self.ticket_status = "chiuso"
        print(f"Ticket chiuso: {self.ticket_customer}, {self.ticket_issue}, {self.ticket_status}")

```

When I call `plot()` to visualize the flow, I get the following warnings:

```
Warning: No node found for 'accesso_account' or 'handle_account_access'. Skipping edge.
Warning: No node found for 'fatturazione' or 'handle_billing'. Skipping edge.
Warning: No node found for 'problema_tecnico' or 'handle_other_issues'. Skipping edge.
Warning: No node found for 'richiesta_di_funzionalità' or 'handle_other_issues'. Skipping edge.
Warning: No node found for 'altro' or 'handle_other_issues'. Skipping edge.
Warning: No node found for 'Gestione completata' or 'close_ticket'. Skipping edge.

```

As a result, the graph is incomplete, and the downstream nodes are not rendered.

**Questions** :

1. Am I using the `@listen` decorator correctly with string labels (e.g., `"accesso_account"`)?
2. Does `route_ticket` need to emit events in a specific format for the router to resolve to the appropriate listener?
3. Are string labels in `@listen("...")` required to match something declared earlier in the flow?

Any help understanding why these nodes aren’t being recognized would be greatly appreciated!

Thanks in advance!

---

<div class="post-metadata">

**Author:** ![maxmoura](https://sea1.discourse-cdn.com/flex025/user_avatar/community.crewai.com/maxmoura/32/4206_2.png) [@maxmoura](https://community.crewai.com/u/maxmoura)\
**Post date:** [May 19, 2025, 6:03pm UTC](https://community.crewai.com/t/unexpected-behavior-with-listen-and-missing-nodes-in-flow-graph/5862/2 "2025-05-19T18:03:41Z")

</div>

Relying on an LLM to pick the category? That’s gonna hit you with two main problems:

1. `plot()` has absolutely no idea what string your router is going to generate, so it can’t possibly guess the potential routes.
2. Your LLM, no matter how good it is, can just hallucinate or go off-script and reply with something like `"ho scelto la fatturazione!"`. Next thing you know, you’re on a one-way trip to Narnia. Seriously, at the bare minimum, you need a check like this:

```python
if "accesso_account" in self.ticket_category.lower():
    return "accesso_account"
elif "fatturazione" in self.ticket_category.lower():
    return "fatturazione"
# Add more elif blocks here for other valid categories...
else:
    raise ValueError(f"Invalid ticket category from LLM: '{self.ticket_category}'.")

```

Another key thing: routers should _only_ return strings, not the other steps in the flow. All the other steps in the flow need to listen for the function _name_ (meaning, they listen for when functions complete, not for the strings those functions return). So, `close_ticket` isn’t listening for `"Gestione completata"`, and `return "Gestione completata"` just won’t cut it. You’ve got to fix that step too.

---

<div class="post-metadata">

**Author:** ![simone\_russo](https://sea1.discourse-cdn.com/flex025/user_avatar/community.crewai.com/simone_russo/32/3485_2.png) [@simone\_russo](https://community.crewai.com/u/simone_russo)\
**Post date:** [May 20, 2025, 12:57pm UTC](https://community.crewai.com/t/unexpected-behavior-with-listen-and-missing-nodes-in-flow-graph/5862/3 "2025-05-20T12:57:05Z")

</div>

> [@maxmoura](#):
>
> ```auto
> if "accesso_account" in self.ticket_category.lower():
> return "accesso_account"
> elif "fatturazione" in self.ticket_category.lower():
> return "fatturazione"
> # Add more elif blocks here for other valid categories...
> else:
> raise ValueError(f"Invalid ticket category from LLM: '{self.ticket_category}'.")
> 
> ```

Thanks Max, your reply really helped clarify a lot of things!

Quick follow-up to make sure I’ve got it right: if I use string labels like `"accesso_account"` in both the `@listen(...)` decorators and as explicit return values from the router, everything wires up correctly and `plot()` can render the flow.

But I was wondering — **what happens if I use an `Enum` instead**?  
For example, I’m thinking of defining something like this:

```python
from enum import Enum

class TicketCategory(Enum):
    ACCOUNT = "accesso_account"
    BILLING = "fatturazione"
    TECHNICAL = "problema_tecnico"
    OTHER = "altro"

```

Then in `route_ticket`, I’d return `TicketCategory.ACCOUNT.value`, and use the same `.value` in the `@listen(...)` decorators.

Would `plot()` still work in this case? Or does it expect **literal strings only** , and enum indirection would break the static graph resolution?

Thanks again for the guidance!

---

<div class="post-metadata">

**Author:** ![maxmoura](https://sea1.discourse-cdn.com/flex025/user_avatar/community.crewai.com/maxmoura/32/4206_2.png) [@maxmoura](https://community.crewai.com/u/maxmoura)\
**Post date:** [May 20, 2025, 4:57pm UTC](https://community.crewai.com/t/unexpected-behavior-with-listen-and-missing-nodes-in-flow-graph/5862/4 "2025-05-20T16:57:57Z")

</div>

Yeah, Simone, that’s an interesting question.

So, `plot()` is going to look for methodS (you can have more than one) decorated with `@router` and use some low-level inspection with the `ast` (Abstract Syntax Tree) module to parse the source code. And what exactly is it looking for? It’s specifically targeting `return` statements. If the value being returned is an `ast.Constant` where the `value` attribute is a `str`, it means you’re returning a _string literal_ (definitely keep that in mind). Then, it gathers all those unique string literals found in the `return` statements. For every `router_name` and its `possible_paths`, it attempts to draw an edge from the `router_name` node to each `path_name` node, got it? And a `path_name` node (like your `"accesso_account"`) will only get drawn if there’s a listener method explicitly decorated with `@listen("accesso_account")`. Sweet.

Based on this logic, if I had to bet a dime, I’d say _no, your `Enum` isn’t explicit enough to be picked up as a string literal_ by `plot()`. For heaven’s sake, just be crystal clear with your string routes!

So, go ahead and build your own test to see if I owe you a dime or if you owe me one. Keep in mind, I was only thinking about how `plot()` works, which is all _a priori_ stuff; your `Enum` logic might still totally work at runtime, _a posteriori_. 🙂

---

<div class="post-metadata">

**Author:** ![simone\_russo](https://sea1.discourse-cdn.com/flex025/user_avatar/community.crewai.com/simone_russo/32/3485_2.png) [@simone\_russo](https://community.crewai.com/u/simone_russo)\
**Post date:** [May 21, 2025, 9:50am UTC](https://community.crewai.com/t/unexpected-behavior-with-listen-and-missing-nodes-in-flow-graph/5862/5 "2025-05-21T09:50:46Z")

</div>

> [@maxmoura](#):
>
> So, `plot()` is going to look for methodS (you can have more than one) decorated with `@router` and use some low-level inspection with the `ast` (Abstract Syntax Tree) module to parse the source code. And what exactly is it looking for? It’s specifically targeting `return` statements. If the value being returned is an `ast.Constant` where the `value` attribute is a `str`, it means you’re returning a _string literal_ (definitely keep that in mind). Then, it gathers all those unique string literals found in the `return` statements. For every `router_name` and its `possible_paths`, it attempts to draw an edge from the `router_name` node to each `path_name` node, got it? And a `path_name` node (like your `"accesso_account"`) will only get drawn if there’s a listener method explicitly decorated with `@listen("accesso_account")`. Sweet.
> 
> Based on this logic, if I had to bet a dime, I’d say _no, your `Enum` isn’t explicit enough to be picked up as a string literal_ by `plot()`. For heaven’s sake, just be crystal clear with your string routes!
> 
> So, go ahead and build your own test to see if I owe you a dime or if you owe me one. Keep in mind, I was only thinking about how `plot()` works, which is all _a priori_ stuff; your `Enum` logic might still totally work at runtime, _a posteriori_.

You’re right: it actually doesn’t work.  
You won the dime 🪙 😄

Apparently, `plot()` really wants to see plain, raw string literals in the `return` statements. The Enum, as elegant and valid as it is at runtime, is just too “opaque” for its static analysis.
