Search documentation

Search for pages in the documentation

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:

text
Node A (output) ──connection──→ Node B (input)
  1. Node A processes and produces output data
  2. Connection carries data to Node B
  3. Node B receives data and can access it

The json Variable

Inside node configurations, access upstream data via the json variable:

cel
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:

cel
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

cel
// 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

liquid
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

cel
json.field                   // Direct access
json.nested.field           // Nested access
json.array[0]               // Array index
json.array[0].field         // Nested array access

Comparisons

cel
json.value == "expected"
json.count > 5
json.score >= 0.8
json.status != "closed"

Logical Operations

cel
json.a > 1 && json.b < 5    // AND
json.x == 1 || json.y == 2  // OR
!json.processed             // NOT

String Operations

cel
json.name.startsWith("A")
json.text.contains("urgent")
json.email.endsWith("@company.com")
json.input.matches("^[A-Z].*")  // Regex

Array Operations

cel
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

cel
has(json.field)             // Check existence
json.field ?? "default"     // Default if null
has(json.nested) && json.nested.field == "value"  // Safe access

Type Conversion

cel
int(json.stringNumber)      // String to int
string(json.number)         // Number to string
double(json.value)          // To decimal

Liquid Reference

Variable Output

liquid
{{ json.name }}
{{ json.meeting.title }}
{{ trigger.firedAt }}

Filters

liquid
{{ 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

liquid
{% if json.urgent %}
URGENT: Action required
{% elsif json.priority == "high" %}
High priority item
{% else %}
Standard processing
{% endif %}

Loops

liquid
{% for item in json.items %}
- {{ item.name }}: {{ item.value }}
{% endfor %}

{% for attendee in json.meeting.attendees %}
{{ forloop.index }}. {{ attendee.name }}
{% endfor %}

Loop Variables

liquid
{{ 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

ObjectContains
jsonUpstream node output
triggerTrigger event data
authorCurrent user info (name, email)
organizationOrganization details

Data Transformation

Node Output Becomes Input

Each node transforms data for downstream:

text
        [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

liquid
AI System Prompt:
Include the original meeting title in your response.

Option 2: Use Zip for parallel paths

text
        ┌─→ (original) ──┐
[Load] ─┤                ├─→ [Zip] → has both
        └─→ [AI] ────────┘

Option 3: Use Broadcast for context

text
        ┌─→ (context) ──┐
[Load] ─┤               ├─→ [Broadcast] → has both
        └─→ [Process] ──┘

Merging Data Streams

Zip: Combine into labeled arrays

text
Input A: { summary: "..." }
Input B: { items: [...] }
            ↓
         [Zip]
            ↓
Output: {
  summaryStream: [{ summary: "..." }],
  itemsStream: [{ items: [...] }]
}

Broadcast: Attach context to items

text
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

cel
// In any node
trigger.meetingPlanId
trigger.dealRoomId

Pattern: Access AI Output

After AI Prompt with return_type: "string":

liquid
{{ json.value }}

After AI Agent with return_type: "string_list":

liquid
{% for item in json.value %}
- {{ item }}
{% endfor %}

Pattern: Format Dates

liquid
{{ 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

liquid
{% if json.callRecording %}
{{ json.callRecording.transcriptSummary }}
{% else %}
No recording available for this meeting.
{% endif %}
cel
// CEL with default
json.name ?? "Unknown"

// CEL existence check
has(json.field) && json.field != ""

Pattern: Iterate with Index

liquid
{% for item in json.actionItems %}
{{ forloop.index }}. {{ item }}
{% endfor %}

Pattern: Join Array to String

liquid
Attendees: {{ json.meeting.attendees | map: "name" | join: ", " }}
// Output: Attendees: John, Jane, Bob

Pattern: First/Last Element

liquid
Primary contact: {{ json.contacts | first | map: "name" }}
Most recent: {{ json.meetings | last | map: "title" }}

Debugging Data Flow

Check What Data You Have

  1. Use execution logs - View input/output at each node
  2. Add a temporary Slack node - Output raw data for inspection
  3. 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] or json.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

liquid
// Clear what's being accessed
Meeting: {{ json.meeting.title }}
Trigger: {{ trigger.event }}

2. Handle Missing Data

Always consider: what if this field is missing?

liquid
{% if json.summary %}{{ json.summary }}{% else %}No summary available{% endif %}

3. Use Descriptive Labels

When using Zip/Broadcast, use meaningful labels:

text
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