← Back to DevBytes

Building a CI/CD Automation Agent with LangGraph: Complete Guide

Building a CI/CD Automation Agent with LangGraph: Complete Guide

Continuous Integration and Continuous Deployment (CI/CD) pipelines have become the backbone of modern software delivery. However, as systems grow more complex, managing these pipelines manually becomes error-prone and time-consuming. By combining the orchestration power of LangGraph with large language models, you can build an intelligent agent that automates pipeline decisions, diagnoses failures, and even proposes fixes. This guide walks you through the entire process, from concept to a working implementation.

What Is a CI/CD Automation Agent?

A CI/CD automation agent is an AI-driven system that observes the state of your delivery pipeline and takes actions on your behalf. Unlike traditional automation scripts that follow rigid rules, an agent powered by LangGraph can reason about context, branch logic dynamically, and interact with external tools like GitHub, Jenkins, or Kubernetes APIs. LangGraph provides a graph-based framework for defining nodes (functions or LLM calls) and edges (transitions between them), making it ideal for modeling the non-linear flow of CI/CD workflows.

At its core, the agent maintains a shared state object that travels through the graph. Each node inspects or mutates that state, and conditional edges decide where to go next. This mirrors real CI/CD behavior: a build might pass, fail, or require manual approval, and each outcome leads to a different downstream action.

Why It Matters

Prerequisites and Setup

Before building the agent, make sure you have Python 3.10 or later and the required packages installed. You will also need an OpenAI API key (or another LLM provider supported by LangChain).

pip install langgraph langchain-openai langchain-core httpx pydantic

Create a project directory and add a configuration file for secrets:

# .env
OPENAI_API_KEY=sk-your-key-here
GITHUB_TOKEN=ghp_your_token_here
WEBHOOK_SECRET=your-webhook-secret

Load these variables at the start of your application using a library like python-dotenv.

Defining the Agent State

The first step in any LangGraph project is defining the state schema. For a CI/CD agent, the state should capture everything a node might need: the repository name, the commit SHA, build logs, test results, and the current decision.

from typing import TypedDict, Literal, Optional
from pydantic import BaseModel

class BuildResult(BaseModel):
    status: Literal["success", "failure", "running"]
    logs: str
    failed_step: Optional[str] = None

class AgentState(TypedDict):
    repo: str
    commit_sha: str
    branch: str
    build: BuildResult
    test_results: Optional[dict]
    diagnosis: Optional[str]
    proposed_fix: Optional[str]
    decision: Literal["deploy", "rollback", "notify", "retry", "halt"]
    messages: list[str]

Using a TypedDict keeps the state lightweight while still giving you type hints. The messages list acts as an audit trail that every node can append to.

Building the Graph Nodes

Each node is a function that takes the current state and returns a partial state update. Let us start with the node that triggers a build.

import httpx
from langgraph.graph import StateGraph, END

def trigger_build(state: AgentState) -> dict:
    """Trigger a CI build via a webhook or API call."""
    response = httpx.post(
        "https://api.your-ci-provider.com/builds",
        json={
            "repo": state["repo"],
            "commit_sha": state["commit_sha"],
            "branch": state["branch"],
        },
        headers={"Authorization": f"Bearer {GITHUB_TOKEN}"},
        timeout=30,
    )
    build_id = response.json()["build_id"]
    return {
        "build": BuildResult(status="running", logs="", failed_step=None),
        "messages": [f"Build {build_id} triggered for {state['commit_sha']}"],
    }

Next, define a node that polls the build status and captures logs when it finishes:

import time

def poll_build(state: AgentState) -> dict:
    """Poll the build until it completes."""
    for _ in range(60):
        response = httpx.get(
            f"https://api.your-ci-provider.com/builds/{state['commit_sha']}",
            headers={"Authorization": f"Bearer {GITHUB_TOKEN}"},
            timeout=10,
        )
        data = response.json()
        if data["status"] in ("success", "failure"):
            return {
                "build": BuildResult(
                    status=data["status"],
                    logs=data.get("logs", ""),
                    failed_step=data.get("failed_step"),
                ),
                "messages": [f"Build finished with status: {data['status']}"],
            }
        time.sleep(10)
    return {
        "build": BuildResult(status="failure", logs="Timeout waiting for build", failed_step="polling"),
        "messages": ["Build polling timed out"],
    }

Adding LLM-Powered Diagnosis

This is where the agent becomes genuinely useful. When a build fails, we send the logs to an LLM that diagnoses the problem and proposes a fix. LangGraph integrates naturally with LangChain's chat models.

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o", temperature=0)

DIAGNOSIS_PROMPT = """You are a CI/CD expert. Analyze the following build failure and provide:
1. A concise diagnosis of the root cause.
2. A proposed fix or next action.

Repository: {repo}
Branch: {branch}
Failed step: {failed_step}

Build logs:
{logs}
"""

def diagnose_failure(state: AgentState) -> dict:
    build = state["build"]
    prompt = DIAGNOSIS_PROMPT.format(
        repo=state["repo"],
        branch=state["branch"],
        failed_step=build.failed_step or "unknown",
        logs=build.logs[-4000:],
    )
    response = llm.invoke(prompt)
    content = response.content
    return {
        "diagnosis": content,
        "messages": [f"Diagnosis generated: {content[:200]}..."],
    }

Notice that we truncate the logs to the last 4000 characters. Build logs can be enormous, and sending the full history wastes tokens and slows down the response.

