05. Connecting Data with Expressions¶
This is how a later node uses a value an earlier node produced. It is the part of eeumsae you use most often and get wrong most often, so this is the one page worth reading end to end.
There is only one piece of syntax.
Where values can come from¶
| How you write it | What you get |
|---|---|
${nodes.<nodeId>.response.body.<field>} |
A previous node's output — by far the most common |
${nodes.<nodeId>.request.data.<field>} |
The input that node received |
${input.<field>} |
A value handed over by an edge (conditional nodes use this scope too) |
${secrets.<key>} |
A workspace secret (API keys and the like) |
${vars.<key>} |
A plain workspace variable |
${connections.<provider>.<field>} |
Tokens and such from a connected service |
${item} / ${item.<field>} |
The current element being iterated (inside loops only) |
${index} |
The iteration number (starting at 0) |
${datasets.<title>.count} |
The dataset's row count |
${datasets.<title>.latest} |
The dataset's most recent row, whole |
${datasets.<title>.latest.<field>} |
A specific field of the most recent row |
Reading a previous node's output (the essential one)¶
Node ids may contain hyphens. For nested fields, just keep drilling down with dots.
${nodes.fetch.response.body.data.customer.name} # nested object
${nodes.fetch.response.body.items.0.name} # first array element — a number is an index
${nodes.fetch.response.body.items.2.price} # third element
Reading secrets and variables¶
apiKey: "${secrets.OPENAI_API_KEY}" # sensitive values
uri: "${vars.API_BASE_URL}/orders" # plain settings
Secrets and plain variables cannot reach each other. You cannot pull a secret out with ${vars.*}, nor the other way round.
→ 08. Variables and Secrets
| raw — passing structure through untouched¶
This is where the most common mistake in eeumsae happens. Please read it.
By default the result of an expression is converted to a string. That is fine for numbers and short text, but it becomes a problem when you pass an array or an object to the next node.
# ❌ the array gets mashed into a string
data: "${nodes.fetch-orders.response.body.data}"
# → you end up with a strange string like "[{id=1, amount=1000}, {id=2, amount=2000}]"
# ✅ the array is passed as an array
data: "${nodes.fetch-orders.response.body.data | raw}"
Where you need | raw¶
| Situation | Example |
|---|---|
| Passing data to a Transform node | data: "${nodes.fetch.response.body.items \| raw}" |
| Passing an array for a loop to iterate | items: "${nodes.fetch.response.body.data \| raw}" |
| Passing a whole object | payload: "${nodes.build.response.body \| raw}" |
The rule for | raw¶
Use it only when the entire value is a single expression. You cannot embed it in the middle of a sentence.
# ✅ the whole value is one expression
data: "${nodes.fetch.response.body.items | raw}"
# ❌ mixed into a sentence — this does not work
text: "Order list: ${nodes.fetch.response.body.items | raw}"
If you want it inside a sentence, then the value was supposed to be a string in the first place. Use it without | raw.
Two ways to pass an object as input¶
Way 1 — write out the structure and use expressions only for the values (recommended)
input:
body:
orderId: "${nodes.trigger.response.body.orderId}"
amount: "${nodes.aggregate.response.body.totalAmount}"
status: "confirmed"
Way 2 — the whole thing with | raw
The only thing to avoid is passing a whole object without | raw.
Edge mapping vs. direct references¶
There are two styles that do the same thing. Pick one and stay consistent.
Style A — direct reference (simple)¶
nodes:
- id: notify
type: CALL
integration: http_request
input:
body:
orderId: "${nodes.trigger.response.body.orderId}"
edges:
- from: trigger
to: notify
The node itself knows where the value comes from. Short and easy to read.
Style B — edge mapping + ${input.*}¶
nodes:
- id: notify
type: CALL
integration: http_request
input:
body:
orderId: "${input.orderId}"
edges:
- from: trigger
to: notify
request:
data:
orderId: "${nodes.trigger.response.body.orderId}"
The node only knows that "it needs an orderId"; the edge knows where to get it from.
When you must use B¶
When the edge leads into a conditional node (CONDITIONAL), B is mandatory.
Conditions are evaluated in the ${input.*} scope, so if the edge does not hand a value over, the condition can see nothing at all.
→ 06. Flow Control
Common problems¶
My expression comes out as literal text¶
When the path inside ${...} is wrong, the execution fails with ExpressionResolveException.
Open that node in the execution history, look at the actual output of the node right before it, and fix the path.
Expressions are not checked when you save. They are only resolved at execution time. That is deliberate, so you can keep saving work in progress in the GUI.
I can't read a value from a JOINT node¶
A JOINT has no output of its own. It only merges paths.
In the node after a JOINT, reference the node before the JOINT directly.
# ❌ a JOINT has no output
${nodes.join.response.body.value}
# ✅ reference the node before the JOINT
${nodes.fetch-a.response.body.value}
{{ }} is not an expression¶
{{key}} is a placeholder belonging to the recipes feature. Never put it in workflow YAML you write yourself.
An expression is always one brace plus $ — ${...}.
It's a schedule trigger but there's no windowStart¶
It only exists if you set lookback. → 03. Setting Up Triggers
Quick reference¶
# previous node output
${nodes.fetch.response.body.data}
${nodes.fetch.response.body.items.0.name} # array index
${nodes.fetch.response.body.data | raw} # structure preserved
# trigger
${nodes.trigger.response.body.orderId} # webhook body
${nodes.trigger.response.body.windowStart} # schedule lookback
# workspace resources
${secrets.MY_API_KEY}
${vars.BASE_URL}
${connections.slack.accessToken}
${datasets.processing-log.count}
${datasets.processing-log.latest.orderId}
# values handed over by an edge / condition scope
${input.orderId}
# inside a loop
${item}
${item.orderId}
${index}
Next → 06. Flow Control