Skip to content

12. Recipes

Finished examples you can copy and use straight away. Every example here is YAML that passes the real eeumsae validator. Paste it into the YAML tab of a workflow's detail screen and save.

After pasting, two things need to be changed to your own:

  • id — it has to be unique within the workspace
  • ${vars.*} / ${secrets.*} — register them in advance on the Variables screen

1. A morning revenue report

Every day at 9 AM → fetch yesterday's orders → aggregate → AI summary → post to Slack

The most representative shape. It contains a schedule, a time window, retries, data shaping, AI and sending.

What you need

Kind Key Value
Variable SHOP_API_BASE https://api.myshop.com
Variable REPORT_CHANNEL_ID A Slack channel ID
Secret SHOP_API_TOKEN Your store's API token
Secret OPENAI_API_KEY Your OpenAI API key
Connection Slack Connect it and invite the bot to the channel
id: daily-revenue-report
name: Daily revenue report

nodes:
  - id: trigger
    name: Every day at 9 AM
    type: ENTRYPOINT
    trigger:
      kind: SCHEDULER
      cron: "0 0 9 * * ?"
      timezone: "Asia/Seoul"
      lookback: PT24H

  - id: fetch-orders
    name: Fetch yesterday's orders
    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: Aggregate revenue
    type: CALL
    integration: transform_jmespath
    input:
      expression: "{orderCount: length(@), totalRevenue: sum([*].amount), largestOrder: max([*].amount)}"
      data: "${nodes.fetch-orders.response.body.data | raw}"

  - id: summarize
    name: AI summary
    type: CALL
    integration: llm_chat
    timeout: 60s
    input:
      apiContract: OPENAI_CHAT
      model: gpt-4o-mini
      apiKey: "${secrets.OPENAI_API_KEY}"
      systemPrompt: "You are an e-commerce operations lead. Do not exaggerate numbers; report facts concisely."
      userPrompt: |
        Summarize yesterday's revenue data below in three lines.

        Order count: ${nodes.aggregate.response.body.orderCount}
        Total revenue: ${nodes.aggregate.response.body.totalRevenue}
        Largest order: ${nodes.aggregate.response.body.largestOrder}

  - id: notify
    name: Send to Slack
    type: CALL
    integration: slack_post_message
    input:
      channel: "${vars.REPORT_CHANNEL_ID}"
      text: |
        📊 Yesterday's revenue report

        ${nodes.summarize.response.body.content}

edges:
  - from: trigger
    to: fetch-orders
  - from: fetch-orders
    to: aggregate
  - from: aggregate
    to: summarize
  - from: summarize
    to: notify

Worth noticing

  • lookback: PT24HwindowStart / windowEnd appear automatically, so you can query "yesterday's".
  • data: "${... | raw}"without | raw the array becomes a string and the aggregation breaks.
  • retry-on: ["429", "5xx"] — reads are safe to repeat, so retries are enabled.
  • The notify node has no retry — the message could go out twice.

Variations

What you want What to change
Make it weekly cron: "0 0 9 ? * MON", lookback: P7D
Somewhere other than Slack Replace notify with http_request
Change the tone of the summary Edit systemPrompt

2. Order webhook → alerts by amount

Receive an order webhook → branch on the amount → alert a different channel for each → join → save to a log

An example that uses branching (CONDITIONAL) and joining (JOINT) together.

What you need

Kind Key
Variable VIP_CHANNEL_ID, ORDER_CHANNEL_ID
Dataset order-log (columns: orderId STRING, amount NUMBER)
Connection Slack
id: order-alert
name: Order webhook alerts

nodes:
  - id: trigger
    name: Order webhook
    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: Branch by amount
    type: CONDITIONAL
    execution-info:
      conditions:
        - label: vip
          expression: "amount >= 100000"
        - label: normal
          otherwise: true

  - id: notify-vip
    name: VIP alert
    type: CALL
    integration: slack_post_message
    input:
      channel: "${vars.VIP_CHANNEL_ID}"
      text: "🔥 Large order! ${nodes.trigger.response.body.customerName} / ${nodes.trigger.response.body.amount} (order ${nodes.trigger.response.body.orderId})"

  - id: notify-normal
    name: Standard alert
    type: CALL
    integration: slack_post_message
    input:
      channel: "${vars.ORDER_CHANNEL_ID}"
      text: "🛒 New order: ${nodes.trigger.response.body.customerName} / ${nodes.trigger.response.body.amount}"

  - id: join
    name: Join
    type: JOINT

  - id: log
    name: Save to the order log
    type: CALL
    integration: dataset
    input:
      datasetTitle: "order-log"
      operation: INSERT
      data:
        orderId: "${nodes.trigger.response.body.orderId}"
        amount: "${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

Worth noticing

  • request.data on the trigger → route edge — without it the condition cannot see amount. This is the most common mistake.
  • The condition is amount >= 100000the name alone, no ${}.
  • The two branches meet at join, and log after it runs exactly once whichever way the execution went.
  • log references trigger directly instead of join, because a JOINT has no output.

Testing

curl -X POST "https://api.eeumsae.com/webhooks/<workspaceId>/order-alert" \
  -H "Content-Type: application/json" \
  -d '{"orderId":"ORD-1234","customerName":"Ada Lovelace","amount":150000}'

3. Auto-routing customer inquiries

