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: PT24H—windowStart/windowEndappear automatically, so you can query "yesterday's".data: "${... | raw}"— without| rawthe array becomes a string and the aggregation breaks.retry-on: ["429", "5xx"]— reads are safe to repeat, so retries are enabled.- The
notifynode 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.dataon thetrigger → routeedge — without it the condition cannot seeamount. This is the most common mistake.- The condition is
amount >= 100000— the name alone, no${}. - The two branches meet at
join, andlogafter it runs exactly once whichever way the execution went. logreferencestriggerdirectly instead ofjoin, 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
systemPromptnails it down with "exactly one word" and "never say anything else". If the AI's output wanders, the branching wobbles. - It uses
containsrather than==, so an answer like "this is a refund inquiry" still matches. - Always keep the
otherwise: truebranch. 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
CONDITIONALinside a loop, so conditions get handled up front. | rawis mandatory onitems— without it the array becomes a string and nothing iterates.- Inside the loop you reference the current element with
${item.id}/${index}. - The
loop-startofLOOP_ENDis the id of the matchingLOOP_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.
Next → 13. Troubleshooting