DEV Community

Cover image for How to Move an n8n Prototype into a LangGraph Production Agent
Ciphernutz
Ciphernutz

Posted on

How to Move an n8n Prototype into a LangGraph Production Agent

You already have an n8n workflow that functions.

It receives a request, calls APIs, uses an LLM, makes decisions, updates a database, and returns a result.

During the prototype stage, this is often enough.

But as the workflow grows, things can start becoming harder to manage.

  • State is spread across multiple nodes
  • Agent decisions become difficult to trace
  • Retry logic becomes complicated
  • Long-running executions need persistence
  • Human approval needs pause/resume
  • Testing individual decisions becomes harder
  • Business logic gets tightly coupled to orchestration

These are scenarios where moving the agent logic from an n8n prototype into LangGraph becomes purposeful.

Replacing n8n just because LangGraph is newer is not quite the point.

The goal is to move the parts that require stateful agent orchestration, explicit control flow, persistence, and long-running execution into a framework designed for those problems.

This guide walks through that migration step by step.

1. Start With an Actual n8n Prototype

Consider a real estate AI assistant that receives a buyer request:

User Request
     ↓
Extract Requirements
     ↓
Search CRM
     ↓
Assign to AI Agent
     ↓
Call Property Search API
     ↓
Score Results
     ↓
Send Response
     ↓
Update CRM
Enter fullscreen mode Exit fullscreen mode

Consequently, an n8n workflow might contain:

  • Webhook node
  • Set/Edit Fields nodes
  • Code nodes
  • HTTP Request nodes
  • IF/Switch nodes
  • AI Agent node
  • CRM integration
  • WhatsApp/Email node
  • Error handling workflow

This is a perfectly reasonable architecture for a prototype.

However, the problem starts becoming noticeable when the workflow becomes something like:

Webhook
   ↓
20+ nodes
   ↓
Multiple IF branches
   ↓
AI Agent
   ↓
Multiple tool calls
   ↓
Retries
   ↓
Human approval
   ↓
CRM update
   ↓
Follow-up
   ↓
Scheduled continuation
Enter fullscreen mode Exit fullscreen mode

At this point, the workflow is doing more than simple automation.

It is becoming an agentic state machine.

This is the perfect stage to evaluate whether you should move the core agent logic into LangGraph.

2. Before Migrating: Separate the Workflow Into Responsibilities

Don't start rewriting the entire n8n workflow immediately.

First, inspect every node and determine what responsibility it actually performs.

A useful mapping looks like this:

n8n Component Responsibility LangGraph Equivalent
Webhook Receive input API layer
Set/Edit Fields Transform data Python function
Code Business logic Python function
IF/Switch Routing Conditional edge
AI Agent Reasoning Agent/LLM node
HTTP Request External operation Tool
Database Data persistence DB/service
Wait Long-running state Persistence/interrupt
Human approval Manual decision interrupt()
Error workflow Recovery Retry/recovery logic

This classification prevents one of the biggest migration mistakes:

Rewriting the entire system when only the agent orchestration needs to change.

The CRM, property database, or your external APIs don't necessarily need to move.

The migration should focus on the orchestration layer.

3. Build the n8n Version First

Before converting anything, define exactly what the existing workflow does.

For example:

Buyer Request
      ↓
Extract Requirements
      ↓
Search Properties
      ↓
Filter Results
      ↓
AI Ranker
      ↓
Return Recommendations
Enter fullscreen mode Exit fullscreen mode

A buyer might send:

Looking for a 3-bedroom apartment in Dubai Marina
under AED 2 million.
Enter fullscreen mode Exit fullscreen mode

The workflow needs to:

  1. Extract the requirements
  2. Search the property database
  3. Filter out unsuitable properties
  4. Rank the remaining properties
  5. Return recommendations

This gives us a clear baseline for the migration.

4. Define the LangGraph State

This is one of the most important changes during the migration.

In an n8n workflow, execution data naturally flows from one node to another.

With LangGraph, you explicitly define the state shared across the graph.

For example:

from typing import TypedDict

class AgentState(TypedDict):
    user_query: str
    requirements: dict
    candidates: list
    matches: list
    selected_property: dict | None
    error: str | None
Enter fullscreen mode Exit fullscreen mode

Now the agent has an explicit state contract.