Receive an inquiry webhook → AI classifies it → route to the responsible channel

An example of branching on an AI response. It uses the contains operator.

What you need

Kind Key
Variable REFUND_CHANNEL_ID, DELIVERY_CHANNEL_ID, SUPPORT_CHANNEL_ID
Secret OPENAI_API_KEY
Connection Slack
id: inquiry-router
name: Customer inquiry auto-routing

nodes:
  - id: trigger
    name: Inquiry webhook
    type: ENTRYPOINT
    trigger:
      kind: WEBHOOK
      input-schema:
        type: object
        required: [message]
        properties:
          message: { type: string }
          email:   { type: string }

  - id: classify
    name: Classify the inquiry
    type: CALL
    integration: llm_chat
    timeout: 30s
    input:
      apiContract: OPENAI_CHAT
      model: gpt-4o-mini
      apiKey: "${secrets.OPENAI_API_KEY}"
      systemPrompt: "Classify the customer inquiry with exactly one word: refund / delivery / product / other. Never say anything else."
      userPrompt: "${nodes.trigger.response.body.message}"

  - id: route
    name: Branch by category
    type: CONDITIONAL
    execution-info:
      conditions:
        - label: refund
          expression: "category contains 'refund'"
        - label: delivery
          expression: "category contains 'delivery'"
        - label: other
          otherwise: true

  - id: to-refund
    name: Alert the refunds team
    type: CALL
    integration: slack_post_message
    input:
      channel: "${vars.REFUND_CHANNEL_ID}"
      text: "💸 Refund inquiry\n${nodes.trigger.response.body.message}"

  - id: to-delivery
    name: Alert the delivery team
    type: CALL
    integration: slack_post_message
    input:
      channel: "${vars.DELIVERY_CHANNEL_ID}"
      text: "📦 Delivery inquiry\n${nodes.trigger.response.body.message}"

  - id: to-general
    name: Alert general support
    type: CALL
    integration: slack_post_message
    input:
      channel: "${vars.SUPPORT_CHANNEL_ID}"
      text: "💬 Inquiry (${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

Worth noticing

  • The systemPrompt nails it down with "exactly one word" and "never say anything else". If the AI's output wanders, the branching wobbles.
  • It uses contains rather than ==, so an answer like "this is a refund inquiry" still matches.
  • Always keep the otherwise: true branch. Then an unexpected answer does not make the inquiry disappear.

4. Processing items one by one

Every hour → fetch pending items → keep only the ones to process → handle each item → completion alert

An example that uses loops (LOOP_START / LOOP_END).

id: per-item-processing
name: Per-item processing

nodes:
  - id: trigger
    name: Run hourly
    type: ENTRYPOINT
    trigger:
      kind: SCHEDULER
      cron: "0 0 * * * ?"
      timezone: "Asia/Seoul"

  - id: fetch
    name: Fetch pending items
    type: CALL
    integration: http_request
    timeout: 30s
    input:
      uri: "${vars.API_BASE_URL}/pending-items"
      method: GET

  - id: filter
    name: Keep only the targets
    type: CALL
    integration: transform_jmespath
    input:
      expression: "{targets: items[?status == `pending`]}"
      data: "${nodes.fetch.response.body | raw}"

  - id: each
    name: Start per-item loop
    type: LOOP_START
    execution-info:
      items: "${nodes.filter.response.body.targets | raw}"

  - id: process
    name: Process the item
    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: Collect loop results
    type: LOOP_END
    execution-info:
      loop-start: each

  - id: report
    name: Report the result
    type: CALL
    integration: slack_post_message
    input:
      channel: "${vars.OPS_CHANNEL_ID}"
      text: "✅ Pending items processed"

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

Worth noticing

  • Filtering happens before the loop. You cannot put a CONDITIONAL inside a loop, so conditions get handled up front.
  • | raw is mandatory on items — without it the array becomes a string and nothing iterates.
  • Inside the loop you reference the current element with ${item.id} / ${index}.
  • The loop-start of LOOP_END is the id of the matching LOOP_START.

5. Getting started in 5 minutes with no connections

The minimal example, needing no external connections at all. Good for a smoke test.

id: hello-eeumsae
name: Smoke test

nodes:
  - id: trigger
    name: Webhook trigger
    type: ENTRYPOINT
    trigger:
      kind: WEBHOOK
      input-schema:
        type: object
        required: [name]
        properties:
          name: { type: string }

  - id: greet
    name: Build a greeting
    type: CALL
    integration: transform_jmespath
    input:
      expression: "{message: join('', ['Hello, ', name, '!'])}"
      data:
        name: "${nodes.trigger.response.body.name}"

edges:
  - from: trigger
    to: greet
curl -X POST "https://api.eeumsae.com/webhooks/<workspaceId>/hello-eeumsae" \
  -H "Content-Type: application/json" \
  -d '{"name": "Ada"}'

Combining them

The recipes above mix together like parts.

What you want to build Combination
A daily report that only alerts on anomalies Recipe 1 + the CONDITIONAL from recipe 2
Classify inquiries, then process each one Recipe 3 + the loop from recipe 4
Prevent duplicate processing Recipe 2 + reading and comparing a dataset

Often it is faster to just say what you want. With an AI assistant connected, a request like "take recipe 1 and change it to send email instead of Slack" works immediately.


Next13. Troubleshooting