Fix: Unrecognized node type: n8n-nodes-base.start

I’m a long time n8n user. Using it since 2020. I have so many workflows which we’re not updated since I created them.

That time we request the n8n team to remove the n8n start node. Finally they did after long time.

Recently I updated my n8n instance to v2. Guess what. Before updating to the newer version, I was supposed to remove all the start nodes. Which I never did.

When I restarted the n8n container, I started getting “Unrecognized node type: n8n-nodes-base.start” error.

I don’t have access to Web UI. So I can’t remove it.

So I wrote a bash script which pulls all the workflows, remove the start node code and patch it.

This only works if you have n8n running on Docker.

Save the below code with the filename anywhere on your Linux machine.

fix_start_nodes.sh you’ll need your n8n container name. In my case it’s n8n-n8n_main

Then you have to run ./fix_start_nodes.sh n8n-n8n_main-1 the script will patch everything.

Once done. Restart the container, Restart the Webserver and you’re good to go.

#!/usr/bin/env bash
#
# fix_start_nodes.sh
#
# Full auto version. For every n8n workflow still containing the legacy
# "n8n-nodes-base.start" node (removed in n8n v2):
#   1. Exports all workflows, finds the broken ones.
#   2. For any broken workflow currently ACTIVE, stops the container and
#      flips it to active=0 directly in the SQLite DB (so n8n won't try
#      to parse the broken node while deactivating during import).
#   3. Restarts, patches each broken workflow's Start node -> Manual
#      Trigger, and re-imports.
#   4. Stops the container again, flips the originally-active ones back
#      to active=1 in the DB.
#   5. Restarts n8n so it activates them cleanly using the fixed nodes.
#
# Requires on HOST: jq, python3 (both usually preinstalled on Ubuntu).
# Requires in CONTAINER: n8n CLI (already there).
#
# Usage:
#   ./fix_start_nodes.sh [container_name] [sqlite_path_in_container]
#
# Defaults:
#   container_name          = n8n-n8n_main-1
#   sqlite_path_in_container = /home/node/.n8n/database.sqlite

set -euo pipefail

CONTAINER="${1:-n8n-n8n_main-1}"
DB_PATH_IN_CONTAINER="${2:-/home/node/.n8n/database.sqlite}"
WORKDIR="$(mktemp -d)"
EXPORT_DIR_IN_CONTAINER="/tmp/n8n_export"

echo "==> Container: ${CONTAINER}"
echo "==> DB path (in container): ${DB_PATH_IN_CONTAINER}"
echo "==> Scratch dir: ${WORKDIR}"

for bin in jq python3 docker; do
  if ! command -v "$bin" >/dev/null 2>&1; then
    echo "ERROR: '$bin' not found on host. Please install it."
    exit 1
  fi
done

if ! docker ps -a --format '{{.Names}}' | grep -qx "${CONTAINER}"; then
  echo "ERROR: container '${CONTAINER}' not found."
  docker ps -a --format '  - {{.Names}}'
  exit 1
fi

# Make sure container is running for the initial export.
if ! docker ps --format '{{.Names}}' | grep -qx "${CONTAINER}"; then
  echo "==> Container not running, starting it..."
  docker start "${CONTAINER}"
  sleep 5
fi

echo "==> Exporting all workflows..."
docker exec "${CONTAINER}" sh -c "rm -rf ${EXPORT_DIR_IN_CONTAINER} && mkdir -p ${EXPORT_DIR_IN_CONTAINER}"
docker exec "${CONTAINER}" n8n export:workflow --all --separate --output="${EXPORT_DIR_IN_CONTAINER}"
docker cp "${CONTAINER}:${EXPORT_DIR_IN_CONTAINER}" "${WORKDIR}/export"

BROKEN_IDS=()
BROKEN_ACTIVE_IDS=()