The workflow can move through:

user_query
    ↓
requirements
    ↓
candidates
    ↓
matches
    ↓
selected_property
Enter fullscreen mode Exit fullscreen mode

This makes the state easier to inspect, test, persist, and reason about.

A useful rule is:

If a piece of information is required by multiple stages of the agent, consider making it part of the graph state.

5. Map n8n Nodes to LangGraph Nodes

The next step is to convert individual workflow operations into graph nodes.

The original n8n flow:

Webhook
   ↓
Code
   ↓
HTTP Request
   ↓
AI Agent
   ↓
IF
Enter fullscreen mode Exit fullscreen mode

Can become:

START
  ↓
normalize_request
  ↓
search_properties
  ↓
rank_properties
  ↓
route_result
Enter fullscreen mode Exit fullscreen mode

A basic graph can be created like this:

from langgraph.graph import StateGraph, START, END

builder = StateGraph(AgentState)

builder.add_node("normalize_request", normalize_request)
builder.add_node("search_properties", search_properties)
builder.add_node("rank_properties", rank_properties)

builder.add_edge(START, "normalize_request")
builder.add_edge("normalize_request", "search_properties")
builder.add_edge("search_properties", "rank_properties")
builder.add_edge("rank_properties", END)

graph = builder.compile()
Enter fullscreen mode Exit fullscreen mode

The important architectural difference is that the workflow is now represented explicitly as a graph.

Each node has a defined responsibility.

6. Move n8n Tools Into Python Tools

So far, we have identified the nodes' functionalities, and they are mapped to LangGraph nodes. Now, we move the tools.

An n8n HTTP Request node might currently call a property API.

So, instead of letting the agent directly deal with raw HTTP logic, wrap the operation as a tool.

For example:

from langchain_core.tools import tool

@tool
def search_properties(
    location: str,
    max_budget: int,
    bedrooms: int
):
    """
    Search available properties.
    """

    # Call property API or database
    results = property_service.search(
        location=location,
        max_budget=max_budget,
        bedrooms=bedrooms
    )

    return results
Enter fullscreen mode Exit fullscreen mode

The tool should have:

  • Clear inputs
  • Clear outputs
  • Validation
  • Error handling
  • A single responsibility

The important distinction is:

The tool operates. The agent decides when to use it.

This keeps the agent's reasoning separate from infrastructure code.

7. Replace n8n IF Nodes With Explicit Graph Routing

This is another essential migration step.

Suppose the n8n workflow contains:

IF match_score > 0.8
       ↓
Strong Match
Enter fullscreen mode Exit fullscreen mode

In LangGraph, make that routing explicit.

def route_match(state: AgentState):

    if not state["matches"]:
        return "no_match"

    if state["matches"][0]["score"] >= 0.8:
        return "strong_match"

    return "weak_match"
Enter fullscreen mode Exit fullscreen mode

Then, connect the routes with:

builderadd_conditional_edges(
    "rank_properties",
    route_match,
    {
        "strong_match": "send_recommendation",
        "weak_match": "request_more_preferences",
        "no_match": "fallback_search"
    }
)
Enter fullscreen mode Exit fullscreen mode

The resulting graph then becomes:

                    rank_properties
                           ↓
                       route_match
                      /     |      \
                     /      |       \
            strong_match weak_match no_match
                  ↓          ↓          ↓
           recommendation  ask user  fallback search
Enter fullscreen mode Exit fullscreen mode

This is much easier to reason when the number of branches increases.

8. Add Persistence Instead of Relying on Execution History

A prototype often relies on execution history.

A production agent cannot assume that the entire execution will always remain active.

So, consider:

Agent starts
   ↓
Search properties
   ↓
Human approval required
   ↓
Wait 6 hours
   ↓
Continue
Enter fullscreen mode Exit fullscreen mode

The agent needs to remember where it was and what state it had and here is where LangGraph persistence becomes important.

Conceptually:

Agent State
    ↓
Checkpoint
    ↓
Thread
    ↓
Resume Execution
Enter fullscreen mode Exit fullscreen mode

Compile the graph with a checkpointer:

graph = builder.compile(
    checkpointer=checkpointer
)
Enter fullscreen mode Exit fullscreen mode

Then invoke it with a stable thread ID:

