n8n import:workflow로 워크플로를 올린 뒤 스케줄이 하나도 돌지 않는다면, 워크플로가 꺼진 것입니다.
문서도 import:workflow가 기본적으로 임포트한 워크플로를 전부 비활성화한다고 적습니다. 제가 쓰는 2.13.4에서는 active가 false가 되고 실행 대상을 가리키는 activeVersionId가 null로 비워집니다.
명령은 성공 메시지와 종료 코드 0을 내니 CI에서는 초록불입니다.
n8n import:workflow --input=wf.json # 여기서 꺼진다
n8n publish:workflow --id=<id> # ← 이 줄이 있어야 다시 돈다
docker restart <n8n container> # 트리거 재등록
배포가 반영됐는지는 믿는 게 아니라 물어봐야 합니다. 저는 셋을 봅니다.
| 확인할 것 | 정상값 | 어긋나면 |
|---|---|---|
workflow_entity.active |
1 | 워크플로가 꺼져 있다 |
triggerCount |
19 (스케줄 18 + 웹훅 1) | 0이면 아무것도 등록되지 않았고, 작으면 일부 유실 |
| 오늘 수집된 원문 | 평일 1,527건 근처 | 두 자리 수면 설정은 맞고 결과가 안 나오는 것 |
아래는 import에 왜 활성화 옵션이 없는지, 스위치가 왜 둘인지, 경고가 있었는데 왜 놓쳤는지, 침묵을 어떻게 감시하는지입니다.
제 시스템은 자동화 전부가 워크플로 하나에 들어 있습니다. 이 값 하나가 0이 되면 하루 409번 돌던 것이 0번 돕니다.
import 에 활성화 옵션이 왜 없을까?
제가 쓰는 2.13.4에서 import:workflow가 받는 인자는 input, separate, userId, projectId 넷이 전부이고 활성화 여부를 지정할 방법이 없습니다.
없는 이유는 임포트 서비스를 열어보면 나옵니다.
선택지가 없고 정해진 동작이기 때문입니다.
// dist/services/import.service.js (n8n 2.13.4)
workflow.versionId = uuid.v4();
const oldActiveVersionId = workflow.id ? activeVersionIdByWorkflow.get(workflow.id) : null;
if (oldActiveVersionId || workflow.activeVersionId || workflow.active) {
this.logger.info(`Deactivating workflow "${workflow.name}". Remember to activate later.`);
}
workflow.active = false; // ← 조건 없음
workflow.activeVersionId = null; // ← 조건 없음
await tx.upsert(WorkflowEntity, workflow, ['id']);
조건문이 감싸는 것은 로그 메시지뿐입니다.
두 줄에 if가 붙어 있지 않습니다. 임포트하면 무조건 꺼집니다.
지금 문서에는 한 줄이 더 있습니다
--activeState=fromJson을 주면 JSON 파일의 active 값을 그대로 쓴다고 적혀 있고, 다만 multi-main과 queue 모드에서만 동작합니다. 제 인스턴스는 단일이라 이 옵션이 있어도 결과는 같습니다.
문서가 버전을 적어두지 않아 2.13.4에 이 옵션이 있었는지는 확인하지 못했고, 위 소스는 제가 쓰는 버전의 것입니다.
스위치가 왜 둘일까?
2.x에는 층이 하나 더 있습니다.
versionId 는 워크플로에 저장된 현재 내용입니다.
activeVersionId 는 지금 실행 중인 내용입니다.
임포트는 앞쪽에 새 UUID를 발급하고 뒤쪽을 null로 비웁니다.
“새 정의가 저장되지만 옛 정의가 계속 돈다”는 상황과 다릅니다. 실행 대상을 가리키는 포인터가 아예 사라지고, 스키마가 그 관계를 강제합니다.
CREATE TABLE "workflow_entity" (
...
"active" boolean NOT NULL, -- DEFAULT 절 없음: 값은 애플리케이션이 정한다
"versionId" varchar(36) NOT NULL,
"activeVersionId" varchar(36),
CONSTRAINT "FK_..." FOREIGN KEY ("activeVersionId")
REFERENCES "workflow_history" ("versionId") ON DELETE RESTRICT
);
되돌리는 건 별도 명령어 publish:workflow입니다. 설명도 그렇게 말합니다.
“Publish a specific version of a workflow. If no version is specified, publishes the current version.”
임포트한 내용은 발행하기 전까지 초안이고, 임포트 서비스가 마지막에 호출하는 함수 이름도 updateIndexForDraft입니다.
제 운영 문서에는 “n8n import:workflow는 워크플로를 비활성화하므로 활성화 + 재시작 필수”라는 한 줄이 있습니다. 한 줄짜리 체크리스트 항목이 있다는 건 한 번 당했다는 뜻입니다.
그 한 줄이 이 글의 출발점입니다.
경고는 있었는데 왜 놓쳤나?
저는 이 사건을 오랫동안 “아무 경고 없이 꺼진 것”으로 기억하고 있었는데, 소스를 열어보니 n8n은 평문으로 경고합니다.
$ n8n import:workflow --input=wf.json
Deactivating workflow "Market Risk Radar - Complete". Remember to activate later.
Successfully imported 1 workflow.
$ echo $?
0
그럼에도 왜 놓칠까요?
이 메시지의 위치와 등급 때문입니다. 등급이 info이고, 바로 다음 줄이 성공 메시지이고, 종료 코드는 0입니다. 마지막 줄만 읽으면 완벽하게 성공한 배포입니다.
그래서 이건 도구의 실패가 아니라 읽기의 실패이고, 읽기의 실패는 체크리스트로는 잘 안 고쳐집니다. 다음에도 똑같이 마지막 줄만 볼 테니까요.
고치려면 사람이 로그를 읽는 대신 기계가 상태를 확인하게 만들어야 합니다.
docker exec <n8n> node -e '
const {DatabaseSync} = require("node:sqlite");
const db = new DatabaseSync("file:/home/node/.n8n/database.sqlite?mode=ro");
console.log(db.prepare(
"SELECT id, active, triggerCount, versionId, activeVersionId FROM workflow_entity"
).all());
'
읽기 전용(mode=ro)으로 엽니다.
확인하러 들어간 셸에서 실수로 쓰는 일을 원천 차단합니다.
정상 상태에서는 active가 1, triggerCount가 19, versionId와 activeVersionId가 같습니다.
triggerCount가 0이면 파일은 올라갔지만 아무것도 등록되지 않은 것이고, 기대보다 작으면 일부 트리거가 유실된 것입니다. 노드 개수 대신 등록된 트리거 개수를 봐야 합니다.
멈춘 것을 어떻게 알아차릴까?
여기 두 번째 문제가 겹칩니다.
정지를 알아차릴 방법이 없습니다.
이 파이프라인의 출력은 슬랙 알림인데, 알림이 안 오면 시스템이 멈춘 것일 수도 있고 알릴 만한 일이 없었던 것일 수도 있습니다. 후자가 정상 레퍼토리에 있습니다.
이 시스템은 주말에 아무것도 하지 않도록 설계돼 있습니다(모든 스케줄이 월~금). 8월 수집량은 평일 22,912건(하루 평균 1,527건), 주말 0건입니다.
토요일 아침에 조용한 것은 정상이고, 그래서 월요일 아침의 침묵도 정상처럼 보입니다.
조용함이 정상 신호인 시스템에서는 조용함으로 고장을 감지할 수 없습니다.
마지막 확인은 산출물 쪽에서 합니다.
위 조회는 설정이 맞다고 말해줄 뿐 결과가 나오는지는 말해주지 않습니다.
오늘 수집된 원문이 평소 범위 안에 있는가를 보고, 평일 1,527건 근처면 정상이고 두 자리 수면 뭔가 잘못된 겁니다.
침묵을 모니터링하려면 침묵의 정상 범위를 먼저 정의해야 하고, 그건 시스템의 스케줄을 알아야만 쓸 수 있습니다. 모니터링이 배포보다 어려운 이유가 대개 여기 있습니다.
남는 것
배포 도구의 기본값은 배포자의 의도를 모릅니다. import가 워크플로를 켜지 않는 건 합리적인 선택입니다. 남의 워크플로 파일을 받아 무심코 실행시키는 것보다는 꺼진 채로 들어오는 게 안전합니다. 문제는 그 안전한 기본값이 내 상황에서는 사고의 형태 그대로라는 것입니다.
“성공”은 명령어의 판단이지 내 목표의 판단이 아닙니다. Successfully imported 1 workflow.는 참말입니다. 파일은 임포트됐습니다. 다만 제가 원한 건 “자동화가 새 정의로 계속 도는 것”이었고, 명령어는 그것에 대해 아무 말도 하지 않았습니다. 종료 코드는 “내가 시킨 일을 했다”는 뜻이지 “네가 원한 상태가 됐다”는 뜻이 아닙니다.
경고는 읽히는 위치에 있어야 경고입니다. n8n은 분명히 말해줬습니다. 다만 그 말이 info 등급으로, 성공 메시지 바로 위에, 종료 코드 0과 함께 나왔습니다. 사람이 마지막 줄만 읽는다는 건 잘못이라기보다 예측 가능한 사실입니다. 그러니 “다음엔 잘 읽자”는 대책이 아니고, 대책은 로그를 읽는 자리에 상태를 조회하는 명령 세 줄을 놓는 것입니다.
자주 묻는 질문
import 말고 UI 에서 올리면 안 꺼지나요
UI 임포트는 다른 경로입니다. 이 글이 다루는 것은 CLI import:workflow 이고, 꺼지는 동작은 임포트 서비스에 조건 없이 박혀 있습니다. 다만 저는 UI 경로를 쓰지 않아서 UI 쪽 동작을 관측하지 못했습니다.
publish:workflow 만 하면 되나요, 재시작도 필요한가요
제 절차는 둘 다입니다. publish:workflow 가 실행 대상 포인터를 되살리고, 컨테이너 재시작이 스케줄 트리거를 다시 등록합니다. 재시작 없이 triggerCount 가 기대값으로 돌아오는지는 라이브에서 시험하지 않았습니다.
왜 REST API 를 쓰지 않나요
제가 쓰는 2.13.4에서 API 키 발급이 안 되고, N8N_BASIC_AUTH_ACTIVE=true 가 설정돼 있어도 REST 요청에 401이 옵니다. 최신 버전에서 이 환경변수는 사실상 죽은 값이라, 설정이 있는데 아무것도 하지 않는 상태가 “인증이 켜져 있다”고 착각하기 딱 좋습니다. 남는 건 CLI 뿐입니다.
임포트가 꺼뜨린 기록이 DB 에 남나요
n8n 2.x 에는 workflow_publish_history 테이블이 있고, 임포트가 활성 워크플로를 끌 때 event = 'deactivated' 행을 남깁니다. 제 인스턴스에서는 이 테이블이 비어 있는데, 마지막 임포트가 이 기능이 생기기 전이었기 때문으로 보입니다.
참고 자료
- n8n 문서 Use the command line 은
import:workflow가 임포트한 워크플로를 비활성화한다는 것과, multi-main·queue 모드 전용--activeState=fromJson옵션의 근거입니다.
부록 1: 왜 CLI로 배포하는가
저는 n8n 워크플로를 커맨드라인으로 배포합니다. 변경을 코드처럼 다루고 싶어서이기도 하지만, 사실은 선택지가 없어서입니다. 제가 쓰는 n8n 2.13.4에서 REST API는 API 키 발급이 안 되는 상태라 쓸 수 없고, 기본 인증은 컨테이너에 N8N_BASIC_AUTH_ACTIVE=true가 설정돼 있어도 REST 요청에 401을 냅니다.
n8n export:workflow --id=<id> --output=wf.json # 라이브 내려받기
# ... JSON 편집 ...
n8n import:workflow --input=wf.json # 올리기
n8n publish:workflow --id=<id> # ← 이 줄
docker restart <n8n container> # 트리거 재등록
부록 2: 이 글에서 하지 않은 것
저는 이 메커니즘을 재현 실험으로 증명하지 않았습니다. 증명하려면 라이브 인스턴스에 import를 다시 걸어 자동화를 정지시켜야 하는데, 돌아가는 시스템에서 그럴 이유가 없습니다.
이 글의 근거는 셋입니다.
- 임포트 서비스 소스 (
workflow.active = false와workflow.activeVersionId = null이 조건 없이 실행됨) - 데이터베이스 스키마 (
active가NOT NULL이면서DEFAULT없음,activeVersionId가versionId와 별개 컬럼) - 사고 이후 운영 문서에 남은 절차 기록
앞의 둘은 누구나 자기 인스턴스에서 확인할 수 있고 세 번째는 제 기록입니다. “소스가 이렇게 생겼다”와 “이렇게 동작하는 것을 관측했다”는 다른 주장이라 구분해 둡니다.
부록 3: 재현
import:workflow의 인자 스키마(명령어 소스 그대로)와, 이 글의 수치를 확인한 명령입니다.
const flagsSchema = z.object({
input: z.string().alias('i').describe('Input file name or directory ...').optional(),
separate: z.boolean().describe('Imports *.json files from directory ...').default(false),
userId: z.string().describe('The ID of the user to assign ...').optional(),
projectId: z.string().describe('The ID of the project to assign ...').optional(),
});
n8n list:workflow # 워크플로 목록
n8n export:workflow --id=<id> --output=/tmp/wf.json # 노드·트리거 구성
docker exec <n8n> node -e '...' # workflow_entity 읽기 전용 조회
워크플로 1개, 노드 65개, 스케줄 트리거 18개, 등록된 트리거 19개(스케줄 18 + 웹훅 1). 평일 409회는 워크플로 JSON에서 18개 스케줄 노드의 cron 식을 추출해 하루치 발동을 세어 계산했습니다(금요일은 주간 리마인더가 더해져 410회).
이 글에서 고친 것 1개
- 2026-09-27 — 임포트가 비활성화한다는 근거로 n8n 문서를 달았고, “활성화 여부를 지정할 방법이 없다”에 버전을 붙였습니다. 지금 문서에는
--activeState=fromJson이 있습니다. multi-main과 queue 모드 전용이라 단일 인스턴스의 결론은 그대로이지만, 버전을 적지 않고 단정한 것은 제 잘못입니다.
이 사이트의 수치는 주 1회 DB와 다시 대조하고, 판단이 바뀌면 지우지 않고 글 안에 덧붙입니다. 갱신은 RSS로 받을 수 있습니다.