12. 실전 레시피¶
복사해서 바로 쓸 수 있는 완성 예제입니다. 모든 예제는 실제 이음새 검증기를 통과한 YAML입니다. 워크플로우 상세의 YAML 탭에 붙여넣고 저장하면 됩니다.
붙여넣은 뒤 아래 두 가지는 본인 것으로 바꿔야 합니다.
id— 워크스페이스 안에서 고유해야 합니다${vars.*}/${secrets.*}— Variables 화면에 미리 등록해두세요
1. 매일 아침 매출 리포트¶
매일 오전 9시 → 어제 주문 조회 → 집계 → AI 요약 → Slack 발송
가장 대표적인 형태입니다. 스케줄, 시간 윈도우, 재시도, 데이터 가공, AI, 발송이 전부 들어 있습니다.
준비물¶
| 종류 | 키 | 값 |
|---|---|---|
| Variable | SHOP_API_BASE |
https://api.myshop.com |
| Variable | REPORT_CHANNEL_ID |
Slack 채널 ID |
| Secret | SHOP_API_TOKEN |
쇼핑몰 API 토큰 |
| Secret | OPENAI_API_KEY |
OpenAI API 키 |
| 연결 | Slack | 연결하고 채널에 봇 초대 |
id: daily-revenue-report
name: 매일 아침 매출 리포트
nodes:
- id: trigger
name: 매일 오전 9시
type: ENTRYPOINT
trigger:
kind: SCHEDULER
cron: "0 0 9 * * ?"
timezone: "Asia/Seoul"
lookback: PT24H
- id: fetch-orders
name: 어제 주문 조회
type: CALL
integration: http_request
timeout: 30s
retry-policy:
max-attempts: 3
backoff:
type: EXPONENTIAL
initial-delay: 500ms
retry-on: ["429", "5xx"]
input:
uri: "${vars.SHOP_API_BASE}/v1/orders"
method: GET
queryParams:
from: "${nodes.trigger.response.body.windowStart}"
to: "${nodes.trigger.response.body.windowEnd}"
status: "PAID"
authenticate:
authMethod: HEADERS
data:
Authorization: "Bearer ${secrets.SHOP_API_TOKEN}"
- id: aggregate
name: 매출 집계
type: CALL
integration: transform_jmespath
input:
expression: "{건수: length(@), 매출합계: sum([*].amount), 최고금액: max([*].amount)}"
data: "${nodes.fetch-orders.response.body.data | raw}"
- id: summarize
name: AI 요약
type: CALL
integration: llm_chat
timeout: 60s
input:
apiContract: OPENAI_CHAT
model: gpt-4o-mini
apiKey: "${secrets.OPENAI_API_KEY}"
systemPrompt: "너는 이커머스 운영 담당자다. 숫자를 과장하지 말고 사실만 간결하게 전달한다."
userPrompt: |
아래 어제 매출 데이터를 3줄로 요약해줘.
주문 건수: ${nodes.aggregate.response.body.건수}
매출 합계: ${nodes.aggregate.response.body.매출합계}
최고 주문 금액: ${nodes.aggregate.response.body.최고금액}
- id: notify
name: Slack 발송
type: CALL
integration: slack_post_message
input:
channel: "${vars.REPORT_CHANNEL_ID}"
text: |
📊 어제 매출 리포트
${nodes.summarize.response.body.content}
edges:
- from: trigger
to: fetch-orders
- from: fetch-orders
to: aggregate
- from: aggregate
to: summarize
- from: summarize
to: notify
눈여겨볼 곳¶
lookback: PT24H—windowStart/windowEnd가 자동으로 생겨 "어제치"를 조회할 수 있습니다.data: "${... | raw}"—| raw가 없으면 배열이 문자열이 되어 집계가 깨집니다.retry-on: ["429", "5xx"]— 조회는 다시 해도 안전하니 재시도를 겁니다.notify노드에는 재시도를 걸지 않았습니다 — 메시지가 두 번 갈 수 있기 때문입니다.
응용¶
| 하고 싶은 것 | 바꿀 곳 |
|---|---|
| 주간 리포트로 | cron: "0 0 9 ? * MON", lookback: P7D |
| Slack 대신 다른 곳으로 | notify를 http_request로 교체 |
| 요약 말투 바꾸기 | systemPrompt 수정 |
2. 주문 웹훅 → 금액별 알림¶
주문 웹훅 수신 → 금액으로 분기 → 각각 다른 채널로 알림 → 합류 → 이력 저장
조건 분기(CONDITIONAL)와 합류(JOINT)를 함께 쓰는 예제입니다.
준비물¶
| 종류 | 키 |
|---|---|
| Variable | VIP_CHANNEL_ID, ORDER_CHANNEL_ID |
| 데이터셋 | 주문처리이력 (컬럼: 주문번호 STRING, 금액 NUMBER) |
| 연결 | Slack |
id: order-alert
name: 주문 웹훅 알림
nodes:
- id: trigger
name: 주문 웹훅
type: ENTRYPOINT
trigger:
kind: WEBHOOK
input-schema:
type: object
required: [orderId, customerName, amount]
properties:
orderId: { type: string }
customerName: { type: string }
amount: { type: number }
- id: route
name: 금액별 분기
type: CONDITIONAL
execution-info:
conditions:
- label: vip
expression: "amount >= 100000"
- label: normal
otherwise: true
- id: notify-vip
name: VIP 알림
type: CALL
integration: slack_post_message
input:
channel: "${vars.VIP_CHANNEL_ID}"
text: "🔥 고액 주문! ${nodes.trigger.response.body.customerName}님 / ${nodes.trigger.response.body.amount}원 (주문번호 ${nodes.trigger.response.body.orderId})"
- id: notify-normal
name: 일반 알림
type: CALL
integration: slack_post_message
input:
channel: "${vars.ORDER_CHANNEL_ID}"
text: "🛒 새 주문: ${nodes.trigger.response.body.customerName}님 / ${nodes.trigger.response.body.amount}원"
- id: join
name: 합류
type: JOINT
- id: log
name: 처리이력 저장
type: CALL
integration: dataset
input:
datasetTitle: "주문처리이력"
operation: INSERT
data:
주문번호: "${nodes.trigger.response.body.orderId}"
금액: "${nodes.trigger.response.body.amount}"
edges:
- from: trigger
to: route
request:
data:
amount: "${nodes.trigger.response.body.amount}"
- from: route
to: notify-vip
label: vip
- from: route
to: notify-normal
label: normal
- from: notify-vip
to: join
- from: notify-normal
to: join
- from: join
to: log
눈여겨볼 곳¶
trigger → route엣지의request.data— 이게 없으면 조건식이amount를 못 봅니다. 가장 흔한 실수입니다.- 조건식은
amount >= 100000—${}없이 이름만 씁니다. - 두 갈래가
join에서 만나고, 그 뒤log는 어느 쪽으로 갔든 한 번만 실행됩니다. log에서는join대신trigger를 직접 참조합니다. JOINT는 출력이 없기 때문입니다.
테스트¶
curl -X POST "https://api.eeumsae.com/webhooks/<워크스페이스ID>/order-alert" \
-H "Content-Type: application/json" \
-d '{"orderId":"ORD-1234","customerName":"김보찬","amount":150000}'
3. 고객 문의 자동 분류¶
문의 웹훅 수신 → AI가 분류 → 담당 채널로 라우팅
AI 응답으로 분기하는 예제입니다. contains 연산자를 씁니다.
준비물¶
| 종류 | 키 |
|---|---|
| Variable | REFUND_CHANNEL_ID, DELIVERY_CHANNEL_ID, SUPPORT_CHANNEL_ID |
| Secret | OPENAI_API_KEY |
| 연결 | Slack |
id: inquiry-router
name: 고객 문의 자동 분류
nodes:
- id: trigger
name: 문의 웹훅
type: ENTRYPOINT
trigger:
kind: WEBHOOK
input-schema:
type: object
required: [message]
properties:
message: { type: string }
email: { type: string }
- id: classify
name: 문의 분류
type: CALL
integration: llm_chat
timeout: 30s
input:
apiContract: OPENAI_CHAT
model: gpt-4o-mini
apiKey: "${secrets.OPENAI_API_KEY}"
systemPrompt: "고객 문의를 딱 한 단어로 분류해라: 환불 / 배송 / 상품문의 / 기타. 다른 말은 절대 하지 마라."
userPrompt: "${nodes.trigger.response.body.message}"
- id: route
name: 분류별 분기
type: CONDITIONAL
execution-info:
conditions:
- label: refund
expression: "category contains '환불'"
- label: delivery
expression: "category contains '배송'"
- label: other
otherwise: true
- id: to-refund
name: 환불팀 알림
type: CALL
integration: slack_post_message
input:
channel: "${vars.REFUND_CHANNEL_ID}"
text: "💸 환불 문의\n${nodes.trigger.response.body.message}"
- id: to-delivery
name: 배송팀 알림
type: CALL
integration: slack_post_message
input:
channel: "${vars.DELIVERY_CHANNEL_ID}"
text: "📦 배송 문의\n${nodes.trigger.response.body.message}"
- id: to-general
name: 일반 문의 알림
type: CALL
integration: slack_post_message
input:
channel: "${vars.SUPPORT_CHANNEL_ID}"
text: "💬 문의 (${nodes.classify.response.body.content})\n${nodes.trigger.response.body.message}"
edges:
- from: trigger
to: classify
- from: classify
to: route
request:
data:
category: "${nodes.classify.response.body.content}"
- from: route
to: to-refund
label: refund
- from: route
to: to-delivery
label: delivery
- from: route
to: to-general
label: other
눈여겨볼 곳¶
systemPrompt에서 "딱 한 단어로", "다른 말은 절대 하지 마라" 로 못 박습니다. AI 출력이 들쭉날쭉하면 분기가 흔들립니다.==대신contains를 씁니다. AI가 "환불 문의입니다"처럼 답해도 잡히도록.otherwise: true가지는 반드시 두세요. 예상 못 한 답이 와도 문의가 사라지지 않습니다.
4. 항목별 반복 처리¶
매시간 → 대기 항목 조회 → 처리 대상만 추리기 → 항목마다 처리 → 완료 알림
루프(LOOP_START / LOOP_END)를 쓰는 예제입니다.
id: per-item-processing
name: 항목별 반복 처리
nodes:
- id: trigger
name: 매시간 실행
type: ENTRYPOINT
trigger:
kind: SCHEDULER
cron: "0 0 * * * ?"
timezone: "Asia/Seoul"
- id: fetch
name: 처리 대상 조회
type: CALL
integration: http_request
timeout: 30s
input:
uri: "${vars.API_BASE_URL}/pending-items"
method: GET
- id: filter
name: 대상만 추리기
type: CALL
integration: transform_jmespath
input:
expression: "{targets: items[?status == `pending`]}"
data: "${nodes.fetch.response.body | raw}"
- id: each
name: 항목별 반복 시작
type: LOOP_START
execution-info:
items: "${nodes.filter.response.body.targets | raw}"
- id: process
name: 항목 처리
type: CALL
integration: http_request
timeout: 10s
retry-policy:
max-attempts: 2
backoff:
type: FIXED
initial-delay: 1s
retry-on: ["5xx"]
input:
uri: "${vars.API_BASE_URL}/items/${item.id}/process"
method: POST
body:
index: "${index}"
- id: collect
name: 반복 집계
type: LOOP_END
execution-info:
loop-start: each
- id: report
name: 결과 알림
type: CALL
integration: slack_post_message
input:
channel: "${vars.OPS_CHANNEL_ID}"
text: "✅ 대기 항목 처리 완료"
edges:
- from: trigger
to: fetch
- from: fetch
to: filter
- from: filter
to: each
- from: each
to: process
- from: process
to: collect
- from: collect
to: report
눈여겨볼 곳¶
- 루프 전에
filter로 걸러냅니다. 루프 안에는CONDITIONAL을 넣을 수 없으니, 조건은 미리 처리합니다. items에| raw필수 — 없으면 배열이 문자열이 되어 반복이 안 됩니다.- 루프 안에서는
${item.id}/${index}로 현재 원소를 참조합니다. LOOP_END의loop-start는 짝이 되는LOOP_START의 id입니다.
5. 연결 없이 5분 만에 시작하기¶
외부 연결이 하나도 필요 없는 최소 예제입니다. 동작 확인용으로 좋습니다.
id: hello-eeumsae
name: 동작 확인
nodes:
- id: trigger
name: 웹훅 트리거
type: ENTRYPOINT
trigger:
kind: WEBHOOK
input-schema:
type: object
required: [name]
properties:
name: { type: string }
- id: greet
name: 인사말 만들기
type: CALL
integration: transform_jmespath
input:
expression: "{message: join('', ['안녕하세요, ', name, '님!'])}"
data:
name: "${nodes.trigger.response.body.name}"
edges:
- from: trigger
to: greet
curl -X POST "https://api.eeumsae.com/webhooks/<워크스페이스ID>/hello-eeumsae" \
-H "Content-Type: application/json" \
-d '{"name": "보찬"}'
조합해서 쓰기¶
위 레시피들은 부품처럼 섞을 수 있습니다.
| 만들고 싶은 것 | 조합 |
|---|---|
| 매일 리포트 + 이상 시에만 알림 | 1번 + 2번의 CONDITIONAL |
| 문의 분류 후 항목별 처리 | 3번 + 4번의 루프 |
| 중복 처리 방지 | 2번 + 데이터셋 조회·대조 |
말로 시키는 게 더 빠를 때도 많습니다. AI 비서를 연결해두면 "1번 레시피에서 슬랙 대신 이메일로 보내게 고쳐줘" 같은 요청이 바로 됩니다.
다음 → 13. 문제 해결