Why Not Just Call an LLM and Execute the Output?
The simplest possible AI operations tool is three lines of pseudocode:
user_input = "scale payment-service to 5 replicas"
command = llm.generate(f"Generate a kubectl command for: {user_input}")
os.system(command)This works in a demo. It also works in a postmortem, as the root cause.
The problem with monolithic LLM calls for infrastructure operations is threefold:
- No separation of concerns. The same model that interprets intent also generates the execution command and decides whether the outcome was successful. There's no checkpoint, no review, no structural opportunity to catch errors.
- No tool boundaries. The LLM generates arbitrary shell commands. If it hallucinates a flag, misidentifies a namespace, or interpolates a variable incorrectly, the command executes verbatim against your cluster.
- No verification loop. The model reports "done" based on the command's exit code. But
kubectl applyreturning 0 doesn't mean your pods are healthy.helm upgradesucceeding doesn't mean your service is reachable.
Skyflo's architecture exists because each of these failure modes requires a structural solution, not a better prompt.
The Unified Agent Workflow
Skyflo doesn't use multiple specialized agents. It runs a single unified agent powered by a LangGraph workflow with four distinct nodes:
| Node | Responsibility | What Happens Here |
|---|---|---|
| Entry | Receive and contextualize the user's request | Parse the incoming message, load conversation history, set up the agent state |
| Model | Reason, plan, and evaluate | The LLM interprets intent, discovers resources via read-only tool calls, proposes actions, and evaluates results after execution |
| Gate | Execute tools with safety enforcement | Read operations execute freely. Write operations pause for human approval before executing. All tool calls go through typed MCP interfaces. |
| Final | Complete the operation | Deliver the final response with evidence and results |
This is a loop, not a pipeline. After the gate executes a tool call, results flow back to the model node. The model evaluates the outcome, decides whether more action is needed, and either issues another tool call (looping back through the gate) or concludes the operation (routing to final).
The critical insight: planning, execution, and verification are not separate agents. They are phases of the same agent's reasoning loop. The model plans when it reasons about what to do. The gate executes when the model issues tool calls. The model verifies when it evaluates tool results and decides whether the outcome matches the original intent.
The Model Phase: Planning and Reasoning
The model node is where the LLM does its work. When a request arrives, the model:
- Parses intent: Identifies the target workload, the concern, and the implied workflow. "Check why payment-service pods are restarting" becomes: target is
payment-service, concern is pod restarts, workflow is diagnostic. - Discovers resources: Issues read-only MCP tool calls to gather cluster state.
kubernetes.list_pods(namespace="default", label_selector="app=payment-service"),kubernetes.get_events(namespace="default"). - Correlates signals: Cross-references pod status, events, recent deployments, and resource metrics to build a situational model.
- Proposes actions: If a mutation is needed, the model produces a tool call with specific parameters. "Patch the deployment to increase memory limit to 512Mi."
- Evaluates outcomes: After tool execution, the model receives the results and assesses whether the operation achieved its goal. If not, it reasons about what to try next.
This is all one agent, one context window, one reasoning process. The model retains full context across the entire operation: what the user asked, what was discovered, what was proposed, what was executed, and what the result was.
The Gate Phase: Tool Execution with Safety
The gate node is the enforcement layer. Every tool call the model produces passes through the gate before execution.
The gate enforces a simple, unambiguous boundary:
- Read operations (get, list, describe, logs, top, events): Execute immediately. No approval needed. Diagnosis should never be gated.
- Write operations (apply, patch, delete, scale, rollback, restart): Pause execution and emit an approval request via SSE. The operation resumes only after explicit human approval.
Typed tool calls, not shell commands. The agent doesn't generate kubectl scale deployment payment-service --replicas=5. It produces a structured tool call: kubernetes.scale(resource="deployment", name="payment-service", namespace="default", replicas=5). The MCP server validates the schema, checks that the resource exists, and executes the operation in a bounded, auditable way.
- The tool call has a schema.
replicasmust be an integer.namespacemust be a string that matches an existing namespace. The MCP server validates these constraints before executing. - The tool call is auditable. The structured call is logged with all parameters, the approval decision, and the execution result.
- The tool call is bounded. The agent can't add arbitrary flags, pipe output to another command, or chain operations that weren't part of the plan.
The gate is in the engine, not the UI. Whether the request comes from the Command Center, a Slack integration, or an API call, the same gate applies. You can't bypass it by using a different client.
Step-by-step execution. The agent doesn't batch all mutations into one transaction. It issues tool calls one at a time, evaluates each result, and decides whether to continue. If step 3 of 5 fails, the agent reports the partial state rather than blindly continuing.
Verification Through the Loop
Verification is not a separate agent. It's what happens when tool results flow back to the model node.
After the gate executes a tool call, the model:
- Reads the result and evaluates whether it matches expectations. Did the scale command succeed? Are the new pods running?
- Issues follow-up checks if needed. The model may call read-only tools to verify the broader system state: are other services still healthy? Did the change trigger unexpected restarts? Are error rates stable?
- Decides to continue or stop. If the outcome is satisfactory, the model routes to the final node with a summary and evidence. If something went wrong, it reasons about remediation and issues new tool calls.
This loop (model, gate, model, gate, ... final) is how Skyflo handles operations that don't succeed on the first attempt. The model adapts based on actual results, not a predetermined script. Every operation, from a simple pod restart to a complex multi-step rollback, follows this same graph.
LangGraph: Why a Graph Runtime, Not a Chain
Early versions of Skyflo used a linear chain: parse, generate, execute, report. It worked for simple operations but collapsed for anything requiring conditional logic, retries, or multi-step workflows.
LangGraph provides the runtime for Skyflo's agent workflow. It's a directed graph framework built on LangChain that supports:
Conditional edges. The model node can route to the gate (when it produces tool calls) or directly to the final node (when no tools are needed). After the gate executes, results always route back to the model for evaluation. The model then decides: more tool calls, or done?
State management. Each node in the graph receives and updates a shared state object. The entry node initializes the state with the user's message. The model node writes its reasoning and tool call decisions. The gate node writes execution results. This shared state is what allows the agent to maintain full context across multiple loop iterations.
Checkpointing. LangGraph checkpoints the state at each node transition. If a tool call fails mid-operation, the system can resume from the last checkpoint rather than restarting the entire workflow. This is essential for long-running operations; you don't want to re-run a 10-step diagnostic because step 7 hit a transient API error.
Streaming. The agent's reasoning and tool calls are streamed in real time via SSE. The operator sees the model thinking, the gate executing tools, and the results flowing back, not a spinner followed by a wall of output.
The graph for a typical Skyflo operation:
User Request
│
▼
┌─────────┐
│ Entry │
└────┬────┘
│
▼
┌─────────┐ tool call results
│ Model │◄──────────────────┐
└────┬────┘ │
│ │
│ has tool calls? │
│ │
├── yes ──▶ ┌────────┐ │
│ │ Gate ├───┘
│ └────────┘
│ (read → execute freely)
│ (write → human approval → execute)
│
├── no (done)
│
▼
┌─────────┐
│ Final │
└─────────┘The key insight: this is a loop, not a pipeline. The model-to-gate-to-model cycle repeats as many times as needed. A simple diagnostic might loop once (list pods, read results, report). A complex incident might loop ten times (discover, check events, check logs, check metrics, propose fix, wait for approval, execute fix, verify pods, verify metrics, report). The same graph handles both.
MCP: The Model Context Protocol and Why It Matters
MCP (Model Context Protocol) is the tool execution layer that sits between the agent and your infrastructure. It's the reason Skyflo can execute operations safely at scale without devolving into prompt-injected shell commands.
The Problem MCP Solves
Without MCP, an AI agent executes infrastructure operations through one of two approaches:
- Generate shell commands. The LLM outputs
kubectl apply -f deployment.yamland a subprocess runs it. This is the "weekend project" approach. It's fast to build and impossible to secure. The LLM can generate any command, including destructive ones, and there's no structural way to validate intent before execution. - Hardcoded function calls. The developer wraps common operations in Python functions (
scale_deployment(),get_pods(),delete_service()) and maps LLM output to function calls. This is better but doesn't scale. Every new tool requires new code. There's no standard interface, no discovery mechanism, and no consistent permission model.
MCP provides a standardized protocol for tool definition, discovery, and execution. Each MCP server exposes a set of typed tools with:
- Input schemas: JSON Schema definitions for every parameter. The LLM knows exactly what parameters a tool accepts, their types, constraints, and descriptions.
- Permission model: Each tool is tagged as read-only or mutating. The engine enforces the appropriate gate based on this tag.
- Output schemas: Structured return values that the agent can parse reliably, not raw terminal output that requires regex parsing.
Typed Tool Execution in Practice
Here's what a Kubernetes MCP tool definition looks like in Skyflo:
@mcp.tool()
async def scale_resource(
resource_type: str, # "deployment", "statefulset", "replicaset"
name: str, # Resource name
namespace: str, # Target namespace
replicas: int, # Desired replica count (must be >= 0)
) -> ScaleResult:
"""Scale a Kubernetes resource to a specified replica count.
This is a MUTATING operation — requires human approval.
"""
# Validate inputs against cluster state
resource = await k8s_client.get(resource_type, name, namespace)
if not resource:
raise ToolError(f"{resource_type}/{name} not found in {namespace}")
# Execute the scale operation
result = await k8s_client.scale(resource, replicas)
return ScaleResult(
resource=f"{resource_type}/{name}",
namespace=namespace,
previous_replicas=resource.spec.replicas,
desired_replicas=replicas,
status=result.status,
)Compare this to the shell command approach:
kubectl scale deployment payment-service --replicas=5 -n defaultThe typed version:
- Validates that the resource exists before attempting to scale
- Returns structured data (previous replicas, new replicas, status) instead of text
- Is tagged as mutating, which triggers the approval gate automatically
- Has a schema that prevents the LLM from passing invalid parameters (e.g., negative replicas, non-existent resource types)
- Is logged as a structured tool call, not a raw string
How MCP Prevents Prompt Injection
Prompt injection in the context of infrastructure operations means an attacker (or a hallucinating model) crafts input that causes the agent to execute unintended operations.
With shell command generation, prompt injection is trivial:
User: "List pods in production; also run rm -rf /"
LLM: "kubectl get pods -n production && rm -rf /"With MCP, this attack vector is structurally eliminated. The LLM doesn't generate shell commands; it selects from a fixed set of typed tools. There is no execute_arbitrary_command tool. The LLM can only call tools that exist in the MCP server's registry, with parameters that match the tool's schema.
Even if the LLM hallucinates a tool that doesn't exist (e.g., filesystem.delete_all()), the MCP server returns a "tool not found" error. The hallucination is caught at the protocol layer, before it reaches your infrastructure.
Streaming Architecture: SSE for Real-Time Operator Feedback
Operations are not batch jobs. When an SRE asks Skyflo to diagnose a production issue, they need to see what's happening (the agent's reasoning, the tools it's calling, the intermediate results) in real time. Not a spinner followed by a summary.
Skyflo uses Server-Sent Events (SSE) to stream every step of the agent's operation to the Command Center:
Agent thought streaming. As the LLM generates its reasoning, each token is streamed to the client. The operator can see the agent thinking through its approach before the plan is finalized.
Tool call streaming. When the agent invokes an MCP tool, the tool call event is streamed immediately, including the tool name, parameters, and the fact that it's executing. The operator doesn't wait for the tool to complete to know what's happening.
Tool result streaming. When the tool returns, its structured result is streamed. For long-running tools (e.g., watching a rollout), intermediate status updates are streamed as they arrive.
Workflow phase streaming. As the graph transitions between nodes (model to gate, gate back to model), transition events are streamed. The operator always knows which phase is active and why.
The SSE implementation uses Redis as a pub/sub backbone. The engine publishes events to a Redis channel keyed by conversation ID. The API server subscribes to this channel and relays events to connected clients. This decouples the engine's event production from client consumption and supports multiple clients observing the same operation, which is useful when a team is watching an incident response together.
Engine (LangGraph) → Redis Pub/Sub → FastAPI SSE Endpoint → Command Center (Next.js)Why SSE over WebSockets? SSE is unidirectional (server to client), which matches the streaming use case. The client sends requests via REST; there's no need for a persistent bidirectional channel. SSE also survives proxy restarts and reconnects automatically, which matters when you're streaming a 10-minute incident diagnosis through an Nginx reverse proxy.
Multi-LLM Support: Why the AI Layer Is Swappable
Skyflo uses LiteLLM as its LLM abstraction layer. The agent can use any supported model: OpenAI GPT-4o, Anthropic Claude, Google Gemini, Groq, or local models via Ollama.
This isn't just a nice-to-have. It's an architectural decision driven by three production realities:
- Model availability. If your OpenAI API key hits a rate limit during an incident at 3 AM, you need a fallback. Skyflo can route to a secondary model without changing workflows.
- Data sovereignty. Some organizations can't send cluster data to external APIs. Running a local model via Ollama keeps everything on-premises.
- Cost optimization. Not every operation needs GPT-4o. Simple read operations (list pods, get logs) work fine with smaller, faster, cheaper models.
The multi-LLM design also means Skyflo isn't locked into any single vendor's trajectory. When a new model launches with better code understanding or faster inference, swapping it in is a configuration change, not a rewrite.
Putting It All Together: A Request From Start to Finish
Let's trace a real request through the architecture:
User: "The payment-service in production is returning 503s. Can you check what's going on?"
- Entry node activates. The request arrives at the FastAPI API server. It's authenticated, associated with a conversation, and routed to the engine. The entry node initializes the agent state with the user's message and conversation history.
- Model node, first reasoning pass. The agent interprets the request and issues read-only MCP tool calls to gather evidence:
kubernetes.list_pods(namespace="production", label_selector="app=payment-service")finds 3 pods, 1 in CrashLoopBackOff.kubernetes.get_pod_logs(name="payment-service-7d4f8b9c6-x2k9m", namespace="production", tail_lines=50)shows OOMKilled.kubernetes.top_pods(namespace="production")shows memory usage at 98% of limit.
- Gate executes reads. These are all read-only tool calls, so the gate executes them immediately with no approval needed. Results flow back to the model.
- Model node, second reasoning pass. The agent evaluates the evidence and proposes a fix: "Pod is OOMKilled. Current memory limit is 256Mi, actual usage peaks at 280Mi. Recommend patching the deployment to increase memory limit to 512Mi." It issues a write tool call.
- Gate: approval required. The gate detects a mutating operation and emits an approval request via SSE. The operator sees the exact patch, the target resource, and the namespace. They approve.
kubernetes.patch_resource(resource_type="deployment", name="payment-service", namespace="production", patch={"spec": {"template": {"spec": {"containers": [{"name": "payment-service", "resources": {"limits": {"memory": "512Mi"}}}]}}}})
- Gate executes the write. The patch applies. The result flows back to the model: deployment patched, rollout triggered.
- Model node, verification pass. The agent evaluates the patch result and issues verification tool calls:
kubernetes.get_rollout_status(name="payment-service", namespace="production")confirms rollout complete, all pods ready.kubernetes.top_pods(namespace="production")shows memory usage now at 45% of new limit.- No new OOMKill events in the last 2 minutes.
- Gate executes verification reads. All read-only, no approval needed. Results flow back to the model.
- Model routes to final. The agent confirms the fix worked. The final node delivers the summary with evidence. The operation is complete.
The entire flow follows the same graph: entry, model, gate, model, gate, ... final. Whether it's a simple pod restart or a complex multi-step rollback, the architecture doesn't change. That's the point.
Why This Architecture Matters
Most AI operations tools are thin wrappers around a single LLM call. They work for demos. They fail in production because production is where context is messy, operations are multi-step, and the cost of mistakes is measured in downtime, not embarrassment.
Skyflo's unified agent architecture exists because:
- A single context window means the agent retains full awareness across the entire operation. What was discovered during investigation informs the fix. What happened during execution informs verification. There's no context lost in handoffs between separate agents.
- Typed tool execution means the agent's capability is bounded by the tool registry, not by the LLM's imagination. New tools are added to the MCP server, not hardcoded into prompts.
- Human-in-the-loop safety means the approval gate is architectural, not cosmetic. It can't be bypassed by a new client, a Slack integration, or a creative prompt.
- The graph-based loop means the agent adapts to the complexity of the operation. Simple tasks loop once. Complex incidents loop many times. The same architecture handles both without special cases.
If you want to understand how this compares to other approaches in the space, see our comparison page. For specific operational use cases, check use cases.
Try Skyflo
Skyflo is open-source and self-hosted. Your cluster data never leaves your infrastructure.
helm repo add skyflo https://charts.skyflo.ai
helm install skyflo skyflo/skyflo