Skip to main content
Glama
AutomateLab-tech

automatelab-n8n-mcp

n8n_lint_workflow

Lint an n8n workflow JSON to catch missing credentials, deprecated node types, broken connections, and other common issues.

Instructions

Lint an n8n workflow JSON. Returns concrete errors and warnings: missing credentials, deprecated node types (Function -> Code, spreadsheetFile -> convertToFile/extractFromFile), broken connections, missing or non-numeric typeVersion, duplicate node names or IDs, AI Agent missing ai_languageModel sub-node, Webhook missing webhookId, IF node still on v1 condition schema.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
workflowYesn8n workflow as either a parsed object or a JSON string.

Implementation Reference

  • The main lintWorkflow function that executes the tool logic. It parses the input workflow, validates nodes (missing names/IDs, duplicates, deprecated types, missing typeVersion, credentials, AI agent connections, webhook IDs, IF v1 schema), checks connections for broken references, and returns formatted issues.
    export async function lintWorkflow(rawArgs: unknown) {
    	const args = inputZod.parse(rawArgs);
    	const workflow =
    		typeof args.workflow === "string"
    			? safeParse(args.workflow)
    			: args.workflow;
    	const issues: Issue[] = [];
    
    	if (!workflow || typeof workflow !== "object" || Array.isArray(workflow)) {
    		issues.push({
    			severity: "error",
    			message: "Workflow is not a JSON object.",
    		});
    		return formatResult(issues);
    	}
    
    	const wf = workflow as Record<string, unknown>;
    	const nodes = wf.nodes;
    	if (!Array.isArray(nodes)) {
    		issues.push({
    			severity: "error",
    			message: "Workflow has no `nodes` array.",
    		});
    		return formatResult(issues);
    	}
    
    	const connections =
    		wf.connections && typeof wf.connections === "object"
    			? (wf.connections as Record<string, unknown>)
    			: {};
    
    	const nodeNames = new Set<string>();
    	const seenIds = new Set<string>();
    	const incomingByType: Record<string, Map<string, number>> = {};
    
    	for (const [src, conf] of Object.entries(connections)) {
    		if (!conf || typeof conf !== "object") continue;
    		for (const [connType, branches] of Object.entries(
    			conf as Record<string, unknown>,
    		)) {
    			if (!Array.isArray(branches)) continue;
    			for (const branch of branches) {
    				if (!Array.isArray(branch)) continue;
    				for (const c of branch) {
    					if (!c || typeof c !== "object") continue;
    					const target = (c as Record<string, unknown>).node;
    					if (typeof target !== "string") continue;
    					const map = (incomingByType[connType] ??= new Map());
    					map.set(target, (map.get(target) ?? 0) + 1);
    				}
    			}
    		}
    		// keep src referenced
    		void src;
    	}
    
    	for (const raw of nodes) {
    		if (!raw || typeof raw !== "object") {
    			issues.push({ severity: "error", message: "Node is not an object." });
    			continue;
    		}
    		const n = raw as Record<string, unknown>;
    		const nodeName = typeof n.name === "string" ? n.name : undefined;
    
    		if (!nodeName) {
    			issues.push({ severity: "error", message: "Node missing string `name`." });
    		} else {
    			if (nodeNames.has(nodeName)) {
    				issues.push({
    					severity: "error",
    					node: nodeName,
    					message: "Duplicate node name.",
    				});
    			}
    			nodeNames.add(nodeName);
    		}
    
    		if (typeof n.id !== "string") {
    			issues.push({
    				severity: "error",
    				node: nodeName,
    				message: "Node missing string `id`.",
    			});
    		} else {
    			if (seenIds.has(n.id)) {
    				issues.push({
    					severity: "error",
    					node: nodeName,
    					message: `Duplicate node id ${n.id}.`,
    				});
    			}
    			seenIds.add(n.id);
    		}
    
    		const nodeType = typeof n.type === "string" ? n.type : undefined;
    		if (!nodeType) {
    			issues.push({
    				severity: "error",
    				node: nodeName,
    				message: "Node missing string `type`.",
    			});
    		} else if (DEPRECATED_NODE_TYPES[nodeType]) {
    			issues.push({
    				severity: "warning",
    				node: nodeName,
    				message: `Node type "${nodeType}" is deprecated. Use "${DEPRECATED_NODE_TYPES[nodeType]}".`,
    			});
    		}
    
    		if (n.typeVersion === undefined || n.typeVersion === null) {
    			issues.push({
    				severity: "error",
    				node: nodeName,
    				message: "Missing `typeVersion`.",
    			});
    		} else if (typeof n.typeVersion !== "number") {
    			issues.push({
    				severity: "error",
    				node: nodeName,
    				message: "`typeVersion` must be a number.",
    			});
    		}
    
    		if (
    			!Array.isArray(n.position) ||
    			n.position.length !== 2 ||
    			n.position.some((v) => typeof v !== "number")
    		) {
    			issues.push({
    				severity: "warning",
    				node: nodeName,
    				message: "`position` should be a [x, y] array of numbers.",
    			});
    		}
    
    		if (nodeType && CREDENTIAL_REQUIRED_TYPES.has(nodeType) && !n.credentials) {
    			issues.push({
    				severity: "warning",
    				node: nodeName,
    				message: `Node type "${nodeType}" usually needs a credential. None set.`,
    			});
    		}
    
    		if (nodeType && AI_AGENT_TYPES.has(nodeType) && nodeName) {
    			const lm = incomingByType["ai_languageModel"]?.get(nodeName) ?? 0;
    			if (lm === 0) {
    				issues.push({
    					severity: "error",
    					node: nodeName,
    					message:
    						"AI Agent has no `ai_languageModel` sub-node connected. Attach a chat model (e.g. lmChatOpenAi).",
    				});
    			}
    		}
    
    		if (nodeType && WEBHOOK_TYPES.has(nodeType) && !n.webhookId) {
    			issues.push({
    				severity: "warning",
    				node: nodeName,
    				message:
    					"Webhook node has no `webhookId`. n8n auto-generates one on import, so the production URL will change. Set `webhookId` to keep a stable URL.",
    			});
    		}
    
    		if (nodeType && IF_NODE_TYPES.has(nodeType)) {
    			const params =
    				n.parameters && typeof n.parameters === "object"
    					? (n.parameters as Record<string, unknown>)
    					: {};
    			const conditions = params.conditions as Record<string, unknown> | undefined;
    			const looksLikeV1 =
    				conditions !== undefined &&
    				typeof conditions === "object" &&
    				!Array.isArray(conditions) &&
    				("boolean" in conditions ||
    					"string" in conditions ||
    					"number" in conditions ||
    					"dateTime" in conditions);
    			if (looksLikeV1) {
    				issues.push({
    					severity: "warning",
    					node: nodeName,
    					message:
    						"IF node uses v1 condition schema (`conditions.boolean[]` etc.). Bump `typeVersion` to 2+ and switch to the v2 condition shape (`conditions.options`, `conditions.conditions[]`, `conditions.combinator`).",
    				});
    			}
    		}
    	}
    
    	for (const [src, conf] of Object.entries(connections)) {
    		if (!nodeNames.has(src)) {
    			issues.push({
    				severity: "error",
    				message: `Connection from unknown node "${src}".`,
    			});
    			continue;
    		}
    		const main = (conf as { main?: unknown })?.main;
    		if (!Array.isArray(main)) continue;
    		for (const branch of main) {
    			if (!Array.isArray(branch)) continue;
    			for (const conn of branch) {
    				if (
    					!conn ||
    					typeof conn !== "object" ||
    					typeof (conn as Record<string, unknown>).node !== "string"
    				) {
    					issues.push({
    						severity: "error",
    						message: `Malformed connection from "${src}".`,
    					});
    					continue;
    				}
    				const target = (conn as { node: string }).node;
    				if (!nodeNames.has(target)) {
    					issues.push({
    						severity: "error",
    						message: `Connection from "${src}" points to missing node "${target}".`,
    					});
    				}
    			}
    		}
    	}
    
    	return formatResult(issues);
    }
  • Input schema for n8n_lint_workflow: accepts a 'workflow' property that can be either a parsed object or a JSON string.
    export const lintWorkflowInputSchema = {
    	type: "object",
    	properties: {
    		workflow: {
    			description:
    				"n8n workflow as either a parsed object or a JSON string.",
    			oneOf: [{ type: "object" }, { type: "string" }],
    		},
    	},
    	required: ["workflow"],
    } as const;
  • src/index.ts:52-57 (registration)
    Registration of the n8n_lint_workflow tool in the tool list, with description and inputSchema reference.
    {
    	name: "n8n_lint_workflow",
    	description:
    		"Lint an n8n workflow JSON. Returns concrete errors and warnings: missing credentials, deprecated node types (Function -> Code, spreadsheetFile -> convertToFile/extractFromFile), broken connections, missing or non-numeric typeVersion, duplicate node names or IDs, AI Agent missing ai_languageModel sub-node, Webhook missing webhookId, IF node still on v1 condition schema.",
    	inputSchema: lintWorkflowInputSchema,
    },
  • src/index.ts:115-116 (registration)
    Handler dispatch case for n8n_lint_workflow in the CallToolRequestSchema handler, calling lintWorkflow.
    case "n8n_lint_workflow":
    	return lintWorkflow(args ?? {});
  • Node catalog constants used by lintWorkflow: DEPRECATED_NODE_TYPES, AI_AGENT_TYPES, WEBHOOK_TYPES, IF_NODE_TYPES, CREDENTIAL_REQUIRED_TYPES.
    /**
     * Catalog of n8n node behaviors used by the lint and generate tools.
     *
     * The lists here are intentionally narrow: they cover the most common nodes
     * a user is likely to ship in a workflow. Unknown node types are treated as
     * valid by the linter (no false positives).
     */
    
    export const DEPRECATED_NODE_TYPES: Record<string, string> = {
    	"n8n-nodes-base.function": "n8n-nodes-base.code",
    	"n8n-nodes-base.functionItem": "n8n-nodes-base.code",
    	"n8n-nodes-base.start": "n8n-nodes-base.manualTrigger",
    	"n8n-nodes-base.spreadsheetFile":
    		"n8n-nodes-base.convertToFile or n8n-nodes-base.extractFromFile",
    };
    
    /**
     * AI agent root types. Both prefixes appear in the wild: the older
     * `n8n-nodes-langchain.*` and the canonical `@n8n/n8n-nodes-langchain.*`.
     */
    export const AI_AGENT_TYPES = new Set<string>([
    	"n8n-nodes-langchain.agent",
    	"@n8n/n8n-nodes-langchain.agent",
    ]);
    
    export const WEBHOOK_TYPES = new Set<string>([
    	"n8n-nodes-base.webhook",
    ]);
    
    export const IF_NODE_TYPES = new Set<string>([
    	"n8n-nodes-base.if",
    ]);
    
    export const CREDENTIAL_REQUIRED_TYPES = new Set<string>([
    	"n8n-nodes-base.airtable",
    	"n8n-nodes-base.discord",
    	"n8n-nodes-base.gmail",
    	"n8n-nodes-base.googleSheets",
    	"n8n-nodes-base.notion",
    	"n8n-nodes-base.openAi",
    	"n8n-nodes-base.postgres",
    	"n8n-nodes-base.slack",
    	"n8n-nodes-base.stripe",
    ]);
    
    export const KNOWN_TRIGGER_TYPES = new Set<string>([
    	"n8n-nodes-base.manualTrigger",
    	"n8n-nodes-base.webhook",
    	"n8n-nodes-base.scheduleTrigger",
    	"n8n-nodes-base.cron",
    	"n8n-nodes-base.rssFeedReadTrigger",
    	"n8n-nodes-base.emailReadImap",
    ]);
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations, the description fully bears the burden of transparency. It explicitly lists the categories of errors and warnings returned (missing credentials, deprecated nodes, etc.), giving agents a clear picture of what the tool does without side effects. This is highly transparent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single sentence with a colon and a list, making it reasonably concise. It front-loads the purpose and then enumerates specifics. Minor structural improvement could be separating the list, but it remains efficient.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (multiple checks) and the absence of an output schema, the description covers most expected information by listing all check categories. However, it does not describe the return format (e.g., structure of errors/warnings), leaving a minor gap in completeness.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% for the single parameter 'workflow', which already describes it as 'n8n workflow as either a parsed object or a JSON string.' The description adds no additional semantic meaning to the parameter beyond what the schema provides, meeting the baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The tool name and description clearly state 'Lint an n8n workflow JSON.' This is a specific verb (lint) and resource (n8n workflow). The description lists concrete checks, distinguishing it from sibling tools like n8n_create_workflow or n8n_list_workflows.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies the tool is used for validating n8n workflows but does not explicitly state when to use it versus alternatives, nor does it provide when-not-to-use guidance. Usage is inferred from the tool's name and purpose.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AutomateLab-tech/n8n-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server