The Two Data Sources
QuivaWorks flows work with two primary data sources that you can reference using variable mapping:
Trigger Data Data provided when the flow starts (webhooks, API calls, manual triggers)
Node Data Output data from any previously executed node in your flow
Core Syntax Pattern
The basic syntax follows this pattern:
$ - Root indicator (always required)
source - Either trigger or a NODE_ID
property - The data you want to access
The $. prefix tells QuivaWorks to evaluate this as a variable mapping expression rather than treating it as a literal string.
Accessing Trigger Data
Trigger data is available from the very first node in your flow. It contains the data provided when the flow was initiated.
Webhook Trigger
API Trigger
Manual Trigger
When a webhook triggers your flow: {
"event" : "order.created" ,
"order_id" : "ORD-12345" ,
"user_id" : "USR-789" ,
"timestamp" : "2025-01-15T10:30:00Z" ,
"data" : {
"total" : 299.99 ,
"items_count" : 3
}
}
{
"eventType" : "$.trigger.event" ,
"orderId" : "$.trigger.order_id" ,
"userId" : "$.trigger.user_id" ,
"orderTotal" : "$.trigger.data.total" ,
"itemCount" : "$.trigger.data.items_count"
}
Result: {
"eventType" : "order.created" ,
"orderId" : "ORD-12345" ,
"userId" : "USR-789" ,
"orderTotal" : 299.99 ,
"itemCount" : 3
}
When an API call triggers your flow: {
"action" : "process_payment" ,
"customer_id" : "CUST-456" ,
"amount" : 150.00 ,
"currency" : "USD" ,
"metadata" : {
"source" : "mobile_app" ,
"device" : "iPhone"
}
}
{
"action" : "$.trigger.action" ,
"customerId" : "$.trigger.customer_id" ,
"paymentAmount" : "$.trigger.amount" ,
"paymentCurrency" : "$.trigger.currency" ,
"source" : "$.trigger.metadata.source"
}
When manually starting a flow with form data: {
"email" : "[email protected] " ,
"name" : "John Doe" ,
"message" : "Please process my request" ,
"priority" : "high"
}
{
"recipientEmail" : "$.trigger.email" ,
"recipientName" : "$.trigger.name" ,
"requestMessage" : "$.trigger.message" ,
"priorityLevel" : "$.trigger.priority"
}
Trigger data is read-only throughout the flow. It contains the original input and doesn’t change as nodes execute.
Accessing Node Data
Node data becomes available after a node completes execution. You can reference any previously executed node using its unique ID.
Basic Node Access
Node Output (fetch_customer)
Variable Mapping
Result
{
"id" : "CUST-789" ,
"firstName" : "Alice" ,
"lastName" : "Smith" ,
"email" : "[email protected] " ,
"phone" : "+1-555-0123" ,
"tier" : "premium"
}
Execution Order Matters
Node 1: fetch_customer
Executes first. Can access $.trigger.* but no other nodes yet.
Node 2: fetch_orders
Can access $.trigger.* and $.fetch_customer.*
Node 3: send_email
Can access $.trigger.*, $.fetch_customer.*, and $.fetch_orders.*
You cannot reference a node that hasn’t executed yet. If Node 2 tries to access $.node_3.data, it will return undefined.
Nested Properties
Access deeply nested data using dot notation:
Simple Nesting
Complex Nesting
Mixed Sources
{
"fetch_user" : {
"profile" : {
"contact" : {
"email" : "[email protected] " ,
"phone" : "+1-555-0100"
}
}
}
}
{
"email" : "$.fetch_user.profile.contact.email" ,
"phone" : "$.fetch_user.profile.contact.phone"
}
{
"api_response" : {
"data" : {
"user" : {
"account" : {
"settings" : {
"notifications" : {
"email" : true ,
"sms" : false
}
}
}
}
}
}
}
{
"emailNotifications" : "$.api_response.data.user.account.settings.notifications.email" ,
"smsNotifications" : "$.api_response.data.user.account.settings.notifications.sms"
}
{
"triggerId" : "$.trigger.request_id" ,
"userEmail" : "$.fetch_user.profile.contact.email" ,
"orderTotal" : "$.fetch_order.payment.total" ,
"shippingAddress" : "$.fetch_user.addresses.shipping.street"
}
Combine trigger data with nested node data seamlessly.
Array Access
Arrays use zero-based indexing (first element is at index 0).
Accessing Array Elements
Data
Positive Index
Negative Index
{
"fetch_orders" : {
"orders" : [
{ "id" : "ORD-001" , "total" : 99.99 },
{ "id" : "ORD-002" , "total" : 149.99 },
{ "id" : "ORD-003" , "total" : 79.99 }
]
}
}
Negative indexes count from the end:
[-1] = last item
[-2] = second to last item
[-3] = third to last item
Accessing Properties in Array Items
{
"products" : [
{ "name" : "Widget" , "price" : 29.99 , "stock" : 100 },
{ "name" : "Gadget" , "price" : 49.99 , "stock" : 50 },
{ "name" : "Doohickey" , "price" : 19.99 , "stock" : 200 }
]
}
{
"firstProductName" : "$.node.products[0].name" ,
"firstProductPrice" : "$.node.products[0].price" ,
"lastProductStock" : "$.node.products[-1].stock"
}
{
"firstProductName" : "Widget" ,
"firstProductPrice" : 29.99 ,
"lastProductStock" : 200
}
Combining Trigger and Node Data
Real-world flows typically combine both data sources:
Scenario
Trigger Data
Node Data
Combined Mapping
// Webhook triggers flow with order ID
// Flow fetches customer and order details
// Combines all data for email
Type Handling
Variable mapping preserves data types from the source:
Strings
Numbers
Booleans
Arrays & Objects
{
"node" : {
"name" : "Alice Smith" ,
"status" : "active"
}
}
{
"userName" : "$.node.name" ,
"userStatus" : "$.node.status"
}
{
"userName" : "Alice Smith" ,
"userStatus" : "active"
}
{
"node" : {
"count" : 42 ,
"price" : 29.99 ,
"rating" : 4.5
}
}
{
"itemCount" : "$.node.count" ,
"itemPrice" : "$.node.price" ,
"itemRating" : "$.node.rating"
}
{
"itemCount" : 42 ,
"itemPrice" : 29.99 ,
"itemRating" : 4.5
}
{
"node" : {
"active" : true ,
"verified" : false ,
"premium" : true
}
}
{
"isActive" : "$.node.active" ,
"isVerified" : "$.node.verified" ,
"isPremium" : "$.node.premium"
}
{
"isActive" : true ,
"isVerified" : false ,
"isPremium" : true
}
{
"node" : {
"tags" : [ "urgent" , "vip" , "priority" ],
"metadata" : {
"source" : "api" ,
"version" : "2.0"
}
}
}
{
"allTags" : "$.node.tags" ,
"meta" : "$.node.metadata"
}
Output (Preserved Structure)
{
"allTags" : [ "urgent" , "vip" , "priority" ],
"meta" : {
"source" : "api" ,
"version" : "2.0"
}
}
Handling Missing Data
When a path doesn’t exist, variable mapping removes the property from the output entirely:
Available Data
Mapping with Missing Path
Result (properties removed)
Properties with missing data are removed from the output entirely. They won’t appear as undefined or null - they simply won’t exist in the result object.
Use a trailing pipe to ensure missing values create the property with an empty string: {
"userPhone" : "$.node.phone|" ,
"userAddress" : "$.node.address.street|"
}
Result: {
"userPhone" : "" ,
"userAddress" : ""
}
This keeps the property in the output instead of removing it completely. Note: The trailing pipe also affects how arrays and objects are handled:
Arrays: Joined as comma-separated string (e.g., "a,b,c")
Objects: Converted to "[object Object]" (usually not desired)
Learn more about the pipe operator in Pipe Operator .
Type Handling with Pipe Operator
The trailing pipe (|) forces string conversion, which affects different data types:
Primitives
Arrays
Objects
{
"node" : {
"name" : "Alice" ,
"age" : 30 ,
"active" : true
}
}
{
"name" : "$.node.name|" ,
"age" : "$.node.age|" ,
"active" : "$.node.active|"
}
{
"name" : "Alice" ,
"age" : "30" ,
"active" : "true"
}
Strings, numbers, and booleans are all converted to strings with pipe. {
"node" : {
"tags" : [ "premium" , "verified" , "active" ]
}
}
{
"withPipe" : "$.node.tags|" ,
"withoutPipe" : "$.node.tags"
}
{
"withPipe" : "premium,verified,active" ,
"withoutPipe" : [ "premium" , "verified" , "active" ]
}
With pipe: Array becomes comma-separated string
Without pipe: Array structure preserved{
"node" : {
"address" : {
"street" : "123 Main St" ,
"city" : "Boston"
}
}
}
{
"withPipe" : "$.node.address|" ,
"withoutPipe" : "$.node.address"
}
{
"withPipe" : "[object Object]" ,
"withoutPipe" : {
"street" : "123 Main St" ,
"city" : "Boston"
}
}
With pipe: Object becomes unhelpful "[object Object]" string
Without pipe: Object structure preserved
Use trailing pipe carefully: Scenario Recommendation Ensuring property exists when data might be missing ✅ Use pipe: `$.value ` Simple array to comma-separated string ✅ Use pipe: `$.tags ` Preserving array structure ❌ No pipe: $.items Preserving object structure ❌ No pipe: $.user.address Array of objects ❌ No pipe: $.orders
Best Practices
Choose clear, meaningful node IDs that describe what the node does: // ✅ Good - Clear and descriptive
{
"email" : "$.fetch_customer_data.email" ,
"total" : "$.calculate_order_total.amount"
}
// ❌ Avoid - Unclear and unmaintainable
{
"email" : "$.node_1.email" ,
"total" : "$.node_5.amount"
}
Only reference nodes that have already executed: Flow Order:
1. trigger → available to all nodes
2. node_a → available to node_b, node_c
3. node_b → available to node_c only
4. node_c → not available to previous nodes
Use the Flow Debugger to verify node execution order.
Always plan for missing or null data. Use a trailing pipe to ensure empty strings: {
"email" : "$.user.email|" ,
"phone" : "$.user.phone|" ,
"address" : "$.user.address|"
}
Or use static text concatenation for default messages: {
"status" : "Status: |$.order.status|" ,
"note" : "Note: |$.order.note|"
}
Use the Flow Debugger to test mappings with actual data:
Run your flow with test data
Inspect each node’s output
Verify mapping expressions return expected values
Check for undefined or null values
Common Patterns
Webhook to Database
Enrichment
Notification
{
"table" : "orders" ,
"data" : {
"order_id" : "$.trigger.order_id" ,
"customer_id" : "$.trigger.customer_id" ,
"created_at" : "$.trigger.timestamp" ,
"total" : "$.trigger.data.total"
}
}
{
"enrichedOrder" : {
"orderId" : "$.trigger.order_id" ,
"customerDetails" : "$.fetch_customer" ,
"orderDetails" : "$.fetch_order" ,
"shippingInfo" : "$.fetch_shipping"
}
}
{
"to" : "$.fetch_customer.email" ,
"message" : "Order $.trigger.order_id is being processed" ,
"userId" : "$.trigger.user_id" ,
"userName" : "$.fetch_customer.name"
}
Quick Syntax Reference
Pattern Description Example $.trigger.propAccess trigger data $.trigger.order_id$.node.propAccess node data $.fetch_user.email$.node.nested.propNested property $.user.address.city$.node.array[0]First array item $.orders[0]$.node.array[-1]Last array item $.orders[-1]$.node.array[0].propProperty in array $.orders[0].total
Try It Yourself
Create a Test Flow
Set up a simple flow with a manual trigger
Add Test Data
Provide sample JSON data in the trigger form
Add a Node
Create a node that uses variable mapping to access trigger data
Run and Debug
Execute the flow and inspect the results in the debugger
What’s Next?