Skip to main content
One of the most powerful features in Activepieces is the ability to pass data between steps. This guide explains how to reference step outputs, use expressions, and map data throughout your workflow.

Understanding Step Outputs

Every step in your workflow produces an output that subsequent steps can access. The output structure depends on the step type.

Accessing Step Outputs

Use the mustache syntax {{ stepName.property }} to reference data from previous steps:
// If you have a step named "get_user" that returns:
{
  "id": "123",
  "name": "John Doe",
  "email": "john@example.com"
}

// Access the data in later steps:
{{ get_user.name }}          // "John Doe"
{{ get_user.email }}         // "john@example.com"
{{ get_user.id }}            // "123"
Step names are automatically generated as step_1, step_2, etc., but you can rename them to something more meaningful like get_user or send_email.

Variable Types

Activepieces supports several types of variables you can use in your workflows:

1. Step Outputs

Access data from any previous step:
{{ trigger.body.name }}           // Data from the trigger
{{ send_http.body.results }}      // HTTP response data
{{ transform_data.output }}        // Code action output

2. Loop Variables

When inside a loop, access iteration data:
// From packages/server/engine/test/handler/flow-looping.test.ts
{
  "type": "LOOP_ON_ITEMS",
  "settings": {
    "items": "{{ [4, 5, 6] }}"  // Array to iterate over
  },
  "firstLoopAction": {
    "name": "process_item",
    "settings": {
      "input": {
        "currentItem": "{{ loop.item }}",   // Current item
        "index": "{{ loop.index }}",         // Current index (1-based)
        "iteration": "{{ loop.iterations }}" // All iterations
      }
    }
  }
}

loop.item

The current item being processed

loop.index

Current iteration number (1-based)

loop.iterations

Array of all completed iterations

3. Connections

Reference saved connection credentials:
{{ connections.my_database.host }}
{{ connections.my_database.username }}

4. Router/Branch Data

Access branch evaluation results:
// Router output from packages/server/engine/test/handler/flow-branching.test.ts
{
  "router": {
    "output": {
      "branches": [
        {
          "branchIndex": 1,
          "branchName": "Test Branch",
          "evaluation": true  // Condition result
        }
      ]
    }
  }
}

Using Expressions

Activepieces supports JavaScript expressions within the mustache syntax:

String Operations

// Concatenation
{{ trigger.firstName + " " + trigger.lastName }}

// Template literals
{{ `Hello, ${trigger.name}!` }}

// String methods
{{ trigger.email.toLowerCase() }}
{{ trigger.name.toUpperCase() }}
{{ trigger.text.substring(0, 10) }}

Number Operations

// Arithmetic
{{ trigger.price * 1.1 }}           // Add 10%
{{ trigger.quantity + 5 }}
{{ (trigger.total - trigger.discount).toFixed(2) }}

// Comparisons
{{ trigger.age >= 18 }}
{{ trigger.score > 100 }}

Array Operations

// Map, filter, and more
{{ trigger.items.map(item => item.name) }}
{{ trigger.users.filter(user => user.active) }}
{{ trigger.numbers.reduce((sum, n) => sum + n, 0) }}

// Array access
{{ trigger.items[0] }}              // First item
{{ trigger.items.length }}          // Array length

Object Operations

// Access nested properties
{{ trigger.user.profile.address.city }}

// Spread operator
{{ { ...trigger.user, updated: true } }}

// Object methods
{{ Object.keys(trigger.data) }}
{{ Object.values(trigger.metadata) }}

Data Mapping Examples

Example 1: Transform API Response

1

HTTP Request Step

A step named fetch_users makes an API call:
{
  "status": 200,
  "body": {
    "users": [
      {"id": 1, "name": "Alice", "active": true},
      {"id": 2, "name": "Bob", "active": false},
      {"id": 3, "name": "Charlie", "active": true}
    ]
  }
}
2

Filter Active Users

Create a code action to filter:
export const code = async (inputs) => {
  const users = inputs.allUsers;
  return users.filter(user => user.active);
};
Input configuration:
{
  "allUsers": "{{ fetch_users.body.users }}"
}
3

Use Filtered Data

