반복 업무 자동화, 계정 없이 n8n으로 먼저 확인하기
n8n은 여러 앱과 API를 코드 없이 연결하는 자동화 도구입니다. 계정 다섯 개 만들기 전에, 내 반복 업무 자동화가 실제로 되는지부터 확인하고 싶은 분들을 위해 그대로 가져와 실행할 수 있는 9가지를 준비했습니다.
01 daily-todo-digest
02 weekly-window-aggregate
03 monthly-kpi-report
04 timezone-clock-compare
05 interval-vs-cron-expression
06 skip-weekend-gate
07 missed-run-catchup
08 digest-email-body
09 multi-step-weather-pipeline
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 매일 할 일 요약
- 02 주간 구간 집계
- 03 월간 리포트 뼈대
- 04 워크플로 시계 vs 서버 시계
- 05 간격 설정 vs 크론 표현식
- 06 주말은 건너뛰는 조건 게이트
- 07 놓친 실행 따라잡기
- 08 다이제스트 이메일 본문
- 09 받기·바꾸기·꾸미기 3단 파이프라인
아홉 개는 단순 반복 업무 자동화가 실제로 자리 잡는 순서대로 놓았습니다. 먼저 매일·매주·매월 정해진 시각에 도는 기본 패턴 셋(01 일간·02 주간·03 월간)으로 시작해, 이 자동화가 흔히 걸려 넘어지는 시간대 함정(04)과 트리거 문법(05)을 짚고, 실제로 도는지 조건으로 거르는 법(06)과 놓친 실행을 따라잡는 법(07)을 지나, 마지막으로 결과를 완성된 형태로 꾸미는 두 가지(08 이메일 본문·09 3단 파이프라인)로 끝납니다. 실제 업무 자동화도 대개 이 순서로 다듬어집니다 — 일단 돌고, 시간이 맞는지 확인하고, 예외를 거르고, 보기 좋게 다듬습니다. 돌린 결과를 JSON·CSV·엑셀로 가공하는 뒷단은 엑셀 자동화 예제 9가지가 맡고, 요청을 주고받는 앞단은 노코드 API 연결 9가지가 맡습니다.
01매일 할 일 요약
무엇을 자동화하나
Schedule 트리거가 아침 8시마다 깨어나고(Manual Trigger 로도 바로 테스트됩니다) HTTP Request 가 할 일 목록을 가져옵니다. Filter 가 한 사람 몫만 남기고, Code 노드가 완료 여부를 체크박스로 찍은 다섯 줄 요약을 만듭니다.

