Expression System
Master DeepChain's powerful expression syntax for dynamic data access
Expression System
Status: Production-Ready β Version: 1.0 Last Updated: November 2025
Overview
Think of expressions as the glue that connects your workflow nodes together. The DeepChain Expression System lets you pass data between nodes using a simple {{ }} syntax. You can reference outputs from previous nodes, access trigger data, and use workflow settings to build dynamic, responsive workflows.
In just a few minutes, you'll be able to route data through your entire workflow without writing any code.
Quick Start
The Simplest Expression
Let's say you have a webhook that sends you a user ID, and you want to fetch that user's data. Here's how:
// Reference a node output by name (most common)
{{ $node["API Request"].json.data.userId }}
// Reference data from the trigger
{{ $trigger.webhookPayload.email }}
// Reference workflow settings
{{ $workflow.settings.apiKey }}
// Combine multiple expressions in one URL
{{ $node["Config"].json.baseUrl }}/users/{{ $trigger.userId }}
// Result: "https://api.example.com/users/user-123"
Tip: Start with the simplest expression you need. You can always make it more complex later!
Using the Data Mapper (Recommended)
You don't have to type these by hand. The Data Mapper helps you build expressions visually:
- Click the π icon next to any configuration field
- Browse the data tree on the left (shows all previous nodes, trigger data, and settings)
- Click any value to insert its expression automatically
- See a live preview of what the expression will return
- Click Insert to add it to your field
This is the easiest way to avoid typos and understand your data structure.
Accessing Node Data
Reference a Node by Name
When you want to use output from a previous node, reference it by name:
// Simple field access
{{ $node["API Request"].json.userId }}
// Nested fields
{{ $node["Get User"].json.profile.email }}
// Array access (get first user)
{{ $node["Search Results"].json.users[0].name }}
// Specific array element
{{ $node["Products"].json.items[2].price }}
Best Practice: Use node names instead of IDs. Names are human-readable and stay the same even if you reorganize your workflow.
The .json Part
Every node output is already a JSON object, so you'll see .json in most expressions. It just means "the actual data from this node."
{{ $node["MyNode"].json.firstName }} // Get firstName field
{{ $node["MyNode"].json }} // Get entire output
Real-World Example
Let's say you have:
- Webhook Node sends you
{ "customerId": "123" } - Fetch Customer node outputs
{ "id": "123", "name": "Alice", "email": "alice@example.com" } - You want to Send Email to that customer
You'd use:
// To field:
{{ $node["Fetch Customer"].json.email }}
// Subject:
Hello {{ $node["Fetch Customer"].json.name }}!
// Result sends to: alice@example.com with subject "Hello Alice!"
Accessing Trigger Data
Your workflow is triggered by somethingβa webhook, schedule, or manual click. That trigger data is always available:
// Webhook sends: { "action": "approve", "userId": "456" }
{{ $trigger.action }} // "approve"
{{ $trigger.userId }} // "456"
// Schedule trigger
{{ $trigger.scheduledTime }} // When the schedule ran
{{ $trigger.cronExpression }} // The cron pattern used
// Manual trigger with custom input
{{ $trigger.inputData.notes }} // Whatever you typed
Accessing Workflow Settings
Every workflow has settings (API keys, timeouts, etc.):
// Your workflow's name and ID
{{ $workflow.name }}
// Custom settings you configured
{{ $workflow.settings.apiKey }}
{{ $workflow.settings.maxRetries }}
{{ $workflow.metadata.department }}
Security Note: Use
$workflow.settingsfor API keys instead of hardcoding them in your nodes!
Powerful Expression Features
Combining Multiple Expressions
You can mix expressions and text in a single field:
// Build a dynamic URL
"https://{{ $node["Config"].json.domain }}/api/v{{ $node["Config"].json.version }}/users"
// If domain is "api.example.com" and version is "2":
// Result: "https://api.example.com/api/v2/users"
// Create a personalized message
"Dear {{ $node["User"].json.firstName }}, your balance is ${{ $node["Account"].json.balance }}"
// Result: "Dear Alice, your balance is $1,250.50"
Tip: You can put as many
{{ }}expressions as you need in one field. They're all evaluated and combined.
Navigating Deep Data Structures
Most real APIs return nested data. You can drill down as deep as you need:
{{ $node["API"].json.data.users[0].address.city }}
{{ $node["Response"].json.result.nested.deeply.buried.value }}
// With arrays of objects
{{ $node["Orders"].json.items[0].customer.email }}
// Optional chaining (if available)
{{ $node["User"].json?.profile?.email ?? "no-email@example.com" }}
Error Handling (Don't Worry!)
If you make a mistake, DeepChain handles it gracefully:
- Invalid expression syntax? The field keeps its original text value
- Referencing a missing node? You'll get a clear error message
- Accessing a field that doesn't exist? Returns empty instead of crashing
This means your workflows won't break unexpectedly.
Built-In Functions Reference
Here are all the functions you can use in expressions. Each includes a practical example you can copy and paste.
String Functions
| Function | Example | Result |
|---|---|---|
length |
{{ $node["Name"].json.value.length }} |
Number of characters |
toUpperCase() |
{{ $node["Email"].json.address.toUpperCase() }} |
"ALICE@EXAMPLE.COM" |
toLowerCase() |
{{ $node["Domain"].json.name.toLowerCase() }} |
"example" |
substring(start, end) |
{{ $node["Code"].json.code.substring(0, 3) }} |
First 3 chars |
includes(text) |
{{ $node["Email"].json.includes("@example.com") }} |
true/false |
startsWith(text) |
{{ $node["Code"].json.startsWith("ERR") }} |
true/false |
split(delimiter) |
{{ $node["Path"].json.split("/") }} |
["api", "users"] |
trim() |
{{ $node["Input"].json.trim() }} |
Removes spaces |
replace(find, replace) |
{{ $node["Text"].json.replace("old", "new") }} |
"new text" |
Array Functions
| Function | Example | Result |
|---|---|---|
length |
{{ $node["Items"].json.length }} |
Number of items |
[index] |
{{ $node["Items"].json[0] }} |
First item |
includes(value) |
{{ $node["Status"].json.includes("active") }} |
true/false |
indexOf(value) |
{{ $node["List"].json.indexOf("alice") }} |
2 |
join(delimiter) |
{{ $node["Tags"].json.join(", ") }} |
"tag1, tag2, tag3" |
reverse() |
{{ $node["Items"].json.reverse() }} |
Reversed array |
slice(start, end) |
{{ $node["Items"].json.slice(0, 3) }} |
First 3 items |
Math Operations
| Operation | Example | Result |
|---|---|---|
| Addition | {{ $node["Price"].json + 10 }} |
110 |
| Subtraction | {{ $node["Total"].json - 20 }} |
80 |
| Multiplication | {{ $node["Price"].json * 1.1 }} |
110 (with 10% tax) |
| Division | {{ $node["Total"].json / 2 }} |
50 |
| Comparison | {{ $node["Amount"].json > 100 }} |
true/false |
Object Operations
| Operation | Example | Result |
|---|---|---|
| Property access | {{ $node["User"].json.email }} |
"alice@example.com" |
| Nested access | {{ $node["User"].json.profile.phone }} |
"555-1234" |
| Optional chaining | {{ $node["User"].json?.settings?.theme ?? "light" }} |
Value or default |
| Keys | {{ Object.keys($node["Data"].json) }} |
["name", "email"] |
Conditional Operations
| Operation | Example | Result |
|---|---|---|
| Ternary operator | {{ $node["Status"].json === "active" ? "Yes" : "No" }} |
"Yes" |
| AND operator | {{ $node["Age"].json > 18 && $node["Verified"].json }} |
true/false |
| OR operator | {{ $node["Plan"].json === "pro" || $node["IsAdmin"].json }} |
true/false |
| NOT operator | {{ !$node["Deleted"].json }} |
true/false |
Real-World Examples
Example 1: Email Personalization
You receive a form submission and want to send a personalized email:
// Trigger data: { name: "Alice", email: "alice@example.com", amount: 500 }
To: {{ $trigger.email }}
Subject: Your ${{ $trigger.amount }} order confirmation!
Body:
Hi {{ $trigger.name }},
Thank you for your order of ${{ $trigger.amount }}.
Order #{{ $node["Save Order"].json.orderId }}
Example 2: Conditional Routing
Use expressions in an If Node to route based on data:
// Field to check:
{{ $node["Get User"].json.accountBalance }}
// Condition: greater than 1000
// If true β route to "Premium Services"
// If false β route to "Standard Services"
Example 3: Building Dynamic URLs
Create URLs that change based on configuration:
// API Node URL field:
{{ $workflow.settings.apiBaseUrl }}/v{{ $workflow.settings.apiVersion }}/users/{{ $trigger.userId }}/orders
// If apiBaseUrl = "https://api.example.com" and version = "3":
// Final URL: https://api.example.com/v3/users/alice123/orders
Example 4: Data Aggregation
Combine data from multiple nodes:
// In a Transform Node, build a complete object:
{
"userId": {{ $trigger.userId }},
"userName": "{{ $node["Fetch User"].json.name }}",
"email": "{{ $node["Fetch User"].json.email }}",
"accountBalance": {{ $node["Get Balance"].json.amount }},
"lastLogin": "{{ $node["Get User"].json.lastLogin }}",
"requestedAt": "{{ $trigger.timestamp }}"
}
Example 5: Text Processing
Extract and transform text:
// Extract domain from email
{{ $trigger.email.split("@")[1] }}
// If email is "alice@example.com", result is "example.com"
// Convert status to display text
{{ $node["Status"].json.code === 200 ? "Success" : "Failed" }}
// Combine first and last name
{{ $node["User"].json.firstName + " " + $node["User"].json.lastName }}
Common Patterns & Recipes
Pattern 1: Safe Access with Defaults
Sometimes data might not exist. Use the ?? operator for defaults:
// If email doesn't exist, use a placeholder
{{ $node["User"].json.email ?? "no-email@example.com" }}
// If status is missing, default to "pending"
{{ $node["Order"].json.status ?? "pending" }}
// Chain multiple levels with optional chaining
{{ $node["User"].json?.profile?.theme ?? "light" }}
Pattern 2: Array Filtering
Find specific items in an array:
// Get all active users (assume array of user objects)
{{ $node["Users"].json.filter(u => u.status === "active") }}
// Get first premium user
{{ $node["Users"].json.find(u => u.plan === "premium") }}
// Count items matching a condition
{{ $node["Orders"].json.filter(o => o.total > 100).length }}
Pattern 3: Array Transformation
Transform data from one format to another:
// Extract just the emails from users
{{ $node["Users"].json.map(u => u.email) }}
// Build price list with 10% markup
{{ $node["Products"].json.map(p => ({
name: p.name,
price: p.basePrice * 1.1
})) }}
Pattern 4: Checking Multiple Conditions
// All conditions must be true
{{ $node["User"].json.age > 18 && $node["User"].json.verified && $node["User"].json.hasPayment }}
// At least one condition must be true
{{ $node["Plan"].json === "pro" || $node["Plan"].json === "business" || $node["IsAdmin"].json }}
// Negate a condition
{{ !$node["IsDeleted"].json }}
Pattern 5: Type Checking
// Check if value is a number
{{ typeof $node["Amount"].json === "number" }}
// Check if value is an array
{{ Array.isArray($node["Items"].json) }}
// Check if string contains text
{{ $node["Email"].json.includes("@example.com") }}
Common Mistakes & How to Avoid Them
Mistake 1: Referencing a Node That Hasn't Run Yet
β Wrong:
// This fails if "Process Data" node comes after the current node
{{ $node["Process Data"].json.result }}
β Correct: Only reference nodes that appear before the current node in your workflow.
Tip: Check your workflow diagram. If there's no arrow from source node to your node, you can't reference it yet.
Mistake 2: Forgetting the .json Part
β Wrong:
{{ $node["User"].firstName }} // Missing .json
β Correct:
{{ $node["User"].json.firstName }} // Has .json
Mistake 3: Using Wrong Quotes
β Wrong:
{{ $node['User'].json.firstName }} // Single quotes can confuse the parser
β Correct:
{{ $node["User"].json.firstName }} // Double quotes are safer
Mistake 4: Typos in Node Names
β Wrong:
{{ $node["Get Usr"].json.email }} // Typo: "Usr" instead of "User"
β Correct: Use the Data Mapper to avoid typing node names manually. It shows you the exact name.
Mistake 5: Complex Logic in Expressions
β Wrong:
{{
$node["Orders"].json
.filter(o => o.status === "pending")
.map(o => ({...complex transformation...}))
.reduce(...complex calculation...)
}}
β Correct: Break complex transformations into separate nodes (Transform Node, Script Node, etc.).
Expressions are great for passing data, not for complex logic.
Best Practices Checklist
- β Keep it simple β Simple expressions are easier to debug and understand
- β Use node names β Way more readable than IDs
- β Test with Data Mapper β Avoid typos and see your data structure
- β Reference earlier nodes only β Don't create circular references
- β
Use sensible defaults β Use
??for optional fields - β Document complex expressions β Add a comment about what it does
- β Don't hardcode sensitive data β Use
$workflow.settingsinstead - β Don't nest deeply β Keep it flat for readability
Expression Cheat Sheet
Quick Reference for All Syntaxes
// Node References
{{ $node["NodeName"].json.field }}
{{ $node["NodeName"].json.array[0] }}
{{ $node["NodeName"].json.nested.deep.field }}
// Trigger References
{{ $trigger.field }}
{{ $trigger.body.userId }}
{{ $trigger.headers.authorization }}
// Workflow References
{{ $workflow.name }}
{{ $workflow.settings.apiKey }}
{{ $workflow.metadata.department }}
// Combining Text and Expressions
"Hello {{ $node["User"].json.name }}!"
// String Methods
{{ $trigger.email.toLowerCase() }}
{{ $node["Text"].json.substring(0, 5) }}
{{ $node["Path"].json.split("/") }}
// Array Methods
{{ $node["Items"].json.length }}
{{ $node["Items"].json[0] }}
{{ $node["Items"].json.join(", ") }}
// Math
{{ $node["Price"].json + 10 }}
{{ $node["Total"].json * 1.1 }}
// Comparisons
{{ $node["Status"].json === "active" }}
{{ $node["Amount"].json > 100 }}
// Conditional
{{ $node["Status"].json === "active" ? "Yes" : "No" }}
// Safe Access
{{ $node["User"].json.email ?? "unknown@example.com" }}
{{ $node["User"].json?.profile?.theme ?? "light" }}
// Filtering Arrays
{{ $node["Items"].json.filter(x => x.active) }}
// Transforming Arrays
{{ $node["Items"].json.map(x => x.name) }}
Troubleshooting
Expression Not Working?
Problem: Expression shows as literal text instead of being evaluated.
Solutions:
- Make sure you're using
{{ }}(double curly braces) - Check the node name is spelled exactly right (copy from Data Mapper)
- Verify the node has already executed (it appears before this node)
- Use the Data Mapper to validate your expression
Getting Undefined or Null
Problem: Expression evaluates to undefined or null.
Solutions:
- Check the field name is spelled correctly
- Use optional chaining:
{{ $node["User"].json?.email ?? "none" }} - Verify the field actually exists in the data
- Use the Data Mapper preview to see the actual data structure
Node Name Contains Spaces or Special Characters
Problem: Node name has spaces or hyphens.
Solution: Just use the name as-is in quotes:
{{ $node["My API Call"].json.data }}
{{ $node["Process-Data"].json.result }}
Array Index Out of Bounds
Problem: Trying to access index that doesn't exist.
Solution: Add a length check first:
{{ $node["Items"].json.length > 0 ? $node["Items"].json[0].name : "No items" }}
Tips for Success
Use Data Mapper β Don't type expressions manually. It's way easier to click.
Test early β Use a simple test execution to verify expressions work.
Start simple β Get basic data passing working first, then add complexity.
Break it into steps β If it's complex, spread logic across multiple nodes.
Name your nodes clearly β "Get User Data" is better than "API Request 1".
Add comments β In the node name or description, explain what data it provides.
Next Steps
- Try an example: Build a simple 3-node workflow using expressions
- Explore Data Mapper: Click the π icon in any field to see it in action
- Read more patterns: Check out data-flow-patterns.md for visual examples
- Build something real: Create a webhook β fetch data β send email workflow
Need help? Check the example workflows in your DeepChain installation!