Data Flow
Understanding how data moves between nodes in workflows
Understanding how data flows through workflows is essential for building effective automations. This guide explains the data model, expression languages, and patterns for moving data between nodes.
Data Flow Basics
How Data Moves
Data flows through connections between node ports:
Node A (output) ──connection──→ Node B (input)
- Node A processes and produces output data
- Connection carries data to Node B
- Node B receives data and can access it
The json Variable
Inside node configurations, access upstream data via the json variable:
json.meeting.title // Access nested fields
json.attendees[0].email // Array access
json.score // Direct field access
The trigger Variable
Trigger data is always available via the trigger variable:
trigger.meetingPlanId
trigger.dealRoomId
trigger.event
trigger.firedAt
Expression Languages
Agents uses two expression languages for different purposes:
CEL (Common Expression Language)
Used for: Dynamic values and conditions
// Field access
trigger.meetingPlanId
// Conditions
json.score > 0.8
// String operations
json.name + " - Summary"
// Null handling
json.value ?? "default"
Where used:
- Node parameter values (e.g., meeting_id, channel)
- If node conditions
- Array paths in Select Many
Liquid Templates
Used for: Text generation
Hello {{ json.name }},
Thank you for attending {{ json.meeting.title }}.
{% if json.actionItems.size > 0 %}
Action items:
{% for item in json.actionItems %}
- {{ item }}
{% endfor %}
{% endif %}
Where used:
- Message bodies (Slack, Email, SMS)
- AI prompt content
- Task descriptions
CEL Reference
Field Access
json.field // Direct access
json.nested.field // Nested access
json.array[0] // Array index
json.array[0].field // Nested array access
Comparisons
json.value == "expected"
json.count > 5
json.score >= 0.8
json.status != "closed"
Logical Operations
json.a > 1 && json.b < 5 // AND
json.x == 1 || json.y == 2 // OR
!json.processed // NOT
String Operations
json.name.startsWith("A")
json.text.contains("urgent")
json.email.endsWith("@company.com")
json.input.matches("^[A-Z].*") // Regex
Array Operations
json.items.size() // Length
"value" in json.tags // Contains
json.items.exists(x, x > 5) // Any match condition
json.items.all(x, x > 0) // All match condition
Null Safety
has(json.field) // Check existence
json.field ?? "default" // Default if null
has(json.nested) && json.nested.field == "value" // Safe access
Type Conversion
int(json.stringNumber) // String to int
string(json.number) // Number to string
double(json.value) // To decimal
Liquid Reference
Variable Output
{{ json.name }}
{{ json.meeting.title }}
{{ trigger.firedAt }}
Filters
{{ json.name | upcase }} // JOHN
{{ json.name | downcase }} // john
{{ json.text | truncate: 100 }} // Limit length
{{ json.date | date: "%B %d, %Y" }} // January 15, 2024
{{ json.items | size }} // Array length
{{ json.value | default: "N/A" }} // Default value
{{ json.text | strip }} // Remove whitespace
{{ json.array | first }} // First element
{{ json.array | last }} // Last element
{{ json.array | join: ", " }} // Array to string
Conditionals
{% if json.urgent %}
URGENT: Action required
{% elsif json.priority == "high" %}
High priority item
{% else %}
Standard processing
{% endif %}
Loops
{% for item in json.items %}
- {{ item.name }}: {{ item.value }}
{% endfor %}
{% for attendee in json.meeting.attendees %}
{{ forloop.index }}. {{ attendee.name }}
{% endfor %}
Loop Variables
{{ forloop.index }} // 1, 2, 3...
{{ forloop.index0 }} // 0, 1, 2...
{{ forloop.first }} // true for first item
{{ forloop.last }} // true for last item
{{ forloop.length }} // Total items
Available Objects
| Object | Contains |
|---|---|
json | Upstream node output |
trigger | Trigger event data |
author | Current user info (name, email) |
organization | Organization details |
Data Transformation
Node Output Becomes Input
Each node transforms data for downstream:
[Load Meeting]
Output: { meeting: {...}, callRecording: {...} }
↓
[AI Prompt]
Input: json.meeting, json.callRecording
Output: { type: "string", value: "Summary..." }
↓
[Slack Post]
Input: json.value (the summary)
Preserving Data Through Chains
Original data is replaced by node output. To preserve:
Option 1: Include in AI output
AI System Prompt:
Include the original meeting title in your response.
Option 2: Use Zip for parallel paths
┌─→ (original) ──┐
[Load] ─┤ ├─→ [Zip] → has both
└─→ [AI] ────────┘
Option 3: Use Broadcast for context
┌─→ (context) ──┐
[Load] ─┤ ├─→ [Broadcast] → has both
└─→ [Process] ──┘
Merging Data Streams
Zip: Combine into labeled arrays
Input A: { summary: "..." }
Input B: { items: [...] }
↓
[Zip]
↓
Output: {
summaryStream: [{ summary: "..." }],
itemsStream: [{ items: [...] }]
}
Broadcast: Attach context to items
Context: { meeting: "Q1 Review" }
Items: [{ name: "John" }, { name: "Jane" }]
↓
[Broadcast]
↓
Output: [
{ context: { meeting: "Q1 Review" }, item: { name: "John" } },
{ context: { meeting: "Q1 Review" }, item: { name: "Jane" } }
]
Common Data Patterns
Pattern: Access Trigger Data
// In any node
trigger.meetingPlanId
trigger.dealRoomId
Pattern: Access AI Output
After AI Prompt with return_type: "string":
{{ json.value }}
After AI Agent with return_type: "string_list":
{% for item in json.value %}
- {{ item }}
{% endfor %}
Pattern: Format Dates
{{ json.meeting.startTime | date: "%B %d, %Y" }}
// Output: January 15, 2024
{{ json.meeting.startTime | date: "%I:%M %p" }}
// Output: 02:30 PM
{{ json.meeting.startTime | date: "%Y-%m-%d" }}
// Output: 2024-01-15
Pattern: Handle Missing Data
{% if json.callRecording %}
{{ json.callRecording.transcriptSummary }}
{% else %}
No recording available for this meeting.
{% endif %}
// CEL with default
json.name ?? "Unknown"
// CEL existence check
has(json.field) && json.field != ""
Pattern: Iterate with Index
{% for item in json.actionItems %}
{{ forloop.index }}. {{ item }}
{% endfor %}
Pattern: Join Array to String
Attendees: {{ json.meeting.attendees | map: "name" | join: ", " }}
// Output: Attendees: John, Jane, Bob
Pattern: First/Last Element
Primary contact: {{ json.contacts | first | map: "name" }}
Most recent: {{ json.meetings | last | map: "title" }}
Debugging Data Flow
Check What Data You Have
- Use execution logs - View input/output at each node
- Add a temporary Slack node - Output raw data for inspection
- Check AI prompts - Verify data appears as expected
Common Issues
Issue: undefined or empty values
- Cause: Wrong path or missing data
- Fix: Check exact path, add conditionals
Issue: Data from wrong source
- Cause: Confusion between json and trigger
- Fix: Remember json = upstream, trigger = event
Issue: Array instead of single value
- Cause: Forgot to index into array
- Fix: Use
json.array[0]orjson.array | first
Issue: Format errors in templates
- Cause: Syntax error in Liquid
- Fix: Check brackets, quotes, filter syntax
Best Practices
1. Be Explicit About Data Sources
// Clear what's being accessed
Meeting: {{ json.meeting.title }}
Trigger: {{ trigger.event }}
2. Handle Missing Data
Always consider: what if this field is missing?
{% if json.summary %}{{ json.summary }}{% else %}No summary available{% endif %}
3. Use Descriptive Labels
When using Zip/Broadcast, use meaningful labels:
labels: ["meetingContext", "analysisResults"]
4. Test with Real Data
Use MANUAL trigger to test with actual meeting data before releasing.
5. Keep Expressions Simple
If an expression gets complex, consider:
- Computing parts in an AI node
- Breaking into multiple nodes
- Using intermediate processing