Build a Custom SQL Agent

Status: ACTIVE (pulled from docs.langchain.com) Source: https://docs.langchain.com/oss/python/langgraph/sql-agent Timestamp: 2026-05-11

Build an agent that answers questions about a SQL database using LangGraph.

Setup

from langchain.chat_models import init_chat_model
from langchain_community.utilities import SQLDatabase
from langchain_community.agent_toolkits import SQLDatabaseToolkit

model = init_chat_model("gpt-5.4")
db = SQLDatabase.from_uri("sqlite:///Chinook.db")
toolkit = SQLDatabaseToolkit(db=db, llm=model)
tools = toolkit.get_tools()

Define Steps

List Tables

def list_tables(state: MessagesState):
    list_tables_tool = next(t for t in tools if t.name == "sql_db_list_tables")
    tool_call = AIMessage(content="", tool_calls=[{
        "name": "sql_db_list_tables", "args": {}, "id": "abc123", "type": "tool_call"
    }])
    tool_message = list_tables_tool.invoke(tool_call.tool_calls[0])
    response = AIMessage(f"Available tables: {tool_message.content}")
    return {"messages": [tool_call, tool_message, response]}

Get Schema

get_schema_tool = next(t for t in tools if t.name == "sql_db_schema")
get_schema_node = ToolNode([get_schema_tool], name="get_schema")

def call_get_schema(state: MessagesState):
    llm_with_tools = model.bind_tools([get_schema_tool], tool_choice="any")
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

Generate and Check Query

run_query_tool = next(t for t in tools if t.name == "sql_db_query")

def generate_query(state: MessagesState):
    system_msg = {"role": "system", "content": "Create a syntactically correct SQL query..."}
    llm_with_tools = model.bind_tools([run_query_tool])
    response = llm_with_tools.invoke([system_msg] + state["messages"])
    return {"messages": [response]}

def should_continue(state: MessagesState) -> Literal[END, "check_query"]:
    if not state["messages"][-1].tool_calls:
        return END
    return "check_query"

Assemble Graph

builder = StateGraph(MessagesState)
builder.add_node(list_tables)
builder.add_node(call_get_schema)
builder.add_node("get_schema", get_schema_node)
builder.add_node(generate_query)
builder.add_node(check_query)
builder.add_node("run_query", run_query_node)

builder.add_edge(START, "list_tables")
builder.add_edge("list_tables", "call_get_schema")
builder.add_edge("call_get_schema", "get_schema")
builder.add_edge("get_schema", "generate_query")
builder.add_conditional_edges("generate_query", should_continue)
builder.add_edge("check_query", "run_query")
builder.add_edge("run_query", "generate_query")

agent = builder.compile()

Human-in-the-Loop for SQL Queries

Add review before executing SQL:

from langgraph.types import interrupt, Command

def run_query_tool_with_interrupt(config: RunnableConfig, **tool_input):
    response = interrupt([{
        "action": run_query_tool.name,
        "args": tool_input,
        "description": "Please review the SQL query"
    }])
    if response["type"] == "accept":
        return run_query_tool.invoke(tool_input, config)
    elif response["type"] == "edit":
        return run_query_tool.invoke(response["args"]["args"], config)
    elif response["type"] == "response":
        return response["args"]

# Compile with checkpointer
from langgraph.checkpoint.memory import InMemorySaver
agent = builder.compile(checkpointer=InMemorySaver())

# Resume after review
agent.stream(Command(resume={"type": "accept"}), config)