n8n-io/n8n · error

invalid_graph

invalid_graph

Error message

invalid_graph

What it means

Returned as HTTP 400 {error:'invalid_graph', reason:...} when startExecution.start() throws GraphValidationError. validateExecutableGraph() throws this class for graphs that can NEVER run: zero trigger nodes, more than one trigger node, a slot index that is not a non-negative integer, or a slot index above MAX_SLOT_INDEX (100). Unlike UnimplementedError (501), these are hard structural defects, not missing features.

Solutions

  1. Ensure exactly one node has type 'trigger'.
  2. Ensure every edge.outputIndex and edge.inputIndex is an integer in [0, 100].
  3. Read response.body.reason — it names the failing rule and the offending edge/node.
  4. Run validateExecutableGraph() (or an equivalent local check) before POSTing during development.

Example fix

// before — no trigger node, invalid slot index
graph = {
  nodes: [{ id: 'n1', name: 'Do', type: 'v1-node' }],
  edges: [{ from: 'n1', to: 'n1', outputIndex: -1, inputIndex: 0 }]
};
// after — one trigger node, non-negative integer indices
graph = {
  nodes: [
    { id: 't', name: 'Start', type: 'trigger' },
    { id: 'n1', name: 'Do', type: 'v1-node' }
  ],
  edges: [{ from: 't', to: 'n1', outputIndex: 0, inputIndex: 0 }]
};
Defensive patterns

Strategy: validation

Validate before calling

const MAX_SLOT_INDEX = 100;
function assertValidGraphStructure(graph) {
  const triggers = graph.nodes.filter(n => n.type === 'trigger');
  if (triggers.length !== 1) throw new Error(`Expected exactly 1 trigger, got ${triggers.length}`);
  for (const e of graph.edges) {
    for (const idx of [e.outputIndex ?? 0, e.inputIndex ?? 0]) {
      if (!Number.isInteger(idx) || idx < 0 || idx > MAX_SLOT_INDEX) {
        throw new Error(`Bad slot index ${idx} on edge ${e.from}->${e.to}`);
      }
    }
  }
}

Type guard

function isStructurallyValidGraph(graph) {
  const triggers = graph.nodes.filter(n => n.type === 'trigger');
  if (triggers.length !== 1) return false;
  return graph.edges.every(e =>
    [e.outputIndex ?? 0, e.inputIndex ?? 0].every(i => Number.isInteger(i) && i >= 0 && i <= 100)
  );
}

Try / catch

const res = await fetch('/workflow-executions', { method: 'POST', body: JSON.stringify(payload) });
if (res.status === 400) {
  const body = await res.json();
  if (body.error === 'invalid_graph') {
    // body.reason names the exact structural rule that failed
  }
}

Prevention

When it happens

Trigger: POST /workflow-executions with a graph that has no node of type 'trigger'; a graph with two 'trigger' nodes; an edge whose outputIndex or inputIndex is a float, negative, or greater than 100. The reason field echoes the exact GraphValidationError message.

Common situations: Building a graph programmatically and forgetting the trigger node. Duplicating a trigger node during composition. Sending connection indices derived from a zero-based vs one-based mismatch. Sending a placeholder index like 999 that exceeds MAX_SLOT_INDEX.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/b9b8b7c2b6c58e65. Report an issue: GitHub.

Appendix: source

Thrown at packages/@n8n/engine/src/server/routes/workflow-executions.ts:75

		const parsed = StartExecutionBody.safeParse(req.body);
		if (!parsed.success) {
			res.status(400).json({
				error: 'invalid_request',
				details: parsed.error.flatten(),
			});
			return;
		}

		try {
			const result = await startExecution.start(parsed.data);
			res.status(201).json(result);
		} catch (error) {
			if (error instanceof AdmittanceRejectedError) {
				res.status(429).json({ error: 'admittance_rejected', reason: error.reason });
				return;
			}
			if (error instanceof GraphValidationError) {
				res.status(400).json({ error: 'invalid_graph', reason: error.message });
				return;
			}
			if (error instanceof UnimplementedError) {
				res.status(501).json({ error: 'unimplemented', reason: error.message });
				return;
			}
			throw error;
		}
	});

	return router;
}

View on GitHub (pinned to 5ac6606e81)