config = {
    "configurable": {
        "thread_id": "lead-123"
    }
}

result = graph.invoke(
    initial_state,
    config=config
)
Enter fullscreen mode Exit fullscreen mode

The thread_id gives the execution a durable identity.

This becomes particularly important for:

  • Long-running agents
  • Human approval
  • Multi-step conversations
  • Recovery
  • Stateful workflows
  • Resuming interrupted execution

9. Convert n8n Wait/Human Approval Into interrupt()

Consider an n8n workflow:

AI recommends property
        ↓
Wait
        ↓
Agent approval
        ↓
Continue
Enter fullscreen mode Exit fullscreen mode

A LangGraph implementation can model the same process using an interrupt:

AI Recommendation
        ↓
interrupt()
        ↓
Human Decision
        ↓
Resume Graph
Enter fullscreen mode Exit fullscreen mode

For example:

from langgraph.types import interrupt

def approval_node(state):

    decision = interrupt({
        "message": "Approve this property recommendation?",
        "property": state["selected_property"]
    })

    return {
        "approval": decision
    }
Enter fullscreen mode Exit fullscreen mode

The important difference is that the agent doesn't need to remain continuously active while waiting.

The state can be persisted and execution can resume when the human decision arrives.

This is especially useful for workflows involving:

  • Financial approvals
  • Sensitive customer actions
  • Contract review
  • High-value sales
  • External side effects

10. Make External API Calls Idempotent

This is one of the most significant production changes.

Imagine the agent executes:

send_whatsapp_message()
Enter fullscreen mode Exit fullscreen mode

Then, the process crashes immediately afterward.

Next, when the graph resumes, the operation might run again.

You could end up with:

Message sent
    ↓
Process crashes
    ↓
Graph resumes
    ↓
Message sent again
Enter fullscreen mode Exit fullscreen mode

The result is a duplicate customer message.

Instead, design external side effects to be idempotent.

For example:

Agent Decision
      ↓
Generate operation_id
      ↓
Check idempotency store
      ↓
Execute side effect
      ↓
Persist result
Enter fullscreen mode Exit fullscreen mode

A simple implementation might use:

def send_message_once(operation_id, message):

    if already_processed(operation_id):
        return get_previous_result(operation_id)

    result = send_message(message)

    save_result(
        operation_id=operation_id,
        result=result
    )

    return result
Enter fullscreen mode Exit fullscreen mode

This pattern is especially important for:

  • Payments
  • Emails
  • WhatsApp messages
  • CRM updates
  • Booking APIs
  • Ticket creation
  • Database writes

A production agent should always assume that execution may be retried or resumed.

11. Move Retry Logic Out of the Prompt

Don't rely on the LLM to decide:

"If the API fails, try again."

Retry behavior belongs in the application layer.

For example:

from tenacity import retry
from tenacity import stop_after_attempt
from tenacity import wait_exponential

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential()
)
def call_property_api():

    return property_api.search()
Enter fullscreen mode Exit fullscreen mode

But not every error should be retried.

Retryable Errors

Usually:

  • Timeout
  • HTTP 429
  • Temporary 5xx errors
  • Temporary network failure

Non-Retryable Errors

Usually:

  • Invalid request
  • Authentication failure
  • Missing required parameter
  • Permission denied
  • Invalid property ID

The workflow should distinguish between these distinctive cases.

For example:

API Error
   ↓
Retryable?
  /      \
Yes      No
 ↓        ↓
Retry   Recovery
Enter fullscreen mode Exit fullscreen mode

This keeps reliability logic deterministic.

12. Add Structured Tool Errors

Avoid returning vague errors like:

raise Exception("API failed")
Enter fullscreen mode Exit fullscreen mode

Instead, return structured information.

For example:

{
    "success": False,
    "error_type": "rate_limit",
    "retryable": True,
    "message": "Property API rate limit exceeded"
}
Enter fullscreen mode Exit fullscreen mode

Now the application can make a deterministic decision.

For example:

Tool Result
    ↓
success?
 /       \
Yes       No
 ↓         ↓
Continue  retryable?
          /       \
        Yes        No
         ↓          ↓
       Retry     Recovery
Enter fullscreen mode Exit fullscreen mode

This is much safer than expecting an LLM to interpret every infrastructure failure that may be possible.