Reference the filtered results:
{{ filter_users.output }}  // [{id: 1, name: "Alice", ...}, ...]

Example 2: Loop Over Items

// Loop configuration from test suite
{
  "name": "process_orders",
  "type": "LOOP_ON_ITEMS",
  "settings": {
    "items": "{{ fetch_orders.body.results }}"  // Array from previous step
  },
  "firstLoopAction": {
    "name": "send_confirmation",
    "type": "PIECE",
    "settings": {
      "pieceName": "@activepieces/piece-gmail",
      "actionName": "send_email",
      "input": {
        "to": "{{ loop.item.customer.email }}",
        "subject": "Order #{{ loop.item.orderNumber }} Confirmed",
        "body": "Thank you for order #{{ loop.index }}!"
      }
    }
  }
}

Example 3: Conditional Data

Use ternary operators for conditional values:
// Set priority based on score
{{ trigger.score > 80 ? 'High' : trigger.score > 50 ? 'Medium' : 'Low' }}

// Default values
{{ trigger.name || 'Unknown' }}
{{ trigger.count ?? 0 }}

Testing Data Flow

When you test a workflow, you can inspect data at each step:
Click Test Flow to run your workflow with sample data.
// From packages/web/src/app/builder/flow-canvas/widgets/test-flow-widget.tsx
const { mutate: runFlow } = flowHooks.useTestFlowOrStartManualTrigger({
  flowVersionId: flowVersion.id,
  onUpdateRun: (response) => {
    // Each step's output is captured
    const steps = response.flowRun.steps;
    // { step_1: {output: ...}, step_2: {output: ...} }
  }
});

Sample Data

Steps can save sample data for testing:
// From packages/shared/src/lib/automation/flows/actions/action.ts
export const SampleDataSetting = {
  "sampleDataFileId": string,       // Saved sample output
  "sampleDataInputFileId": string,  // Saved sample input
  "lastTestDate": string,           // When last tested
  "currentSelectedData": object     // Current test data
}
Sample data is automatically saved when you test individual steps, making it easy to test downstream steps without re-running the entire flow.

Advanced Data Techniques

Working with JSON

// Parse JSON string
{{ JSON.parse(trigger.jsonString) }}

// Stringify object
{{ JSON.stringify(trigger.data) }}

// Pretty print
{{ JSON.stringify(trigger.data, null, 2) }}

Date and Time

// Current timestamp
{{ Date.now() }}

// Format date
{{ new Date(trigger.timestamp).toISOString() }}

// Date arithmetic
{{ new Date(Date.now() + 86400000).toISOString() }}  // Tomorrow

Error Handling in Expressions

// Safe property access
{{ trigger?.user?.profile?.email || 'no-email@example.com' }}

// Try-catch in code actions
export const code = async (inputs) => {
  try {
    return JSON.parse(inputs.data);
  } catch (error) {
    return { error: error.message };
  }
};

Common Patterns

Extract only the fields you need from a large object:
{
  "name": "{{ trigger.user.name }}",
  "email": "{{ trigger.user.email }}",
  "created": "{{ new Date().toISOString() }}"
}
Combine data from different steps:
{
  "userData": "{{ get_user.output }}",
  "orderData": "{{ get_orders.body }}",
  "timestamp": "{{ Date.now() }}"
}
Construct URLs from variables:
{{ `https://api.example.com/users/${trigger.userId}/orders?limit=10` }}
Include fields only if they exist:
{
  "name": "{{ trigger.name }}",
  ...{{ trigger.email ? { email: trigger.email } : {} }}
}

Best Practices

  1. Use Meaningful Step Names: Rename steps to reflect what they do (e.g., get_user instead of step_1)
  2. Test Data Flow: Always test with real data to verify expressions work correctly
  3. Handle Missing Data: Use null coalescing (??) or defaults for optional fields
  4. Keep Expressions Simple: Complex logic should go in code actions
  5. Document Complex Mappings: Add notes explaining non-obvious data transformations

Next Steps

Loops & Branches

Learn control flow and conditional logic

Code Actions

Write custom data transformations

Debugging

Debug data flow issues

Error Handling

Handle data errors gracefully