콘텐츠로 이동

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: PT24HwindowStart / windowEnd가 자동으로 생겨 "어제치"를 조회할 수 있습니다.
  • data: "${... | raw}"| raw 가 없으면 배열이 문자열이 되어 집계가 깨집니다.
  • retry-on: ["429", "5xx"] — 조회는 다시 해도 안전하니 재시도를 겁니다.
  • notify 노드에는 재시도를 걸지 않았습니다 — 메시지가 두 번 갈 수 있기 때문입니다.

응용

하고 싶은 것 바꿀 곳
주간 리포트로 cron: "0 0 9 ? * MON", lookback: P7D
Slack 대신 다른 곳으로 notifyhttp_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_ENDloop-start는 짝이 되는 LOOP_STARTid입니다.

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. 문제 해결