<main class="stage">
<svg class="flow" viewBox="-28 -28 654 212" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="godrich 204-01 daily-todo-digest">
<path class="edge" d="M170 28 C299 28 299 28 428 28"/>
<path class="edge edge--flow" style="--i:0" d="M170 28 C299 28 299 28 428 28"/>
<circle class="pulse" style="--i:0;offset-path:path('M170 28 C299 28 299 28 428 28')" r="7"/>
<path class="edge" d="M384 28 C406 28 406 28 428 28"/>
<path class="edge edge--flow" style="--i:1" d="M384 28 C406 28 406 28 428 28"/>
<circle class="pulse" style="--i:1;offset-path:path('M384 28 C406 28 406 28 428 28')" r="7"/>
<path class="edge" d="M513 56 C513 78 85 78 85 100"/>
<path class="edge edge--flow" style="--i:2" d="M513 56 C513 78 85 78 85 100"/>
<circle class="pulse" style="--i:2;offset-path:path('M513 56 C513 78 85 78 85 100')" r="7"/>
<path class="edge" d="M170 128 C192 128 192 128 214 128"/>
<path class="edge edge--flow" style="--i:3" d="M170 128 C192 128 192 128 214 128"/>
<circle class="pulse" style="--i:3;offset-path:path('M170 128 C192 128 192 128 214 128')" r="7"/>
<path class="edge" d="M384 128 C406 128 406 128 428 128"/>
<path class="edge edge--flow" style="--i:4" d="M384 128 C406 128 406 128 428 128"/>
<circle class="pulse" style="--i:4;offset-path:path('M384 128 C406 128 406 128 428 128')" r="7"/>
<g class="node node--trigger" style="--i:0" transform="translate(0 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Schedule Trigger</text><text class="node__type" x="14" y="42">Schedule Trigger</text></g>
<g class="node node--trigger" style="--i:1" transform="translate(214 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Manual Trigger</text><text class="node__type" x="14" y="42">Manual Trigger</text></g>
<g class="node" style="--i:2" transform="translate(428 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Get Todos</text><text class="node__type" x="14" y="42">Http Request</text></g>
<g class="node" style="--i:3" transform="translate(0 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Filter User 3</text><text class="node__type" x="14" y="42">Filter</text></g>
<g class="node" style="--i:4" transform="translate(214 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Limit</text><text class="node__type" x="14" y="42">Limit</text></g>
<g class="node" style="--i:5" transform="translate(428 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Build Summary</text><text class="node__type" x="14" y="42">Code</text></g>
</svg>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 한 바퀴 (데모 2초와 같은 값 — gif 가 끊기지 않게)
$easing: linear; // 대시 흐름은 등속
$color: $subject-blue; // 강조(흐름·켜진 노드) 색
$ink: $subject-cream; // 글자·테두리
$n: 6; // 노드 수 — 켜지는 순서 간격
@include stage($stage-ink);
.flow { width: 100%; height: 100%; max-height: 100vh; display: block; overflow: visible; }
.edge { fill: none; stroke: $ink; stroke-opacity: .28; stroke-width: 3; stroke-linecap: round; }
.edge--flow { stroke: $color; stroke-opacity: 1; stroke-dasharray: 10 14; animation: flow $duration $easing infinite; }
@keyframes flow { to { stroke-dashoffset: -48; } }
.node__box { fill: transparent; stroke: $ink; stroke-opacity: .45; stroke-width: 2; rx: $r-sm; }
.node__name { fill: $ink; font-weight: 700; font-size: 17px; font-family: -apple-system, "Segoe UI", sans-serif; }
.node__type { fill: $ink; fill-opacity: .6; font-weight: 500; font-size: 12px; font-family: -apple-system, "Segoe UI", sans-serif; letter-spacing: .04em; }
.node--trigger .node__box { stroke-dasharray: 6 5; }
// 켜진 노드는 테두리가 강조색으로 두꺼워진다 (filter 대신 stroke — 렌더 비용). --i 는 <g> 에서 상속된다
.node .node__box { animation: box-lit $duration $ease-spring infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes box-lit {
0%, 100% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
12%, 32% { stroke: $color; stroke-opacity: 1; stroke-width: 4; }
50% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
}
.pulse { fill: $color; offset-distance: 0%; offset-rotate: 0deg; animation: travel $duration $ease-out infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes travel {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
45% { offset-distance: 100%; opacity: 1; }
55%, 100% { offset-distance: 100%; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse { animation: none; }
.edge--flow { stroke-dasharray: none; } // 흐름은 멈추고 연결선만 남긴다
.pulse { display: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.flow {
width: 100%;
height: 100%;
max-height: 100vh;
display: block;
overflow: visible;
}
.edge {
fill: none;
stroke: #fff7e6;
stroke-opacity: 0.28;
stroke-width: 3;
stroke-linecap: round;
}
.edge--flow {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-dasharray: 10 14;
animation: flow 2s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -48;
}
}
.node__box {
fill: transparent;
stroke: #fff7e6;
stroke-opacity: 0.45;
stroke-width: 2;
rx: 8px;
}
.node__name {
fill: #fff7e6;
font-weight: 700;
font-size: 17px;
font-family: -apple-system, "Segoe UI", sans-serif;
}
.node__type {
fill: #fff7e6;
fill-opacity: 0.6;
font-weight: 500;
font-size: 12px;
font-family: -apple-system, "Segoe UI", sans-serif;
letter-spacing: 0.04em;
}
.node--trigger .node__box {
stroke-dasharray: 6 5;
}
.node .node__box {
animation: box-lit 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: calc(var(--i) * 2s / 6);
}
@keyframes box-lit {
0%, 100% {
stroke: #fff7e6;
stroke-opacity: 0.45;
stroke-width: 2;
}
12%, 32% {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-width: 4;
}
50% {
stroke: #fff7e6;
stroke-opacity: 0.45;
stroke-width: 2;
}
}
.pulse {
fill: #2f6df6;
offset-distance: 0%;
offset-rotate: 0deg;
animation: travel 2s ease-out infinite;
animation-delay: calc(var(--i) * 2s / 6);
}
@keyframes travel {
0% {
offset-distance: 0%;
opacity: 0;
}
10% {
opacity: 1;
}
45% {
offset-distance: 100%;
opacity: 1;
}
55%, 100% {
offset-distance: 100%;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse {
animation: none;
}
.edge--flow {
stroke-dasharray: none;
}
.pulse {
display: none;
}
}01 · 매일 할 일 요약 — 측정 누적면적 6.786% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
JSON에서 볼 곳
아침 업무 브리핑이나 완료 항목 확인처럼, 매일 같은 걸 점검하는 반복 업무에 그대로 씁니다.
{
"name": "Build Summary",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const items = $input.all();\nconst lines = items.map(item => `- [${item.json.completed ? 'x' : ' '}] ${item.json.title}`);\nconst summary = `# Daily Summary\\n\\n${items.length} to-dos for user 3 today\\n\\n` + lines.join('\\n');\nreturn [{ json: { summary } }];"
}
}
02주간 구간 집계
무엇을 자동화하나
Schedule 이 월요일 09시에 깨어나 HTTP 로 댓글 전체를 가져오면 Code 가 이번 주 몫만 남깁니다. Summarize 노드가 Aggregate 와 달리 글(postId)별로 그룹을 나눠서 댓글 수를 셉니다.

<main class="stage">
<svg class="flow" viewBox="-28 -28 654 212" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="godrich 204-02 weekly-window-aggregate">
<path class="edge" d="M170 28 C299 28 299 28 428 28"/>
<path class="edge edge--flow" style="--i:0" d="M170 28 C299 28 299 28 428 28"/>
<circle class="pulse" style="--i:0;offset-path:path('M170 28 C299 28 299 28 428 28')" r="7"/>
<path class="edge" d="M384 28 C406 28 406 28 428 28"/>
<path class="edge edge--flow" style="--i:1" d="M384 28 C406 28 406 28 428 28"/>
<circle class="pulse" style="--i:1;offset-path:path('M384 28 C406 28 406 28 428 28')" r="7"/>
<path class="edge" d="M513 56 C513 78 85 78 85 100"/>
<path class="edge edge--flow" style="--i:2" d="M513 56 C513 78 85 78 85 100"/>
<circle class="pulse" style="--i:2;offset-path:path('M513 56 C513 78 85 78 85 100')" r="7"/>
<path class="edge" d="M170 128 C192 128 192 128 214 128"/>
<path class="edge edge--flow" style="--i:3" d="M170 128 C192 128 192 128 214 128"/>
<circle class="pulse" style="--i:3;offset-path:path('M170 128 C192 128 192 128 214 128')" r="7"/>
<g class="node node--trigger" style="--i:0" transform="translate(0 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Schedule Trigger</text><text class="node__type" x="14" y="42">Schedule Trigger</text></g>
<g class="node node--trigger" style="--i:1" transform="translate(214 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Manual Trigger</text><text class="node__type" x="14" y="42">Manual Trigger</text></g>
<g class="node" style="--i:2" transform="translate(428 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Get Comments</text><text class="node__type" x="14" y="42">Http Request</text></g>
<g class="node" style="--i:3" transform="translate(0 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Keep This Week</text><text class="node__type" x="14" y="42">Code</text></g>
<g class="node" style="--i:4" transform="translate(214 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Count Per Post</text><text class="node__type" x="14" y="42">Summarize</text></g>
</svg>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 한 바퀴 (데모 2초와 같은 값 — gif 가 끊기지 않게)
$easing: linear; // 대시 흐름은 등속
$color: $subject-blue; // 강조(흐름·켜진 노드) 색
$ink: $subject-ink; // 글자·테두리
$n: 5; // 노드 수 — 켜지는 순서 간격
@include stage($stage-yellow);
.flow { width: 100%; height: 100%; max-height: 100vh; display: block; overflow: visible; }
.edge { fill: none; stroke: $ink; stroke-opacity: .28; stroke-width: 3; stroke-linecap: round; }
.edge--flow { stroke: $color; stroke-opacity: 1; stroke-dasharray: 10 14; animation: flow $duration $easing infinite; }
@keyframes flow { to { stroke-dashoffset: -48; } }
.node__box { fill: transparent; stroke: $ink; stroke-opacity: .45; stroke-width: 2; rx: $r-sm; }
.node__name { fill: $ink; font-weight: 700; font-size: 17px; font-family: -apple-system, "Segoe UI", sans-serif; }
.node__type { fill: $ink; fill-opacity: .6; font-weight: 500; font-size: 12px; font-family: -apple-system, "Segoe UI", sans-serif; letter-spacing: .04em; }
.node--trigger .node__box { stroke-dasharray: 6 5; }
// 켜진 노드는 테두리가 강조색으로 두꺼워진다 (filter 대신 stroke — 렌더 비용). --i 는 <g> 에서 상속된다
.node .node__box { animation: box-lit $duration $ease-spring infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes box-lit {
0%, 100% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
12%, 32% { stroke: $color; stroke-opacity: 1; stroke-width: 4; }
50% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
}
.pulse { fill: $color; offset-distance: 0%; offset-rotate: 0deg; animation: travel $duration $ease-out infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes travel {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
45% { offset-distance: 100%; opacity: 1; }
55%, 100% { offset-distance: 100%; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse { animation: none; }
.edge--flow { stroke-dasharray: none; } // 흐름은 멈추고 연결선만 남긴다
.pulse { display: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.flow {
width: 100%;
height: 100%;
max-height: 100vh;
display: block;
overflow: visible;
}
.edge {
fill: none;
stroke: #17141a;
stroke-opacity: 0.28;
stroke-width: 3;
stroke-linecap: round;
}
.edge--flow {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-dasharray: 10 14;
animation: flow 2s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -48;
}
}
.node__box {
fill: transparent;
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
rx: 8px;
}
.node__name {
fill: #17141a;
font-weight: 700;
font-size: 17px;
font-family: -apple-system, "Segoe UI", sans-serif;
}
.node__type {
fill: #17141a;
fill-opacity: 0.6;
font-weight: 500;
font-size: 12px;
font-family: -apple-system, "Segoe UI", sans-serif;
letter-spacing: 0.04em;
}
.node--trigger .node__box {
stroke-dasharray: 6 5;
}
.node .node__box {
animation: box-lit 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: calc(var(--i) * 2s / 5);
}
@keyframes box-lit {
0%, 100% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
12%, 32% {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-width: 4;
}
50% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
}
.pulse {
fill: #2f6df6;
offset-distance: 0%;
offset-rotate: 0deg;
animation: travel 2s ease-out infinite;
animation-delay: calc(var(--i) * 2s / 5);
}
@keyframes travel {
0% {
offset-distance: 0%;
opacity: 0;
}
10% {
opacity: 1;
}
45% {
offset-distance: 100%;
opacity: 1;
}
55%, 100% {
offset-distance: 100%;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse {
animation: none;
}
.edge--flow {
stroke-dasharray: none;
}
.pulse {
display: none;
}
}02 · 주간 구간 집계 — 측정 누적면적 6.14% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
JSON에서 볼 곳
주간 참여도 리포트나 글별 반응 집계처럼, 매주 같은 지표를 확인할 때 씁니다.
{
"name": "Count Per Post",
"type": "n8n-nodes-base.summarize",
"typeVersion": 1.1,
"parameters": {
"fieldsToSummarize": {
"values": [
{ "aggregation": "count", "field": "id" }
]
},
"fieldsToSplitBy": "postId",
"options": { "outputFormat": "separateItems" }
}
}
03월간 리포트 뼈대
무엇을 자동화하나
Schedule 이 매월 1일 09시에 깨어나 HTTP 로 사용자 목록을 가져오면 Code 노드 하나가 Aggregate·Markdown 노드 없이 곧바로 문장형 리포트를 만듭니다. 노드 수가 적을수록 고칠 곳도 적습니다.

<main class="stage">
<svg class="flow" viewBox="-28 -28 654 212" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="godrich 204-03 monthly-kpi-report">
<path class="edge" d="M170 28 C299 28 299 28 428 28"/>
<path class="edge edge--flow" style="--i:0" d="M170 28 C299 28 299 28 428 28"/>
<circle class="pulse" style="--i:0;offset-path:path('M170 28 C299 28 299 28 428 28')" r="7"/>
<path class="edge" d="M384 28 C406 28 406 28 428 28"/>
<path class="edge edge--flow" style="--i:1" d="M384 28 C406 28 406 28 428 28"/>
<circle class="pulse" style="--i:1;offset-path:path('M384 28 C406 28 406 28 428 28')" r="7"/>
<path class="edge" d="M513 56 C513 78 85 78 85 100"/>
<path class="edge edge--flow" style="--i:2" d="M513 56 C513 78 85 78 85 100"/>
<circle class="pulse" style="--i:2;offset-path:path('M513 56 C513 78 85 78 85 100')" r="7"/>
<g class="node node--trigger" style="--i:0" transform="translate(0 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Schedule Trigger</text><text class="node__type" x="14" y="42">Schedule Trigger</text></g>
<g class="node node--trigger" style="--i:1" transform="translate(214 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Manual Trigger</text><text class="node__type" x="14" y="42">Manual Trigger</text></g>
<g class="node" style="--i:2" transform="translate(428 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Get Users</text><text class="node__type" x="14" y="42">Http Request</text></g>
<g class="node" style="--i:3" transform="translate(0 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Build Monthly Report</text><text class="node__type" x="14" y="42">Code</text></g>
</svg>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 한 바퀴 (데모 2초와 같은 값 — gif 가 끊기지 않게)
$easing: linear; // 대시 흐름은 등속
$color: $subject-ink; // 강조(흐름·켜진 노드) 색
$ink: $subject-ink; // 글자·테두리 (주황 무대 위 글자는 잉크 — 크림은 3.11:1 미달)
$n: 4; // 노드 수 — 켜지는 순서 간격
@include stage($stage-orange);
.flow { width: 100%; height: 100%; max-height: 100vh; display: block; overflow: visible; }
.edge { fill: none; stroke: $ink; stroke-opacity: .28; stroke-width: 3; stroke-linecap: round; }
.edge--flow { stroke: $color; stroke-opacity: 1; stroke-dasharray: 10 14; animation: flow $duration $easing infinite; }
@keyframes flow { to { stroke-dashoffset: -48; } }
.node__box { fill: transparent; stroke: $ink; stroke-opacity: .45; stroke-width: 2; rx: $r-sm; }
.node__name { fill: $ink; font-weight: 700; font-size: 17px; font-family: -apple-system, "Segoe UI", sans-serif; }
.node__type { fill: $ink; fill-opacity: .72; font-weight: 500; font-size: 12px; font-family: -apple-system, "Segoe UI", sans-serif; letter-spacing: .04em; }
.node--trigger .node__box { stroke-dasharray: 6 5; }
// 켜진 노드는 테두리가 강조색으로 두꺼워진다 (filter 대신 stroke — 렌더 비용). --i 는 <g> 에서 상속된다
.node .node__box { animation: box-lit $duration $ease-spring infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes box-lit {
0%, 100% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
12%, 32% { stroke: $color; stroke-opacity: 1; stroke-width: 4; }
50% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
}
.pulse { fill: $color; offset-distance: 0%; offset-rotate: 0deg; animation: travel $duration $ease-out infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes travel {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
45% { offset-distance: 100%; opacity: 1; }
55%, 100% { offset-distance: 100%; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse { animation: none; }
.edge--flow { stroke-dasharray: none; } // 흐름은 멈추고 연결선만 남긴다
.pulse { display: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.flow {
width: 100%;
height: 100%;
max-height: 100vh;
display: block;
overflow: visible;
}
.edge {
fill: none;
stroke: #17141a;
stroke-opacity: 0.28;
stroke-width: 3;
stroke-linecap: round;
}
.edge--flow {
stroke: #17141a;
stroke-opacity: 1;
stroke-dasharray: 10 14;
animation: flow 2s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -48;
}
}
.node__box {
fill: transparent;
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
rx: 8px;
}
.node__name {
fill: #17141a;
font-weight: 700;
font-size: 17px;
font-family: -apple-system, "Segoe UI", sans-serif;
}
.node__type {
fill: #17141a;
fill-opacity: 0.72;
font-weight: 500;
font-size: 12px;
font-family: -apple-system, "Segoe UI", sans-serif;
letter-spacing: 0.04em;
}
.node--trigger .node__box {
stroke-dasharray: 6 5;
}
.node .node__box {
animation: box-lit 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: calc(var(--i) * 2s / 4);
}
@keyframes box-lit {
0%, 100% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
12%, 32% {
stroke: #17141a;
stroke-opacity: 1;
stroke-width: 4;
}
50% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
}
.pulse {
fill: #17141a;
offset-distance: 0%;
offset-rotate: 0deg;
animation: travel 2s ease-out infinite;
animation-delay: calc(var(--i) * 2s / 4);
}
@keyframes travel {
0% {
offset-distance: 0%;
opacity: 0;
}
10% {
opacity: 1;
}
45% {
offset-distance: 100%;
opacity: 1;
}
55%, 100% {
offset-distance: 100%;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse {
animation: none;
}
.edge--flow {
stroke-dasharray: none;
}
.pulse {
display: none;
}
}03 · 월간 리포트 뼈대 — 측정 누적면적 4.968% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
JSON에서 볼 곳
월간 현황 메일이나 가입자 수 보고처럼, 매달 한 번 상태를 알려야 할 때 씁니다.
{
"name": "Build Monthly Report",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const users = $input.all().map(item => item.json);\nconst lines = users.map(u => `- ${u.name} <${u.email}>`);\nconst monthlyReport = `# Monthly Report\\n\\n${users.length} users on file this month.\\n\\n` + lines.join('\\n');\nreturn [{ json: { monthlyReport } }];"
}
}
04워크플로 시계 vs 서버 시계
무엇을 자동화하나
워크플로 설정에 시간대를 박아두면 Schedule 이 그 시간대 기준으로 도는데, Code 노드 안에서 그 시간대를 따르는 $now 와 항상 UTC 인 순수 JS Date 를 나란히 찍어보면 둘이 같은 순간을 다르게 말합니다.

<main class="stage">
<svg class="flow" viewBox="-28 -28 654 112" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="godrich 204-04 timezone-clock-compare">
<path class="edge" d="M170 28 C299 28 299 28 428 28"/>
<path class="edge edge--flow" style="--i:0" d="M170 28 C299 28 299 28 428 28"/>
<circle class="pulse" style="--i:0;offset-path:path('M170 28 C299 28 299 28 428 28')" r="7"/>
<path class="edge" d="M384 28 C406 28 406 28 428 28"/>
<path class="edge edge--flow" style="--i:1" d="M384 28 C406 28 406 28 428 28"/>
<circle class="pulse" style="--i:1;offset-path:path('M384 28 C406 28 406 28 428 28')" r="7"/>
<g class="node node--trigger" style="--i:0" transform="translate(0 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Schedule Trigger</text><text class="node__type" x="14" y="42">Schedule Trigger</text></g>
<g class="node node--trigger" style="--i:1" transform="translate(214 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Manual Trigger</text><text class="node__type" x="14" y="42">Manual Trigger</text></g>
<g class="node" style="--i:2" transform="translate(428 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Compare Clocks</text><text class="node__type" x="14" y="42">Code</text></g>
</svg>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 한 바퀴 (데모 2초와 같은 값 — gif 가 끊기지 않게)
$easing: linear; // 대시 흐름은 등속
$color: $subject-blue; // 강조(흐름·켜진 노드) 색
$ink: $subject-ink; // 글자·테두리
$n: 3; // 노드 수 — 켜지는 순서 간격
@include stage($stage-paper);
.flow { width: 100%; height: 100%; max-height: 100vh; display: block; overflow: visible; }
.edge { fill: none; stroke: $ink; stroke-opacity: .28; stroke-width: 3; stroke-linecap: round; }
.edge--flow { stroke: $color; stroke-opacity: 1; stroke-dasharray: 10 14; animation: flow $duration $easing infinite; }
@keyframes flow { to { stroke-dashoffset: -48; } }
.node__box { fill: transparent; stroke: $ink; stroke-opacity: .45; stroke-width: 2; rx: $r-sm; }
.node__name { fill: $ink; font-weight: 700; font-size: 17px; font-family: -apple-system, "Segoe UI", sans-serif; }
.node__type { fill: $ink; fill-opacity: .6; font-weight: 500; font-size: 12px; font-family: -apple-system, "Segoe UI", sans-serif; letter-spacing: .04em; }
.node--trigger .node__box { stroke-dasharray: 6 5; }
// 켜진 노드는 테두리가 강조색으로 두꺼워진다 (filter 대신 stroke — 렌더 비용). --i 는 <g> 에서 상속된다
.node .node__box { animation: box-lit $duration $ease-spring infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes box-lit {
0%, 100% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
12%, 32% { stroke: $color; stroke-opacity: 1; stroke-width: 4; }
50% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
}
.pulse { fill: $color; offset-distance: 0%; offset-rotate: 0deg; animation: travel $duration $ease-out infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes travel {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
45% { offset-distance: 100%; opacity: 1; }
55%, 100% { offset-distance: 100%; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse { animation: none; }
.edge--flow { stroke-dasharray: none; } // 흐름은 멈추고 연결선만 남긴다
.pulse { display: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.flow {
width: 100%;
height: 100%;
max-height: 100vh;
display: block;
overflow: visible;
}
.edge {
fill: none;
stroke: #17141a;
stroke-opacity: 0.28;
stroke-width: 3;
stroke-linecap: round;
}
.edge--flow {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-dasharray: 10 14;
animation: flow 2s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -48;
}
}
.node__box {
fill: transparent;
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
rx: 8px;
}
.node__name {
fill: #17141a;
font-weight: 700;
font-size: 17px;
font-family: -apple-system, "Segoe UI", sans-serif;
}
.node__type {
fill: #17141a;
fill-opacity: 0.6;
font-weight: 500;
font-size: 12px;
font-family: -apple-system, "Segoe UI", sans-serif;
letter-spacing: 0.04em;
}
.node--trigger .node__box {
stroke-dasharray: 6 5;
}
.node .node__box {
animation: box-lit 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: calc(var(--i) * 2s / 3);
}
@keyframes box-lit {
0%, 100% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
12%, 32% {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-width: 4;
}
50% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
}
.pulse {
fill: #2f6df6;
offset-distance: 0%;
offset-rotate: 0deg;
animation: travel 2s ease-out infinite;
animation-delay: calc(var(--i) * 2s / 3);
}
@keyframes travel {
0% {
offset-distance: 0%;
opacity: 0;
}
10% {
opacity: 1;
}
45% {
offset-distance: 100%;
opacity: 1;
}
55%, 100% {
offset-distance: 100%;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse {
animation: none;
}
.edge--flow {
stroke-dasharray: none;
}
.pulse {
display: none;
}
}04 · 워크플로 시계 vs 서버 시계 — 측정 누적면적 3.016% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
JSON에서 볼 곳
스케줄이 엉뚱한 시각에 도는 문제 진단이 필요할 때, 이 항목을 그대로 재현해서 원인을 확인하면 됩니다.
{
"name": "Compare Clocks",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const workflowNow = $now.toFormat('yyyy-LL-dd HH:mm:ss');\nconst workflowZone = $now.zoneName;\nconst serverNow = new Date().toISOString();\nreturn [{ json: { workflowNow, workflowZone, serverNow } }];"
}
}
05간격 설정 vs 크론 표현식
무엇을 자동화하나
Schedule 트리거 두 개를 나란히 둡니다. 하나는 Hours 필드에 6을, 다른 하나는 Custom(Cron) 필드에 0 */6 * * * 를 적어 같은 6시간 간격을 두 문법으로 표현합니다.

<main class="stage">
<svg class="flow" viewBox="-28 -28 654 212" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="godrich 204-05 interval-vs-cron-expression">
<path class="edge" d="M85 56 C85 78 85 78 85 100"/>
<path class="edge edge--flow" style="--i:0" d="M85 56 C85 78 85 78 85 100"/>
<circle class="pulse" style="--i:0;offset-path:path('M85 56 C85 78 85 78 85 100')" r="7"/>
<path class="edge" d="M299 56 C299 78 85 78 85 100"/>
<path class="edge edge--flow" style="--i:1" d="M299 56 C299 78 85 78 85 100"/>
<circle class="pulse" style="--i:1;offset-path:path('M299 56 C299 78 85 78 85 100')" r="7"/>
<path class="edge" d="M513 56 C513 78 85 78 85 100"/>
<path class="edge edge--flow" style="--i:2" d="M513 56 C513 78 85 78 85 100"/>
<circle class="pulse" style="--i:2;offset-path:path('M513 56 C513 78 85 78 85 100')" r="7"/>
<g class="node node--trigger" style="--i:0" transform="translate(0 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Every 6 Hours (Interva</text><text class="node__type" x="14" y="42">Schedule Trigger</text></g>
<g class="node node--trigger" style="--i:1" transform="translate(214 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Every 6 Hours (Cron)</text><text class="node__type" x="14" y="42">Schedule Trigger</text></g>
<g class="node node--trigger" style="--i:2" transform="translate(428 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Manual Trigger</text><text class="node__type" x="14" y="42">Manual Trigger</text></g>
<g class="node" style="--i:3" transform="translate(0 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Mark Cadence</text><text class="node__type" x="14" y="42">Set</text></g>
</svg>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 한 바퀴 (데모 2초와 같은 값 — gif 가 끊기지 않게)
$easing: linear; // 대시 흐름은 등속
$color: $subject-blue; // 강조(흐름·켜진 노드) 색
$ink: $subject-cream; // 글자·테두리
$n: 4; // 노드 수 — 켜지는 순서 간격
@include stage($stage-ink);
.flow { width: 100%; height: 100%; max-height: 100vh; display: block; overflow: visible; }
.edge { fill: none; stroke: $ink; stroke-opacity: .28; stroke-width: 3; stroke-linecap: round; }
.edge--flow { stroke: $color; stroke-opacity: 1; stroke-dasharray: 10 14; animation: flow $duration $easing infinite; }
@keyframes flow { to { stroke-dashoffset: -48; } }
.node__box { fill: transparent; stroke: $ink; stroke-opacity: .45; stroke-width: 2; rx: $r-sm; }
.node__name { fill: $ink; font-weight: 700; font-size: 17px; font-family: -apple-system, "Segoe UI", sans-serif; }
.node__type { fill: $ink; fill-opacity: .6; font-weight: 500; font-size: 12px; font-family: -apple-system, "Segoe UI", sans-serif; letter-spacing: .04em; }
.node--trigger .node__box { stroke-dasharray: 6 5; }
// 켜진 노드는 테두리가 강조색으로 두꺼워진다 (filter 대신 stroke — 렌더 비용). --i 는 <g> 에서 상속된다
.node .node__box { animation: box-lit $duration $ease-spring infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes box-lit {
0%, 100% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
12%, 32% { stroke: $color; stroke-opacity: 1; stroke-width: 4; }
50% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
}
.pulse { fill: $color; offset-distance: 0%; offset-rotate: 0deg; animation: travel $duration $ease-out infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes travel {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
45% { offset-distance: 100%; opacity: 1; }
55%, 100% { offset-distance: 100%; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse { animation: none; }
.edge--flow { stroke-dasharray: none; } // 흐름은 멈추고 연결선만 남긴다
.pulse { display: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.flow {
width: 100%;
height: 100%;
max-height: 100vh;
display: block;
overflow: visible;
}
.edge {
fill: none;
stroke: #fff7e6;
stroke-opacity: 0.28;
stroke-width: 3;
stroke-linecap: round;
}
.edge--flow {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-dasharray: 10 14;
animation: flow 2s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -48;
}
}
.node__box {
fill: transparent;
stroke: #fff7e6;
stroke-opacity: 0.45;
stroke-width: 2;
rx: 8px;
}
.node__name {
fill: #fff7e6;
font-weight: 700;
font-size: 17px;
font-family: -apple-system, "Segoe UI", sans-serif;
}
.node__type {
fill: #fff7e6;
fill-opacity: 0.6;
font-weight: 500;
font-size: 12px;
font-family: -apple-system, "Segoe UI", sans-serif;
letter-spacing: 0.04em;
}
.node--trigger .node__box {
stroke-dasharray: 6 5;
}
.node .node__box {
animation: box-lit 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: calc(var(--i) * 2s / 4);
}
@keyframes box-lit {
0%, 100% {
stroke: #fff7e6;
stroke-opacity: 0.45;
stroke-width: 2;
}
12%, 32% {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-width: 4;
}
50% {
stroke: #fff7e6;
stroke-opacity: 0.45;
stroke-width: 2;
}
}
.pulse {
fill: #2f6df6;
offset-distance: 0%;
offset-rotate: 0deg;
animation: travel 2s ease-out infinite;
animation-delay: calc(var(--i) * 2s / 4);
}
@keyframes travel {
0% {
offset-distance: 0%;
opacity: 0;
}
10% {
opacity: 1;
}
45% {
offset-distance: 100%;
opacity: 1;
}
55%, 100% {
offset-distance: 100%;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse {
animation: none;
}
.edge--flow {
stroke-dasharray: none;
}
.pulse {
display: none;
}
}05 · 간격 설정 vs 크론 표현식 — 측정 누적면적 3.878% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
JSON에서 볼 곳
간단한 주기는 UI로 충분하지만, 복잡한 조건은 크론으로 풀어야 합니다 — "매월 마지막 평일" 같은 조건이 그렇습니다.
{
"name": "Every 6 Hours (Cron)",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.4,
"parameters": {
"rule": {
"interval": [
{ "field": "cronExpression", "expression": "0 */6 * * *" }
]
}
}
}
06주말은 건너뛰는 조건 게이트
무엇을 자동화하나
Code 가 월~일 일곱 개 샘플 날짜를 만들면 IF 가 주말 여부(dayOfWeek 가 0 또는 6)로 갈라, 평일은 HTTP 로 글을 가져와 리포트를 만들고 주말은 곧바로 건너뜁니다. 두 갈래 다 Merge 뒤에서 같은 action 필드로 만납니다.

<main class="stage">
<svg class="flow" viewBox="-28 -28 654 312" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="godrich 204-06 skip-weekend-gate">
<path class="edge" d="M170 28 C299 28 299 28 428 28"/>
<path class="edge edge--flow" style="--i:0" d="M170 28 C299 28 299 28 428 28"/>
<circle class="pulse" style="--i:0;offset-path:path('M170 28 C299 28 299 28 428 28')" r="7"/>
<path class="edge" d="M384 28 C406 28 406 28 428 28"/>
<path class="edge edge--flow" style="--i:1" d="M384 28 C406 28 406 28 428 28"/>
<circle class="pulse" style="--i:1;offset-path:path('M384 28 C406 28 406 28 428 28')" r="7"/>
<path class="edge" d="M513 56 C513 78 85 78 85 100"/>
<path class="edge edge--flow" style="--i:2" d="M513 56 C513 78 85 78 85 100"/>
<circle class="pulse" style="--i:2;offset-path:path('M513 56 C513 78 85 78 85 100')" r="7"/>
<path class="edge" d="M170 128 C192 128 192 128 214 128"/>
<path class="edge edge--flow" style="--i:3" d="M170 128 C192 128 192 128 214 128"/>
<circle class="pulse" style="--i:3;offset-path:path('M170 128 C192 128 192 128 214 128')" r="7"/>
<path class="edge" d="M170 128 C299 128 299 128 428 128"/>
<path class="edge edge--flow" style="--i:3" d="M170 128 C299 128 299 128 428 128"/>
<circle class="pulse" style="--i:3;offset-path:path('M170 128 C299 128 299 128 428 128')" r="7"/>
<path class="edge" d="M299 156 C299 178 85 178 85 200"/>
<path class="edge edge--flow" style="--i:4" d="M299 156 C299 178 85 178 85 200"/>
<circle class="pulse" style="--i:4;offset-path:path('M299 156 C299 178 85 178 85 200')" r="7"/>
<path class="edge" d="M513 156 C513 178 299 178 299 200"/>
<path class="edge edge--flow" style="--i:5" d="M513 156 C513 178 299 178 299 200"/>
<circle class="pulse" style="--i:5;offset-path:path('M513 156 C513 178 299 178 299 200')" r="7"/>
<path class="edge" d="M384 228 C400 228 -16 228 0 228"/>
<path class="edge edge--flow" style="--i:7" d="M384 228 C400 228 -16 228 0 228"/>
<circle class="pulse" style="--i:7;offset-path:path('M384 228 C400 228 -16 228 0 228')" r="7"/>
<g class="node node--trigger" style="--i:0" transform="translate(0 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Schedule Trigger</text><text class="node__type" x="14" y="42">Schedule Trigger</text></g>
<g class="node node--trigger" style="--i:1" transform="translate(214 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Manual Trigger</text><text class="node__type" x="14" y="42">Manual Trigger</text></g>
<g class="node" style="--i:2" transform="translate(428 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Make Week Samples</text><text class="node__type" x="14" y="42">Code</text></g>
<g class="node" style="--i:3" transform="translate(0 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Is Weekend</text><text class="node__type" x="14" y="42">If</text></g>
<g class="node" style="--i:4" transform="translate(214 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Skip Weekend</text><text class="node__type" x="14" y="42">Set</text></g>
<g class="node" style="--i:5" transform="translate(428 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Get Posts</text><text class="node__type" x="14" y="42">Http Request</text></g>
<g class="node" style="--i:7" transform="translate(214 200)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Build Weekday Report</text><text class="node__type" x="14" y="42">Code</text></g>
<g class="node" style="--i:6" transform="translate(0 200)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Merge Branches</text><text class="node__type" x="14" y="42">Merge</text></g>
</svg>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 한 바퀴 (데모 2초와 같은 값 — gif 가 끊기지 않게)
$easing: linear; // 대시 흐름은 등속
$color: $subject-blue; // 강조(흐름·켜진 노드) 색
$ink: $subject-ink; // 글자·테두리
$n: 8; // 노드 수 — 켜지는 순서 간격
@include stage($stage-yellow);
.flow { width: 100%; height: 100%; max-height: 100vh; display: block; overflow: visible; }
.edge { fill: none; stroke: $ink; stroke-opacity: .28; stroke-width: 3; stroke-linecap: round; }
.edge--flow { stroke: $color; stroke-opacity: 1; stroke-dasharray: 10 14; animation: flow $duration $easing infinite; }
@keyframes flow { to { stroke-dashoffset: -48; } }
.node__box { fill: transparent; stroke: $ink; stroke-opacity: .45; stroke-width: 2; rx: $r-sm; }
.node__name { fill: $ink; font-weight: 700; font-size: 17px; font-family: -apple-system, "Segoe UI", sans-serif; }
.node__type { fill: $ink; fill-opacity: .6; font-weight: 500; font-size: 12px; font-family: -apple-system, "Segoe UI", sans-serif; letter-spacing: .04em; }
.node--trigger .node__box { stroke-dasharray: 6 5; }
// 켜진 노드는 테두리가 강조색으로 두꺼워진다 (filter 대신 stroke — 렌더 비용). --i 는 <g> 에서 상속된다
.node .node__box { animation: box-lit $duration $ease-spring infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes box-lit {
0%, 100% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
12%, 32% { stroke: $color; stroke-opacity: 1; stroke-width: 4; }
50% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
}
.pulse { fill: $color; offset-distance: 0%; offset-rotate: 0deg; animation: travel $duration $ease-out infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes travel {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
45% { offset-distance: 100%; opacity: 1; }
55%, 100% { offset-distance: 100%; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse { animation: none; }
.edge--flow { stroke-dasharray: none; } // 흐름은 멈추고 연결선만 남긴다
.pulse { display: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.flow {
width: 100%;
height: 100%;
max-height: 100vh;
display: block;
overflow: visible;
}
.edge {
fill: none;
stroke: #17141a;
stroke-opacity: 0.28;
stroke-width: 3;
stroke-linecap: round;
}
.edge--flow {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-dasharray: 10 14;
animation: flow 2s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -48;
}
}
.node__box {
fill: transparent;
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
rx: 8px;
}
.node__name {
fill: #17141a;
font-weight: 700;
font-size: 17px;
font-family: -apple-system, "Segoe UI", sans-serif;
}
.node__type {
fill: #17141a;
fill-opacity: 0.6;
font-weight: 500;
font-size: 12px;
font-family: -apple-system, "Segoe UI", sans-serif;
letter-spacing: 0.04em;
}
.node--trigger .node__box {
stroke-dasharray: 6 5;
}
.node .node__box {
animation: box-lit 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: calc(var(--i) * 2s / 8);
}
@keyframes box-lit {
0%, 100% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
12%, 32% {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-width: 4;
}
50% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
}
.pulse {
fill: #2f6df6;
offset-distance: 0%;
offset-rotate: 0deg;
animation: travel 2s ease-out infinite;
animation-delay: calc(var(--i) * 2s / 8);
}
@keyframes travel {
0% {
offset-distance: 0%;
opacity: 0;
}
10% {
opacity: 1;
}
45% {
offset-distance: 100%;
opacity: 1;
}
55%, 100% {
offset-distance: 100%;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse {
animation: none;
}
.edge--flow {
stroke-dasharray: none;
}
.pulse {
display: none;
}
}06 · 주말은 건너뛰는 조건 게이트 — 측정 누적면적 10.608% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
JSON에서 볼 곳
영업일에만 도는 알림이나 주말 무음 처리가 필요할 때 이 패턴을 그대로 씁니다.
{
"name": "Is Weekend",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"parameters": {
"conditions": {
"conditions": [
{ "leftValue": "={{ $json.dayOfWeek }}", "rightValue": 0, "operator": { "type": "number", "operation": "equals" } },
{ "leftValue": "={{ $json.dayOfWeek }}", "rightValue": 6, "operator": { "type": "number", "operation": "equals" } }
],
"combinator": "or"
}
}
}
07놓친 실행 따라잡기
무엇을 자동화하나
Schedule 이 15분마다 돌아야 하는데, Code 노드가 워크플로 자체에 저장되는 정적 데이터($getWorkflowStaticData)로 마지막 실행 시각을 기억해뒀다가 이번엔 몇 분 지났고 몇 번을 놓쳤는지 계산합니다. n8n이 잠깐 죽어 있던 시간을 스스로 알아채는 방법입니다.

<main class="stage">
<svg class="flow" viewBox="-28 -28 654 112" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="godrich 204-07 missed-run-catchup">
<path class="edge" d="M170 28 C299 28 299 28 428 28"/>
<path class="edge edge--flow" style="--i:0" d="M170 28 C299 28 299 28 428 28"/>
<circle class="pulse" style="--i:0;offset-path:path('M170 28 C299 28 299 28 428 28')" r="7"/>
<path class="edge" d="M384 28 C406 28 406 28 428 28"/>
<path class="edge edge--flow" style="--i:1" d="M384 28 C406 28 406 28 428 28"/>
<circle class="pulse" style="--i:1;offset-path:path('M384 28 C406 28 406 28 428 28')" r="7"/>
<g class="node node--trigger" style="--i:0" transform="translate(0 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Schedule Trigger</text><text class="node__type" x="14" y="42">Schedule Trigger</text></g>
<g class="node node--trigger" style="--i:1" transform="translate(214 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Manual Trigger</text><text class="node__type" x="14" y="42">Manual Trigger</text></g>
<g class="node" style="--i:2" transform="translate(428 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Detect Missed Runs</text><text class="node__type" x="14" y="42">Code</text></g>
</svg>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 한 바퀴 (데모 2초와 같은 값 — gif 가 끊기지 않게)
$easing: linear; // 대시 흐름은 등속
$color: $subject-ink; // 강조(흐름·켜진 노드) 색
$ink: $subject-ink; // 글자·테두리 (주황 무대 위 글자는 잉크 — 크림은 3.11:1 미달)
$n: 3; // 노드 수 — 켜지는 순서 간격
@include stage($stage-orange);
.flow { width: 100%; height: 100%; max-height: 100vh; display: block; overflow: visible; }
.edge { fill: none; stroke: $ink; stroke-opacity: .28; stroke-width: 3; stroke-linecap: round; }
.edge--flow { stroke: $color; stroke-opacity: 1; stroke-dasharray: 10 14; animation: flow $duration $easing infinite; }
@keyframes flow { to { stroke-dashoffset: -48; } }
.node__box { fill: transparent; stroke: $ink; stroke-opacity: .45; stroke-width: 2; rx: $r-sm; }
.node__name { fill: $ink; font-weight: 700; font-size: 17px; font-family: -apple-system, "Segoe UI", sans-serif; }
.node__type { fill: $ink; fill-opacity: .72; font-weight: 500; font-size: 12px; font-family: -apple-system, "Segoe UI", sans-serif; letter-spacing: .04em; }
.node--trigger .node__box { stroke-dasharray: 6 5; }
// 켜진 노드는 테두리가 강조색으로 두꺼워진다 (filter 대신 stroke — 렌더 비용). --i 는 <g> 에서 상속된다
.node .node__box { animation: box-lit $duration $ease-spring infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes box-lit {
0%, 100% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
12%, 32% { stroke: $color; stroke-opacity: 1; stroke-width: 4; }
50% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
}
.pulse { fill: $color; offset-distance: 0%; offset-rotate: 0deg; animation: travel $duration $ease-out infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes travel {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
45% { offset-distance: 100%; opacity: 1; }
55%, 100% { offset-distance: 100%; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse { animation: none; }
.edge--flow { stroke-dasharray: none; } // 흐름은 멈추고 연결선만 남긴다
.pulse { display: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.flow {
width: 100%;
height: 100%;
max-height: 100vh;
display: block;
overflow: visible;
}
.edge {
fill: none;
stroke: #17141a;
stroke-opacity: 0.28;
stroke-width: 3;
stroke-linecap: round;
}
.edge--flow {
stroke: #17141a;
stroke-opacity: 1;
stroke-dasharray: 10 14;
animation: flow 2s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -48;
}
}
.node__box {
fill: transparent;
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
rx: 8px;
}
.node__name {
fill: #17141a;
font-weight: 700;
font-size: 17px;
font-family: -apple-system, "Segoe UI", sans-serif;
}
.node__type {
fill: #17141a;
fill-opacity: 0.72;
font-weight: 500;
font-size: 12px;
font-family: -apple-system, "Segoe UI", sans-serif;
letter-spacing: 0.04em;
}
.node--trigger .node__box {
stroke-dasharray: 6 5;
}
.node .node__box {
animation: box-lit 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: calc(var(--i) * 2s / 3);
}
@keyframes box-lit {
0%, 100% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
12%, 32% {
stroke: #17141a;
stroke-opacity: 1;
stroke-width: 4;
}
50% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
}
.pulse {
fill: #17141a;
offset-distance: 0%;
offset-rotate: 0deg;
animation: travel 2s ease-out infinite;
animation-delay: calc(var(--i) * 2s / 3);
}
@keyframes travel {
0% {
offset-distance: 0%;
opacity: 0;
}
10% {
opacity: 1;
}
45% {
offset-distance: 100%;
opacity: 1;
}
55%, 100% {
offset-distance: 100%;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse {
animation: none;
}
.edge--flow {
stroke-dasharray: none;
}
.pulse {
display: none;
}
}07 · 놓친 실행 따라잡기 — 측정 누적면적 2.97% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
JSON에서 볼 곳
서버 재시작 뒤 누락 감지나 실행 이력 없이 간격 재기가 필요할 때 이 패턴을 씁니다.
{
"name": "Detect Missed Runs",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"parameters": {
"jsCode": "const staticData = $getWorkflowStaticData('node');\nconst expectedIntervalMinutes = 15;\nconst now = Date.now();\nconst lastRun = staticData.lastRun || (now - expectedIntervalMinutes * 3 * 60000);\nconst elapsedMinutes = Math.round((now - lastRun) / 60000);\nconst missedRuns = Math.max(0, Math.floor(elapsedMinutes / expectedIntervalMinutes) - 1);\nstaticData.lastRun = now;\nreturn [{ json: { elapsedMinutes, missedRuns } }];"
}
}
08다이제스트 이메일 본문
무엇을 자동화하나
Schedule 이 3시간마다 RSS 로 최신 글을 읽으면 Sort 가 최신순으로 세우고 Limit 이 5개로 줄인 뒤 Code 가 제목·인사말을 마크다운으로 엮고, Markdown 노드가 그걸 그대로 보낼 수 있는 HTML 로 바꿉니다.

<main class="stage">
<svg class="flow" viewBox="-28 -28 654 312" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="godrich 204-08 digest-email-body">
<path class="edge" d="M170 28 C299 28 299 28 428 28"/>
<path class="edge edge--flow" style="--i:0" d="M170 28 C299 28 299 28 428 28"/>
<circle class="pulse" style="--i:0;offset-path:path('M170 28 C299 28 299 28 428 28')" r="7"/>
<path class="edge" d="M384 28 C406 28 406 28 428 28"/>
<path class="edge edge--flow" style="--i:1" d="M384 28 C406 28 406 28 428 28"/>
<circle class="pulse" style="--i:1;offset-path:path('M384 28 C406 28 406 28 428 28')" r="7"/>
<path class="edge" d="M513 56 C513 78 85 78 85 100"/>
<path class="edge edge--flow" style="--i:2" d="M513 56 C513 78 85 78 85 100"/>
<circle class="pulse" style="--i:2;offset-path:path('M513 56 C513 78 85 78 85 100')" r="7"/>
<path class="edge" d="M170 128 C192 128 192 128 214 128"/>
<path class="edge edge--flow" style="--i:3" d="M170 128 C192 128 192 128 214 128"/>
<circle class="pulse" style="--i:3;offset-path:path('M170 128 C192 128 192 128 214 128')" r="7"/>
<path class="edge" d="M384 128 C406 128 406 128 428 128"/>
<path class="edge edge--flow" style="--i:4" d="M384 128 C406 128 406 128 428 128"/>
<circle class="pulse" style="--i:4;offset-path:path('M384 128 C406 128 406 128 428 128')" r="7"/>
<path class="edge" d="M513 156 C513 178 85 178 85 200"/>
<path class="edge edge--flow" style="--i:5" d="M513 156 C513 178 85 178 85 200"/>
<circle class="pulse" style="--i:5;offset-path:path('M513 156 C513 178 85 178 85 200')" r="7"/>
<g class="node node--trigger" style="--i:0" transform="translate(0 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Schedule Trigger</text><text class="node__type" x="14" y="42">Schedule Trigger</text></g>
<g class="node node--trigger" style="--i:1" transform="translate(214 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Manual Trigger</text><text class="node__type" x="14" y="42">Manual Trigger</text></g>
<g class="node" style="--i:2" transform="translate(428 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">RSS Read</text><text class="node__type" x="14" y="42">Rss Feed Read</text></g>
<g class="node" style="--i:3" transform="translate(0 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Sort By Newest</text><text class="node__type" x="14" y="42">Sort</text></g>
<g class="node" style="--i:4" transform="translate(214 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Limit</text><text class="node__type" x="14" y="42">Limit</text></g>
<g class="node" style="--i:5" transform="translate(428 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Build Email Markdown</text><text class="node__type" x="14" y="42">Code</text></g>
<g class="node" style="--i:6" transform="translate(0 200)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Markdown to HTML</text><text class="node__type" x="14" y="42">Markdown</text></g>
</svg>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 한 바퀴 (데모 2초와 같은 값 — gif 가 끊기지 않게)
$easing: linear; // 대시 흐름은 등속
$color: $subject-blue; // 강조(흐름·켜진 노드) 색
$ink: $subject-ink; // 글자·테두리
$n: 7; // 노드 수 — 켜지는 순서 간격
@include stage($stage-paper);
.flow { width: 100%; height: 100%; max-height: 100vh; display: block; overflow: visible; }
.edge { fill: none; stroke: $ink; stroke-opacity: .28; stroke-width: 3; stroke-linecap: round; }
.edge--flow { stroke: $color; stroke-opacity: 1; stroke-dasharray: 10 14; animation: flow $duration $easing infinite; }
@keyframes flow { to { stroke-dashoffset: -48; } }
.node__box { fill: transparent; stroke: $ink; stroke-opacity: .45; stroke-width: 2; rx: $r-sm; }
.node__name { fill: $ink; font-weight: 700; font-size: 17px; font-family: -apple-system, "Segoe UI", sans-serif; }
.node__type { fill: $ink; fill-opacity: .6; font-weight: 500; font-size: 12px; font-family: -apple-system, "Segoe UI", sans-serif; letter-spacing: .04em; }
.node--trigger .node__box { stroke-dasharray: 6 5; }
// 켜진 노드는 테두리가 강조색으로 두꺼워진다 (filter 대신 stroke — 렌더 비용). --i 는 <g> 에서 상속된다
.node .node__box { animation: box-lit $duration $ease-spring infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes box-lit {
0%, 100% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
12%, 32% { stroke: $color; stroke-opacity: 1; stroke-width: 4; }
50% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
}
.pulse { fill: $color; offset-distance: 0%; offset-rotate: 0deg; animation: travel $duration $ease-out infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes travel {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
45% { offset-distance: 100%; opacity: 1; }
55%, 100% { offset-distance: 100%; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse { animation: none; }
.edge--flow { stroke-dasharray: none; } // 흐름은 멈추고 연결선만 남긴다
.pulse { display: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.flow {
width: 100%;
height: 100%;
max-height: 100vh;
display: block;
overflow: visible;
}
.edge {
fill: none;
stroke: #17141a;
stroke-opacity: 0.28;
stroke-width: 3;
stroke-linecap: round;
}
.edge--flow {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-dasharray: 10 14;
animation: flow 2s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -48;
}
}
.node__box {
fill: transparent;
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
rx: 8px;
}
.node__name {
fill: #17141a;
font-weight: 700;
font-size: 17px;
font-family: -apple-system, "Segoe UI", sans-serif;
}
.node__type {
fill: #17141a;
fill-opacity: 0.6;
font-weight: 500;
font-size: 12px;
font-family: -apple-system, "Segoe UI", sans-serif;
letter-spacing: 0.04em;
}
.node--trigger .node__box {
stroke-dasharray: 6 5;
}
.node .node__box {
animation: box-lit 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: calc(var(--i) * 2s / 7);
}
@keyframes box-lit {
0%, 100% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
12%, 32% {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-width: 4;
}
50% {
stroke: #17141a;
stroke-opacity: 0.45;
stroke-width: 2;
}
}
.pulse {
fill: #2f6df6;
offset-distance: 0%;
offset-rotate: 0deg;
animation: travel 2s ease-out infinite;
animation-delay: calc(var(--i) * 2s / 7);
}
@keyframes travel {
0% {
offset-distance: 0%;
opacity: 0;
}
10% {
opacity: 1;
}
45% {
offset-distance: 100%;
opacity: 1;
}
55%, 100% {
offset-distance: 100%;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse {
animation: none;
}
.edge--flow {
stroke-dasharray: none;
}
.pulse {
display: none;
}
}08 · 다이제스트 이메일 본문 — 측정 누적면적 8.606% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
JSON에서 볼 곳
정기 뉴스레터나 슬랙·이메일 알림 본문을 만들 때 이 흐름을 그대로 씁니다.
{
"name": "Markdown to HTML",
"type": "n8n-nodes-base.markdown",
"typeVersion": 1,
"parameters": {
"mode": "markdownToHtml",
"markdown": "={{ $json.markdown }}",
"destinationKey": "emailHtml",
"options": {}
}
}
09받기·바꾸기·꾸미기 3단 파이프라인
무엇을 자동화하나
Schedule 이 매일 06시에 키 없는 날씨 API 로 3일치 예보를 받으면, Code 가 최고·최저 기온 평균을 계산하고, Markdown 노드가 문장형 리포트로 다듬습니다. 이 세 단계가 대부분의 리포트 워크플로의 기본형입니다.

<main class="stage">
<svg class="flow" viewBox="-28 -28 654 212" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="godrich 204-09 multi-step-weather-pipeline">
<path class="edge" d="M170 28 C299 28 299 28 428 28"/>
<path class="edge edge--flow" style="--i:0" d="M170 28 C299 28 299 28 428 28"/>
<circle class="pulse" style="--i:0;offset-path:path('M170 28 C299 28 299 28 428 28')" r="7"/>
<path class="edge" d="M384 28 C406 28 406 28 428 28"/>
<path class="edge edge--flow" style="--i:1" d="M384 28 C406 28 406 28 428 28"/>
<circle class="pulse" style="--i:1;offset-path:path('M384 28 C406 28 406 28 428 28')" r="7"/>
<path class="edge" d="M513 56 C513 78 85 78 85 100"/>
<path class="edge edge--flow" style="--i:2" d="M513 56 C513 78 85 78 85 100"/>
<circle class="pulse" style="--i:2;offset-path:path('M513 56 C513 78 85 78 85 100')" r="7"/>
<path class="edge" d="M170 128 C192 128 192 128 214 128"/>
<path class="edge edge--flow" style="--i:3" d="M170 128 C192 128 192 128 214 128"/>
<circle class="pulse" style="--i:3;offset-path:path('M170 128 C192 128 192 128 214 128')" r="7"/>
<g class="node node--trigger" style="--i:0" transform="translate(0 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Schedule Trigger</text><text class="node__type" x="14" y="42">Schedule Trigger</text></g>
<g class="node node--trigger" style="--i:1" transform="translate(214 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Manual Trigger</text><text class="node__type" x="14" y="42">Manual Trigger</text></g>
<g class="node" style="--i:2" transform="translate(428 0)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Get 3-Day Forecast</text><text class="node__type" x="14" y="42">Http Request</text></g>
<g class="node" style="--i:3" transform="translate(0 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Compute Stats</text><text class="node__type" x="14" y="42">Code</text></g>
<g class="node" style="--i:4" transform="translate(214 100)"><rect class="node__box" width="170" height="56"/><text class="node__name" x="14" y="24">Build Report</text><text class="node__type" x="14" y="42">Markdown</text></g>
</svg>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 한 바퀴 (데모 2초와 같은 값 — gif 가 끊기지 않게)
$easing: linear; // 대시 흐름은 등속
$color: $subject-blue; // 강조(흐름·켜진 노드) 색
$ink: $subject-cream; // 글자·테두리
$n: 5; // 노드 수 — 켜지는 순서 간격
@include stage($stage-ink);
.flow { width: 100%; height: 100%; max-height: 100vh; display: block; overflow: visible; }
.edge { fill: none; stroke: $ink; stroke-opacity: .28; stroke-width: 3; stroke-linecap: round; }
.edge--flow { stroke: $color; stroke-opacity: 1; stroke-dasharray: 10 14; animation: flow $duration $easing infinite; }
@keyframes flow { to { stroke-dashoffset: -48; } }
.node__box { fill: transparent; stroke: $ink; stroke-opacity: .45; stroke-width: 2; rx: $r-sm; }
.node__name { fill: $ink; font-weight: 700; font-size: 17px; font-family: -apple-system, "Segoe UI", sans-serif; }
.node__type { fill: $ink; fill-opacity: .6; font-weight: 500; font-size: 12px; font-family: -apple-system, "Segoe UI", sans-serif; letter-spacing: .04em; }
.node--trigger .node__box { stroke-dasharray: 6 5; }
// 켜진 노드는 테두리가 강조색으로 두꺼워진다 (filter 대신 stroke — 렌더 비용). --i 는 <g> 에서 상속된다
.node .node__box { animation: box-lit $duration $ease-spring infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes box-lit {
0%, 100% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
12%, 32% { stroke: $color; stroke-opacity: 1; stroke-width: 4; }
50% { stroke: $ink; stroke-opacity: .45; stroke-width: 2; }
}
.pulse { fill: $color; offset-distance: 0%; offset-rotate: 0deg; animation: travel $duration $ease-out infinite; animation-delay: calc(var(--i) * #{$duration} / #{$n}); }
@keyframes travel {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
45% { offset-distance: 100%; opacity: 1; }
55%, 100% { offset-distance: 100%; opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse { animation: none; }
.edge--flow { stroke-dasharray: none; } // 흐름은 멈추고 연결선만 남긴다
.pulse { display: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.flow {
width: 100%;
height: 100%;
max-height: 100vh;
display: block;
overflow: visible;
}
.edge {
fill: none;
stroke: #fff7e6;
stroke-opacity: 0.28;
stroke-width: 3;
stroke-linecap: round;
}
.edge--flow {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-dasharray: 10 14;
animation: flow 2s linear infinite;
}
@keyframes flow {
to {
stroke-dashoffset: -48;
}
}
.node__box {
fill: transparent;
stroke: #fff7e6;
stroke-opacity: 0.45;
stroke-width: 2;
rx: 8px;
}
.node__name {
fill: #fff7e6;
font-weight: 700;
font-size: 17px;
font-family: -apple-system, "Segoe UI", sans-serif;
}
.node__type {
fill: #fff7e6;
fill-opacity: 0.6;
font-weight: 500;
font-size: 12px;
font-family: -apple-system, "Segoe UI", sans-serif;
letter-spacing: 0.04em;
}
.node--trigger .node__box {
stroke-dasharray: 6 5;
}
.node .node__box {
animation: box-lit 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
animation-delay: calc(var(--i) * 2s / 5);
}
@keyframes box-lit {
0%, 100% {
stroke: #fff7e6;
stroke-opacity: 0.45;
stroke-width: 2;
}
12%, 32% {
stroke: #2f6df6;
stroke-opacity: 1;
stroke-width: 4;
}
50% {
stroke: #fff7e6;
stroke-opacity: 0.45;
stroke-width: 2;
}
}
.pulse {
fill: #2f6df6;
offset-distance: 0%;
offset-rotate: 0deg;
animation: travel 2s ease-out infinite;
animation-delay: calc(var(--i) * 2s / 5);
}
@keyframes travel {
0% {
offset-distance: 0%;
opacity: 0;
}
10% {
opacity: 1;
}
45% {
offset-distance: 100%;
opacity: 1;
}
55%, 100% {
offset-distance: 100%;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.edge--flow, .node, .node .node__box, .pulse {
animation: none;
}
.edge--flow {
stroke-dasharray: none;
}
.pulse {
display: none;
}
}09 · 받기·바꾸기·꾸미기 3단 파이프라인 — 측정 누적면적 6.193% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기
JSON에서 볼 곳
일간 날씨 브리핑이나 범용 리포트 파이프라인 뼈대가 필요할 때 이 구조를 그대로 씁니다.
{
"name": "Build Report",
"type": "n8n-nodes-base.markdown",
"typeVersion": 1,
"parameters": {
"mode": "markdownToHtml",
"markdown": "={{ '# Weather Report — ' + $json.reportDate + '\\n\\n3-day average high: ' + $json.avgHigh + '°C\\n3-day average low: ' + $json.avgLow + '°C' }}",
"destinationKey": "report",
"options": {}
}
}
준비물 — 계정·권한·버전
아홉 개 다 준비물 행이 "없음"이고 등급도 전부 V2 로 같지만, 항목마다 실제로 검증했다는 사실을 그대로 보여주기 위해 표로 남겨둡니다. 등급은 V0(구조)·V1(가져오기 확인)·V2(실행까지 확인) 세 단계로 재는데, 이 편은 9개 전부 V2 입니다.
| 항목 | 등급 | 준비물 | n8n 버전 |
|---|---|---|---|
| 01 매일 할 일 요약 | V2 | 없음 | 2.38.5 |
| 02 주간 구간 집계 | V2 | 없음 | 2.38.5 |
| 03 월간 리포트 뼈대 | V2 | 없음 | 2.38.5 |
| 04 워크플로 시계 vs 서버 시계 | V2 | 없음 | 2.38.5 |
| 05 간격 설정 vs 크론 표현식 | V2 | 없음 | 2.38.5 |
| 06 주말은 건너뛰는 조건 게이트 | V2 | 없음 | 2.38.5 |
| 07 놓친 실행 따라잡기 | V2 | 없음 | 2.38.5 |
| 08 다이제스트 이메일 본문 | V2 | 없음 | 2.38.5 |
| 09 받기·바꾸기·꾸미기 3단 파이프라인 | V2 | 없음 | 2.38.5 |
이 파일을 n8n에 넣는 방법은 셋 중 하나를 고르면 됩니다. 캔버스에 Ctrl/Cmd+V 로 JSON 텍스트를 붙여넣거나, 캔버스 메뉴의 Import from File 로 zip 속 파일을 고르거나, CLI를 쓴다면 n8n import:workflow --input=파일.json 한 줄이면 됩니다. 그다음 캔버스 왼쪽 위 Test workflow 버튼(또는 Manual Trigger 노드의 재생 아이콘)을 누르면 바로 돌아갑니다.
어디서 깨지는가 — 함정 하나
리포트 자동화, 어디서 깨지나요?
가장 많이 헷갈리는 지점은 04 항목의 시간대입니다 — 워크플로 설정에 시간대를 Asia/Seoul 로 박아두면 Schedule 트리거의 발동 시각과 $now 표현식은 그 시간대를 따르지만, Code 노드 안에서 아무 생각 없이 new Date() 를 쓰면 그 값은 서버의 시스템 시간대(대개 UTC)를 그대로 따라갑니다. 둘을 섞어 쓰면 "분명 9시에 도는데 리포트엔 자정으로 찍힌다" 같은 버그가 납니다. 압축을 풀면 workflows/ 폴더 안에 아홉 개 JSON 파일이 바로 들어 있고, 여는 암호는 문장 속에 심어둔 kenrd2uf 입니다. 02 항목의 Summarize 노드도 비슷한 함정이 있습니다 — 출력 필드 이름이 직접 지은 게 아니라 count_id 처럼 집계 방식과 필드명이 자동으로 합쳐진 이름이라, 짐작해서 count 라고만 쓰면 다음 노드가 그 필드를 못 찾습니다. 눌렀을 때 상태가 바뀌는지 보여 주는 화면의 대응물은 버튼 클릭 효과 9종인데, 자동화의 '돌고 있다' 신호를 버튼의 로딩·완료 표시로 옮겨 온 셈입니다.
FAQ
n8n을 처음 써보는데 계정부터 만들어야 하나요?
아니요. 이 zip 의 워크플로 아홉 개는 전부 커뮤니티 노드나 계정이 필요한 자격증명을 쓰지 않아서, 로컬 환경이나 무료로 켤 수 있는 n8n 인스턴스에 가져오기(Import)만 하면 Manual Trigger 로 바로 돌려볼 수 있습니다.
내 n8n 버전이 다르면 어떻게 하나요?
가져오다가 typeVersion 이 안 맞는다는 오류를 보면, 같은 종류 노드를 캔버스에 직접 하나 놓아보고 export 된 값이 얼마인지 눈으로 확인한 다음 그 값으로 고쳐 쓰면 해결됩니다. n8n 2.38.5(Node 24) 기준으로는 이 zip 의 워크플로 아홉 개가 가져오기·실행 둘 다 그대로 통과했습니다. 가져오기부터 실행까지의 검증 방식이 궁금하면 n8n 워크플로우 예제 9종의 준비물 절이 같은 절차를 먼저 보여 줍니다.
n8n 자동화 워크플로우를 실제 업무(엑셀·구글시트·슬랙)로 넓히려면?
HTTP Request 노드나 마지막 Code 노드가 만든 리포트를 원하는 목적지 노드(Google Sheets·Slack·이메일)로 이어붙이면 됩니다. 키가 필요한 노드는 Authentication 옵션에서 자격증명을 따로 추가하고, 워크플로 JSON 안에 직접 값을 적어 공유하지 않는 게 n8n 공식 문서가 권하는 방식입니다.
n8n을 조금 더 익히고 싶다면 자동화 기둥 글 모음에서 이어지는 예제를 볼 수 있고, 이 사이트가 어떤 기준으로 검증하는지는 소개 페이지에 적어 두었습니다.









