Migration Guard Custom
What it does
A reviewer reads the migration and states what it intends to do. An analyst runs it through the database's explain and lock-inspection tools to see how it behaves under load without applying it. The flow then branches on the analyst's verdict: a safe migration gets a brief approval note, and a risky one gets a report naming the exact hazard and the online-safe rewrite. Nothing is applied to the database.
The cast · 4 agents
Run the migration through explain and inspect the locks it would take, without applying it. Decide whether it is risky: a full table rewrite, a blocking lock on a large or hot table, or an unindexed backfill all count. Return an object with a 'risky' boolean and the reasons.
Write a report that names the hazard, explains what would happen under load, and gives the online-safe rewrite (for example: add the column nullable, backfill in batches, then set not-null). Save it to a file.
Write a short note confirming the migration is safe to run and why. Save it to a file.
Read the migration SQL and state plainly what it does: which tables and columns change, and whether it adds, alters, or backfills. Do not judge safety yet.
Flow
Interface
Depends on · 2 MCP servers
io.github.supabase/postgres-mcpnot in the registry yetdb
io.github.modelcontextprotocol/filesystemnot in the registry yetfs# Generated by @socketcat/compiler for target: langgraph
# blueprint: com.socketcat/migration-guard v1.0.0 schema: socketcat.dev/blueprint/v0
# This code is yours. Edit it freely. The socketcat_runtime helper is optional and can be vendored.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
import socketcat_runtime as _rt
class State(TypedDict, total=False):
migrationSql: object
verdict: object
intent: object
analysis: object
def node_AgentInvoke_0(state):
return _rt.run_agent("reviewer", state, {"migrationSql":"migrationSql"}, "intent")
def node_AgentInvoke_1(state):
return _rt.run_agent("analyst", state, {"intent":"intent","migrationSql":"migrationSql"}, "analysis")
def node_Branch_2(state):
return {}
def node_Join_3(state):
return {}
def node_AgentInvoke_4(state):
return _rt.run_agent("blocker", state, {"analysis":"analysis"}, "verdict")
def node_AgentInvoke_5(state):
return _rt.run_agent("approver", state, {"analysis":"analysis"}, "verdict")
def route_Branch_2(state):
if _rt.cond("analysis.risky == true", state): return "AgentInvoke_4"
return "AgentInvoke_5"
def build():
b = StateGraph(State)
b.add_node("AgentInvoke_0", node_AgentInvoke_0)
b.add_node("AgentInvoke_1", node_AgentInvoke_1)
b.add_node("Branch_2", node_Branch_2)
b.add_node("Join_3", node_Join_3)
b.add_node("AgentInvoke_4", node_AgentInvoke_4)
b.add_node("AgentInvoke_5", node_AgentInvoke_5)
b.add_edge(START, "AgentInvoke_0")
b.add_edge("AgentInvoke_0", "AgentInvoke_1")
b.add_edge("AgentInvoke_1", "Branch_2")
b.add_conditional_edges("Branch_2", route_Branch_2, {"AgentInvoke_4": "AgentInvoke_4", "AgentInvoke_5": "AgentInvoke_5"})
b.add_edge("Join_3", END)
b.add_edge("AgentInvoke_4", "Join_3")
b.add_edge("AgentInvoke_5", "Join_3")
return b.compile()
INPUTS = ["migrationSql"]
OUTPUTS = ["verdict"]
if __name__ == "__main__":
_rt.main(build, INPUTS, OUTPUTS)
▸blueprint.json (the portable format)
{
"id": "com.socketcat/migration-guard",
"flow": {
"type": "sequence",
"blocks": [
{
"in": {
"migrationSql": "migrationSql"
},
"out": "intent",
"use": "reviewer",
"type": "agent"
},
{
"in": {
"intent": "intent",
"migrationSql": "migrationSql"
},
"out": "analysis",
"use": "analyst",
"type": "agent"
},
{
"else": [
{
"in": {
"analysis": "analysis"
},
"out": "verdict",
"use": "approver",
"type": "agent"
}
],
"type": "branch",
"cases": [
{
"when": "analysis.risky == true",
"blocks": [
{
"in": {
"analysis": "analysis"
},
"out": "verdict",
"use": "blocker",
"type": "agent"
}
]
}
]
}
]
},
"tags": [
"databases",
"migrations",
"safety",
"branch"
],
"title": "Migration Guard",
"agents": {
"analyst": {
"model": {
"hint": "reasoning"
},
"title": "Analyst",
"tools": [
"db.explain",
"db.list_locks"
],
"output": {
"type": "object",
"required": [
"risky"
],
"properties": {
"risky": {
"type": "boolean"
},
"reasons": {
"type": "string"
}
}
},
"instructions": "Run the migration through explain and inspect the locks it would take, without applying it. Decide whether it is risky: a full table rewrite, a blocking lock on a large or hot table, or an unindexed backfill all count. Return an object with a 'risky' boolean and the reasons."
},
"blocker": {
"model": {
"hint": "reasoning"
},
"title": "Blocker",
"tools": [
"fs.write_file"
],
"instructions": "Write a report that names the hazard, explains what would happen under load, and gives the online-safe rewrite (for example: add the column nullable, backfill in batches, then set not-null). Save it to a file."
},
"approver": {
"model": {
"hint": "fast"
},
"title": "Approver",
"tools": [
"fs.write_file"
],
"instructions": "Write a short note confirming the migration is safe to run and why. Save it to a file."
},
"reviewer": {
"model": {
"hint": "reasoning"
},
"title": "Reviewer",
"instructions": "Read the migration SQL and state plainly what it does: which tables and columns change, and whether it adds, alters, or backfills. Do not judge safety yet."
}
},
"estate": {
"intent": {
"type": "string",
"description": "The reviewer's plain reading of what the migration changes."
},
"analysis": {
"type": "object",
"description": "The analyst's explain output and lock findings, with a 'risky' flag."
}
},
"$schema": "socketcat.dev/blueprint/v0",
"authors": [
{
"url": "https://socketcat.com",
"name": "SocketCat"
}
],
"license": "MIT",
"servers": [
{
"ref": "io.github.supabase/postgres-mcp",
"alias": "db"
},
{
"ref": "io.github.modelcontextprotocol/filesystem",
"alias": "fs"
}
],
"summary": "Reviews a proposed SQL migration for locks and cost, then branches to an approval note or a blocking report.",
"targets": [
"langgraph",
"*"
],
"version": "1.0.0",
"interface": {
"inputs": {
"migrationSql": {
"type": "string",
"description": "The migration SQL to review."
}
},
"outputs": {
"verdict": {
"type": "string",
"description": "The approval note or the blocking report, whichever the review produced."
}
}
},
"extensions": {
"dev.langgraph": {
"checkpointer": "memory"
}
},
"description": "A reviewer reads the migration and states what it intends to do. An analyst runs it through the database's explain and lock-inspection tools to see how it behaves under load without applying it. The flow then branches on the analyst's verdict: a safe migration gets a brief approval note, and a risky one gets a report naming the exact hazard and the online-safe rewrite. Nothing is applied to the database."
}