Defining Conditional Edges

The power of LangGraph comes from conditional edges. After polling the build, the agent should branch based on the outcome: success leads to deployment, failure leads to diagnosis, and a timeout leads to notification.

def route_after_build(state: AgentState) -> str:
    status = state["build"].status
    if status == "success":
        return "deploy"
    elif status == "failure":
        return "diagnose"
    else:
        return "notify"

def deploy(state: AgentState) -> dict:
    httpx.post(
        "https://api.your-deploy-system.com/deploy",
        json={"repo": state["repo"], "commit_sha": state["commit_sha"]},
        headers={"Authorization": f"Bearer {GITHUB_TOKEN}"},
        timeout=30,
    )
    return {
        "decision": "deploy",
        "messages": [f"Deployment triggered for {state['commit_sha']}"],
    }

def notify_team(state: AgentState) -> dict:
    message = f"CI/CD Agent update for {state['repo']}:\n"
    message += "\n".join(state["messages"][-5:])
    if state.get("diagnosis"):
        message += f"\n\nDiagnosis: {state['diagnosis']}"
    httpx.post(
        "https://hooks.slack.com/services/your-webhook",
        json={"text": message},
        timeout=10,
    )
    return {"decision": "notify", "messages": ["Team notified"]}

Assembling the Graph

Now we wire everything together using StateGraph. We add nodes, set the entry point, and connect them with both fixed and conditional edges.

from langgraph.graph import StateGraph, END

graph = StateGraph(AgentState)

graph.add_node("trigger_build", trigger_build)
graph.add_node("poll_build", poll_build)
graph.add_node("diagnose_failure", diagnose_failure)
graph.add_node("deploy", deploy)
graph.add_node("notify_team", notify_team)

graph.set_entry_point("trigger_build")
graph.add_edge("trigger_build", "poll_build")
graph.add_conditional_edges(
    "poll_build",
    route_after_build,
    {
        "deploy": "deploy",
        "diagnose": "diagnose_failure",
        "notify": "notify_team",
    },
)
graph.add_edge("diagnose_failure", "notify_team")
graph.add_edge("deploy", END)
graph.add_edge("notify_team", END)

app = graph.compile()

The compiled app is a runnable object. You invoke it with an initial state and it executes the graph until it reaches END.

Running the Agent

To use the agent, construct an initial state and invoke the compiled graph:

initial_state: AgentState = {
    "repo": "my-org/my-service",
    "commit_sha": "a1b2c3d4e5f6",
    "branch": "main",
    "build": BuildResult(status="running", logs="", failed_step=None),
    "test_results": None,
    "diagnosis": None,
    "proposed_fix": None,
    "decision": "halt",
    "messages": [],
}

final_state = app.invoke(initial_state)
print("Final decision:", final_state["decision"])
print("Audit trail:")
for msg in final_state["messages"]:
    print(f"  - {msg}")

When the build succeeds, the agent deploys and exits. When it fails, the agent diagnoses the failure, notifies the team with the LLM's analysis, and exits. The audit trail gives you full visibility into what happened.

Adding Human-in-the-Loop Approval

For production deployments, you often want a human to approve before the agent proceeds. LangGraph supports interrupts, which pause execution and wait for external input.

from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
app_with_approval = graph.compile(
    checkpointer=checkpointer,
    interrupt_before=["deploy"],
)

config = {"configurable": {"thread_id": "deployment-123"}}
state = app_with_approval.invoke(initial_state, config=config)

# Execution pauses before deploy. A human reviews the state:
print("Paused for approval. Current state:", state)

# To resume:
state = app_with_approval.invoke(None, config=config)

This pattern lets you combine autonomous diagnosis with human oversight, which is critical for high-stakes environments.

Best Practices

Extending the Agent

Once you have the basic graph working, you can extend it with additional nodes. For example, you might add a node that runs security scans after a successful build, or a node that automatically creates a pull request with the proposed fix. You could also integrate with observability tools like Datadog or Prometheus to feed metrics into the agent's decision-making.

Another valuable extension is adding a retry node. If the diagnosis suggests the failure was transient (for example, a flaky test or a network timeout), the agent can re-trigger the build up to a configurable limit before escalating to the team.

def should_retry(state: AgentState) -> str:
    diagnosis = state.get("diagnosis", "").lower()
    transient_keywords = ["timeout", "flaky", "transient", "network"]
    if any(keyword in diagnosis for keyword in transient_keywords):
        return "retry"
    return "notify"

# Add to graph:
graph.add_node("retry_build", trigger_build)
graph.add_conditional_edges(
    "diagnose_failure",
    should_retry,
    {"retry": "retry_build", "notify": "notify_team"},
)
graph.add_edge("retry_build", "poll_build")

Conclusion

Building a CI/CD automation agent with LangGraph gives you a structured, observable, and intelligent way to manage software delivery. By modeling your pipeline as a graph of nodes and conditional edges, you gain the flexibility to handle complex scenarios that rigid scripts cannot. The integration of LLM-powered diagnosis turns failures from silent bottlenecks into actionable insights, while human-in-the-loop checkpoints keep you in control of high-stakes decisions. Start with the minimal graph described here, then iteratively add nodes for security scanning, automatic rollback, and fix proposals as your confidence grows. With careful attention to state design, idempotency, and secret management, this agent can become a reliable member of your DevOps team.

— Ad —

Google AdSense will appear here after approval

← Back to all articles