for f in "${WORKDIR}/export"/*.json; do
  [ -e "$f" ] || continue
  HAS_START=$(jq '[.nodes[]? | select(.type == "n8n-nodes-base.start")] | length' "$f")
  [ "$HAS_START" -eq 0 ] && continue

  WF_ID=$(jq -r '.id' "$f")
  WF_NAME=$(jq -r '.name' "$f")
  WF_ACTIVE=$(jq -r '.active' "$f")

  echo "==> Broken: \"${WF_NAME}\" (ID: ${WF_ID}, active: ${WF_ACTIVE})"
  BROKEN_IDS+=("$WF_ID")
  if [ "$WF_ACTIVE" = "true" ]; then
    BROKEN_ACTIVE_IDS+=("$WF_ID")
  fi

  # Patch the node type in place, keep everything else.
  START_NAMES=$(jq -r '[.nodes[] | select(.type == "n8n-nodes-base.start") | .name] | .[]' "$f")
  TMP="${f}.tmp.json"
  cp "$f" "$TMP"
  while IFS= read -r START_NAME; do
    [ -z "$START_NAME" ] && continue
    jq --arg name "$START_NAME" '
      .nodes = [
        .nodes[] |
        if .name == $name then
          .type = "n8n-nodes-base.manualTrigger"
          | .typeVersion = 1
          | .parameters = {}
        else . end
      ]
    ' "$TMP" > "${TMP}.out" && mv "${TMP}.out" "$TMP"
  done <<< "$START_NAMES"
  mv "$TMP" "$f"
done

if [ "${#BROKEN_IDS[@]}" -eq 0 ]; then
  echo "==> No workflows with legacy Start node found. Nothing to do."
  exit 0
fi

echo ""
echo "==> Total broken: ${#BROKEN_IDS[@]}  |  Currently active (need DB toggle): ${#BROKEN_ACTIVE_IDS[@]}"

# ---- Step: if any broken workflow is active, deactivate it directly in the DB ----
if [ "${#BROKEN_ACTIVE_IDS[@]}" -gt 0 ]; then
  echo "==> Stopping container to safely patch SQLite DB..."
  docker stop "${CONTAINER}"

  echo "==> Copying DB out to host..."
  docker cp "${CONTAINER}:${DB_PATH_IN_CONTAINER}" "${WORKDIR}/database.sqlite"

  ID_LIST=$(printf "'%s'," "${BROKEN_ACTIVE_IDS[@]}")
  ID_LIST="${ID_LIST%,}"  # trim trailing comma

  echo "==> Setting active=0 for IDs: ${BROKEN_ACTIVE_IDS[*]}"
  python3 - "$WORKDIR/database.sqlite" "$ID_LIST" <<'PYEOF'
import sqlite3, sys
db_path, id_list = sys.argv[1], sys.argv[2]
conn = sqlite3.connect(db_path)
sql = f"UPDATE workflow_entity SET active = 0 WHERE id IN ({id_list})"
cur = conn.execute(sql)
conn.commit()
print(f"Rows updated: {cur.rowcount}")
conn.close()
PYEOF

  echo "==> Copying patched DB back into container..."
  docker cp "${WORKDIR}/database.sqlite" "${CONTAINER}:${DB_PATH_IN_CONTAINER}"

  echo "==> Starting container..."
  docker start "${CONTAINER}"
  sleep 5
fi

# ---- Step: copy fixed workflow JSON back in and re-import ----
echo "==> Copying fixed workflows back into container..."
docker cp "${WORKDIR}/export" "${CONTAINER}:${EXPORT_DIR_IN_CONTAINER}_fixed"

echo "==> Re-importing fixed workflows..."
docker exec "${CONTAINER}" n8n import:workflow --separate --input="${EXPORT_DIR_IN_CONTAINER}_fixed"

# ---- Step: reactivate the ones that were originally active ----
if [ "${#BROKEN_ACTIVE_IDS[@]}" -gt 0 ]; then
  echo "==> Stopping container to re-activate fixed workflows in DB..."
  docker stop "${CONTAINER}"

  docker cp "${CONTAINER}:${DB_PATH_IN_CONTAINER}" "${WORKDIR}/database2.sqlite"

  ID_LIST=$(printf "'%s'," "${BROKEN_ACTIVE_IDS[@]}")
  ID_LIST="${ID_LIST%,}"

  echo "==> Setting active=1 for IDs: ${BROKEN_ACTIVE_IDS[*]}"
  python3 - "$WORKDIR/database2.sqlite" "$ID_LIST" <<'PYEOF'
import sqlite3, sys
db_path, id_list = sys.argv[1], sys.argv[2]
conn = sqlite3.connect(db_path)
sql = f"UPDATE workflow_entity SET active = 1 WHERE id IN ({id_list})"
cur = conn.execute(sql)
conn.commit()
print(f"Rows updated: {cur.rowcount}")
conn.close()
PYEOF

  docker cp "${WORKDIR}/database2.sqlite" "${CONTAINER}:${DB_PATH_IN_CONTAINER}"

  echo "==> Starting container (final)..."
  docker start "${CONTAINER}"
fi

echo ""
echo "==> Done. Fixed workflow IDs: ${BROKEN_IDS[*]}"
echo "==> Re-activated (were active before): ${BROKEN_ACTIVE_IDS[*]:-none}"
echo ""
echo "==> Tail logs to confirm clean activation:"
echo "    docker logs -f ${CONTAINER}"
2 « J'aime »

Good fix to share - the Start node deprecation caught quite a few long-running instances by surprise, especially those that updated without checking the migration notes first. For anyone who still has Web UI access and only has a handful of affected workflows, a faster path is to open each workflow, delete the Start node (shows as greyed out with a warning), reconnect the trigger node directly to the next step, and save. For bulk cases or when the UI is inaccessible like in your situation, the script approach is the right call.