Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/vrashmanyu605-eng/Agentic_Sales-Markerting/llms.txt

Use this file to discover all available pages before exploring further.

graph/workflow.py is the single source of truth for how agents connect. It registers all nodes, sets the entry point, defines static edges for the sequential pipeline, and adds a conditional edge from the supervisor that either starts the next lead’s pipeline or terminates the graph. Understanding this file tells you exactly what runs, in what order, and when the workflow stops.

Full source

graph/workflow.py
from langgraph.graph import StateGraph, END
from graph.state import SalesMarketingState
from agents.supervisor import supervisor_agent
from agents.client_sentiment_agent import client_sentiment_agent
from agents.competitor_analysis_agent import competitor_analysis_agent
from agents.icp_matching_agent import icp_matching_agent
from agents.lead_research_agent import lead_research_agent
from agents.marketing_content_agent import marketing_content_agent
from agents.outreach_generation_agent import outreach_generation_agent
from agents.proposal_generation_agent import proposal_generation_agent
from agents.sales_strategy_agent import sales_strategy_agent
from agents.crm_update_agent import crm_update_agent
from agents.discovery_agent import discovery_agent


graph = StateGraph(SalesMarketingState)

# ── Nodes ──────────────────────────────────────────────────────────────────────
graph.add_node("discovery_agent",          discovery_agent)
graph.add_node("supervisor",               supervisor_agent)
graph.add_node("lead_research_agent",      lead_research_agent)
graph.add_node("icp_matching_agent",       icp_matching_agent)
graph.add_node("competitor_analysis_agent",competitor_analysis_agent)
graph.add_node("outreach_generation_agent",outreach_generation_agent)
graph.add_node("proposal_generation_agent",proposal_generation_agent)
graph.add_node("sales_strategy_agent",     sales_strategy_agent)
graph.add_node("marketing_content_agent",  marketing_content_agent)
graph.add_node("client_sentiment_agent",   client_sentiment_agent)
graph.add_node("crm_update_agent",         crm_update_agent)


# ── Entry ──────────────────────────────────────────────────────────────────────
graph.set_entry_point("discovery_agent")

graph.add_edge("discovery_agent", "supervisor")


# ── Supervisor router ──────────────────────────────────────────────────────────
def supervisor_router(state: SalesMarketingState) -> str:
    next_agent = state.get("next_agent", "finished")
    print(f"\n[ROUTER] -> {next_agent}")
    return next_agent

graph.add_conditional_edges(
    "supervisor",
    supervisor_router,
    {
        # Only these two outcomes are valid from supervisor
        "lead_research_agent": "lead_research_agent",
        "finished":             END,
    }
)


# ── Deterministic sequential pipeline ─────────────────────────────────────────
graph.add_edge("lead_research_agent",       "icp_matching_agent")
graph.add_edge("icp_matching_agent",        "competitor_analysis_agent")
graph.add_edge("competitor_analysis_agent", "outreach_generation_agent")
graph.add_edge("outreach_generation_agent", "proposal_generation_agent")
graph.add_edge("proposal_generation_agent", "crm_update_agent")
graph.add_edge("crm_update_agent", "supervisor")

app = graph.compile()

Graph initialization

graph = StateGraph(SalesMarketingState)
StateGraph takes a typed state class as its schema. Every node in the graph receives the full SalesMarketingState dict as input and returns a (partial or full) dict that LangGraph merges back into the shared state. No node receives a separate input object — all context flows through state.

Node registration

All ten agents are registered with graph.add_node. The string name used here is what the router returns to select the next node, and what appears in app.stream() event keys.
Node nameAgent function
"discovery_agent"discovery_agent
"supervisor"supervisor_agent
"lead_research_agent"lead_research_agent
"icp_matching_agent"icp_matching_agent
"competitor_analysis_agent"competitor_analysis_agent
"outreach_generation_agent"outreach_generation_agent
"proposal_generation_agent"proposal_generation_agent
"sales_strategy_agent"sales_strategy_agent
"marketing_content_agent"marketing_content_agent
"client_sentiment_agent"client_sentiment_agent
"crm_update_agent"crm_update_agent
sales_strategy_agent, marketing_content_agent, and client_sentiment_agent are registered but not connected by edges in the current pipeline. You can wire them in by adding add_edge calls.

Entry point

graph.set_entry_point("discovery_agent")
discovery_agent is always the first node to execute. It receives the full initial_state from main.py, searches for leads, and populates pending_leads. The next static edge sends it directly to the supervisor.

Static edge: discovery to supervisor

graph.add_edge("discovery_agent", "supervisor")
After discovery completes, execution always moves to the supervisor. There is no conditional branching here — the supervisor is the only agent that decides what happens next.

Conditional edge: supervisor router

def supervisor_router(state: SalesMarketingState) -> str:
    next_agent = state.get("next_agent", "finished")
    return next_agent

graph.add_conditional_edges(
    "supervisor",
    supervisor_router,
    {
        "lead_research_agent": "lead_research_agent",
        "finished":             END,
    }
)
The supervisor_router function is the only point of dynamic routing in the graph. It reads state["next_agent"], which supervisor_agent sets based on whether pending_leads is empty:
  • "lead_research_agent" — a lead is available; start its pipeline
  • "finished" — no leads remain; terminate the graph via END
The mapping dict passed to add_conditional_edges constrains the router to exactly these two outcomes. Returning any other string raises a LangGraph error.
The supervisor terminates the workflow by returning next_agent: "finished" when pending_leads is empty. The supervisor_router maps "finished" to LangGraph’s built-in END sentinel, which stops graph execution cleanly. You do not need to handle termination yourself.

Deterministic sequential pipeline

graph.add_edge("lead_research_agent",       "icp_matching_agent")
graph.add_edge("icp_matching_agent",        "competitor_analysis_agent")
graph.add_edge("competitor_analysis_agent", "outreach_generation_agent")
graph.add_edge("outreach_generation_agent", "proposal_generation_agent")
graph.add_edge("proposal_generation_agent", "crm_update_agent")
graph.add_edge("crm_update_agent",          "supervisor")
These six add_edge calls define the per-lead pipeline. Every agent runs in this fixed order; there is no branching or skipping. The final edge returns control to the supervisor, closing the loop. The loop works as follows:
  1. Supervisor pops lead #1, sets next_agent: "lead_research_agent"
  2. Pipeline runs: lead research → ICP matching → competitor analysis → outreach → proposal → CRM update
  3. crm_update_agent finishes; the edge sends execution back to the supervisor
  4. Supervisor pops lead #2 (or sets next_agent: "finished" if the queue is empty)
  5. Repeat until the queue is drained

Compiled app and streaming

app = graph.compile()
graph.compile() validates all edges and produces an executable app. In main.py, you run the graph with:
main.py
for event in app.stream(initial_state):
    for node, state in event.items():
        # node is the name of the agent that just ran
        # state is the full SalesMarketingState after that agent
        print(f"NODE: {node.upper()}")
app.stream() yields one event dict per node execution. Each dict has a single key — the node name — mapped to the state snapshot after that node ran. This lets you inspect intermediate outputs without waiting for the full workflow to complete.

Learn more

State reference

All fields in SalesMarketingState — what each agent reads and writes

Architecture overview

Three-layer design and the full pipeline execution sequence

Build docs developers (and LLMs) love