13. Keep Deterministic Logic Outside the Agent

Another easy mistake during migration is putting everything inside the LLM.

Don't do that.

For example:

Deterministic

  • Budget filtering
  • Property availability
  • Authentication
  • Duplicate detection
  • Schema validation
  • Permission checks
  • Retry policy

Agentic

  • Interpret natural-language preferences
  • Compare qualitative requirements
  • Select relevant tools
  • Explain recommendations
  • Generate personalized responses

The architecture should, therefore, look like:

                    Agent
                      ↓
             ┌────────┴─────────┐
             ↓                   ↓
     Deterministic Logic    AI Reasoning
             ↓                   ↓
       Database / APIs       LLM + Tools
Enter fullscreen mode Exit fullscreen mode

The LLM should not be responsible for decisions that can be reliably enforced in code.

14. Test the Graph Before Calling It in Production

A successful demo is not the same thing as a production-ready agent.

Make a rule to test individual nodes first.

For example:

def test_route_strong_match():

    state = {
        "matches": [
            {"score": 0.91}
        ]
    }

    assert route_match(state) == "strong_match"
Enter fullscreen mode Exit fullscreen mode

Test tools separately:

def test_property_search():

    result = search_properties.invoke({
        "location": "Dubai Marina",
        "max_budget": 2000000,
        "bedrooms": 3
    })

    ascertain that the result is not None
Enter fullscreen mode Exit fullscreen mode

Then test the failure scenarios.

Test at least the following scenarios:

  • API timeout
  • API 500
  • API 429
  • Invalid input
  • Empty search result
  • LLM timeout
  • Tool failure
  • Duplicate request
  • Human rejection
  • Human approval
  • Graph resume
  • Database failure

Also, remember to test whether the same execution can safely resume.

15. Test the Agent's State Transitions

Don't only test the final response but also test what happens between nodes.

For example:

Input
 ↓
Normalization
 ↓
Search
 ↓
Ranking
 ↓
Routing
 ↓
Recommendation
Enter fullscreen mode Exit fullscreen mode

For each transition, verify:

  • Required state exists
  • Data has the expected schema
  • Tool results are valid
  • Errors are handled
  • Routing decisions are correct

This makes debugging much easier than testing the agent only from the outside.

Also Explore our n8n Workflow Automation service → https://ciphernutz.com/service/n8n-workflow-automation

16. Don't Migrate Everything Out of n8n

A common mistake is treating the migration as simple as:

n8n → LangGraph
Enter fullscreen mode Exit fullscreen mode

and having nothing left in n8n.

That isn't always necessary.

A better architecture can be:

                    n8n
                     │
          ┌──────────┼──────────┐
          ↓          ↓          ↓
      Webhooks    CRM Events  Scheduled Jobs
          │
          ↓
                 LangGraph
                     │
          ┌──────────┼──────────┐
          ↓          ↓          ↓
        State      Tools     Reasoning
          │          │          │
          └──────────┼──────────┘
                     ↓
                Agent Result
                     ↓
                    n8n
                     ↓
              Notifications / CRM
Enter fullscreen mode Exit fullscreen mode

n8n can continue handling:

  • SaaS integrations
  • Webhooks
  • Scheduled jobs
  • Notifications
  • Simple automations
  • CRM triggers

Similarly, LangGraph can handle:

  • Agent state
  • Reasoning
  • Tool selection
  • Conditional execution
  • Long-running execution
  • Human approval
  • Agent recovery

This creates a hybrid architecture rather than forcing everything into one platform.

17. Production Architecture

A production architecture could look like this:

                 ┌───────────────┐
                 │ API / Webhook │
                 └───────┬───────┘
                         ↓
                 ┌─────────────────┐
                 │ LangGraph Agent │
                 └────────┬────────┘
                          ↓
                  ┌────────────────┐
                  │   Agent State  │
                  └────────┬───────┘
                           ↓
          ┌────────────────────────────────┐
          │                                │
     Deterministic                     Agentic
        Nodes                            Nodes
          │                                │
          ↓                                ↓
    Database / APIs                  LLM + Tools
          │                                │
          └──────────────┬─────────────────┘
                         ↓
                    Checkpointer
                         ↓
                     Pos
Enter fullscreen mode Exit fullscreen mode

Top comments (0)