Skip to main content
Control flow lets you create dynamic workflows that can iterate over data and make decisions based on conditions. Activepieces provides two powerful control flow actions: Loops and Routers.

Loop Over Items

The Loop action iterates over an array and executes actions for each item. This is perfect for processing lists, bulk operations, and batch processing.

Basic Loop Structure

// From packages/shared/src/lib/automation/flows/actions/action.ts
{
  "name": "loop",
  "type": "LOOP_ON_ITEMS",
  "displayName": "Loop Over Items",
  "settings": {
    "items": "{{ trigger.items }}"  // Array to iterate over
  },
  "firstLoopAction": {
    // Actions to execute for each item
  }
}
1

Add a Loop Action

Click the + button and select Loop Over Items.
2

Configure Items Array

Set the items to loop over. This can be:
  • Data from a previous step: {{ get_data.body.results }}
  • A literal array: {{ [1, 2, 3, 4, 5] }}
  • An expression: {{ trigger.users.filter(u => u.active) }}
3

Add Actions Inside Loop

Click inside the loop to add actions that will execute for each item.

Loop Variables

Inside a loop, you have access to special variables:
// Current item being processed
{{ loop.item }}

// Access item properties
{{ loop.item.name }}
{{ loop.item.email }}
{{ loop.item.id }}

Loop Examples

Loop over a list of users and send each one an email:
{
  "name": "email_loop",
  "type": "LOOP_ON_ITEMS",
  "settings": {
    "items": "{{ get_users.body.users }}"
  },
  "firstLoopAction": {
    "name": "send_email",
    "type": "PIECE",
    "settings": {
      "pieceName": "@activepieces/piece-gmail",
      "actionName": "send_email",
      "input": {
        "to": "{{ loop.item.email }}",
        "subject": "Hello {{ loop.item.name }}!",
        "body": "You are user #{{ loop.index }}"
      }
    }
  }
}

Loop Output

When a loop completes, it produces an output with all iterations:
// From packages/server/engine/test/handler/flow-looping.test.ts
{
  "output": {
    "iterations": [
      {
        "index": 1,
        "item": 4,
        "send_email": { "output": { /* step output */ } }
      },
      {
        "index": 2,
        "item": 5,
        "send_email": { "output": { /* step output */ } }
      },
      {
        "index": 3,
        "item": 6,
        "send_email": { "output": { /* step output */ } }
      }
    ],
    "index": 3,      // Total iterations
    "item": 6        // Last item
  }
}
You can access loop results in subsequent steps using {{ loop_name.output.iterations }}

Conditional Branches (Router)

The Router action lets you create conditional branches in your workflow, executing different actions based on conditions.

Router Structure

// From packages/shared/src/lib/automation/flows/actions/action.ts
{
  "name": "router",
  "type": "ROUTER",
  "displayName": "Branch",
  "settings": {
    "executionType": "EXECUTE_FIRST_MATCH",  // or "EXECUTE_ALL_MATCH"
    "conditions": [[
      {
        "firstValue": "{{ trigger.status }}",
        "operator": "TEXT_EXACTLY_MATCHES",
        "secondValue": "completed",
        "caseSensitive": false
      }
    ]]
  },
  "children": [
    // Branch 1: When condition is true
    { /* actions */ },
    // Branch 2: Fallback (when condition is false)
    { /* actions */ }
  ]
}
1

Add Router Action

Click the + button and select Router (or Branch).
2

Configure Conditions

Set up conditions that determine which branch to execute.
3

Add Branch Actions

Add actions to each branch path.

Branch Operators

Activepieces supports many condition operators:

Text Operators

  • Contains
  • Does not contain
  • Exactly matches
  • Starts with
  • Ends with

Number Operators

  • Equal to
  • Greater than
  • Less than

Boolean Operators

  • Is true
  • Is false

Existence Operators

  • Exists
  • Does not exist

Branch Operators Reference

// From packages/shared/src/lib/automation/flows/actions/action.ts
export enum BranchOperator {
  TEXT_CONTAINS = 'TEXT_CONTAINS',
  TEXT_DOES_NOT_CONTAIN = 'TEXT_DOES_NOT_CONTAIN',
  TEXT_EXACTLY_MATCHES = 'TEXT_EXACTLY_MATCHES',
  TEXT_DOES_NOT_EXACTLY_MATCH = 'TEXT_DOES_NOT_EXACTLY_MATCH',
  TEXT_STARTS_WITH = 'TEXT_START_WITH',
  TEXT_DOES_NOT_START_WITH = 'TEXT_DOES_NOT_START_WITH',
  TEXT_ENDS_WITH = 'TEXT_ENDS_WITH',
  TEXT_DOES_NOT_END_WITH = 'TEXT_DOES_NOT_END_WITH',
  NUMBER_IS_GREATER_THAN = 'NUMBER_IS_GREATER_THAN',
  NUMBER_IS_LESS_THAN = 'NUMBER_IS_LESS_THAN',
  NUMBER_IS_EQUAL_TO = 'NUMBER_IS_EQUAL_TO',
  BOOLEAN_IS_TRUE = 'BOOLEAN_IS_TRUE',
  BOOLEAN_IS_FALSE = 'BOOLEAN_IS_FALSE',
  EXISTS = 'EXISTS',
  DOES_NOT_EXIST = 'DOES_NOT_EXIST',
  // ... and more
}

Execution Types

Execute only the first branch whose condition is true:
{
  "executionType": "EXECUTE_FIRST_MATCH"
}
Use this when you want if-else behavior.

Branch Examples

Route based on order status:
// From packages/server/engine/test/handler/flow-branching.test.ts
{
  "name": "router",
  "type": "ROUTER",
  "settings": {
    "executionType": "EXECUTE_FIRST_MATCH",
    "conditions": [[
      {
        "firstValue": "{{ trigger.order.status }}",
        "operator": "TEXT_EXACTLY_MATCHES",
        "secondValue": "completed",
        "caseSensitive": false
      }
    ]]
  },
  "children": [
    // Branch 1: Completed orders
    {
      "name": "send_confirmation",
      "type": "PIECE",
      "settings": {
        "pieceName": "@activepieces/piece-gmail",
        "actionName": "send_email",
        "input": {
          "to": "{{ trigger.order.customerEmail }}",
          "subject": "Order Completed"
        }
      }
    },
    // Branch 2: Fallback for other statuses
    {
      "name": "send_status_update",
      "type": "PIECE",
      "settings": {
        "pieceName": "@activepieces/piece-gmail",
        "actionName": "send_email",
        "input": {
          "to": "admin@example.com",
          "subject": "Order Status: {{ trigger.order.status }}"
        }
      }
    }
  ]
}

Router Output

// From packages/server/engine/test/handler/flow-branching.test.ts
{
  "router": {
    "output": {
      "branches": [
        {
          "branchIndex": 1,
          "branchName": "High Priority",
          "evaluation": true  // This branch was executed
        }
      ]
    }
  }
}

Combining Loops and Branches

You can nest loops inside branches and vice versa:

Loop with Conditional Logic

{
  "name": "process_users",
  "type": "LOOP_ON_ITEMS",
  "settings": {
    "items": "{{ get_users.body.users }}"
  },
  "firstLoopAction": {
    "name": "check_status",
    "type": "ROUTER",
    "settings": {
      "executionType": "EXECUTE_FIRST_MATCH",
      "conditions": [[
        {
          "firstValue": "{{ loop.item.active }}",
          "operator": "BOOLEAN_IS_TRUE"
        }
      ]]
    },
    "children": [
      // Active users: send welcome
      {
        "name": "send_welcome",
        "type": "PIECE",
        "settings": { /* ... */ }
      },
      // Inactive users: send reactivation
      {
        "name": "send_reactivation",
        "type": "PIECE",
        "settings": { /* ... */ }
      }
    ]
  }
}

Branch with Loops

{
  "name": "order_router",
  "type": "ROUTER",
  "settings": {
    "conditions": [[
      {
        "firstValue": "{{ trigger.orderType }}",
        "operator": "TEXT_EXACTLY_MATCHES",
        "secondValue": "bulk"
      }
    ]]
  },
  "children": [
    // Bulk orders: loop through items
    {
      "name": "bulk_loop",
      "type": "LOOP_ON_ITEMS",
      "settings": {
        "items": "{{ trigger.items }}"
      },
      "firstLoopAction": { /* process each item */ }
    },
    // Single orders: process directly
    {
      "name": "process_single",
      "type": "PIECE",
      "settings": { /* ... */ }
    }
  ]
}

Skipping Control Flow

You can skip loops and routers conditionally:
// From packages/server/engine/test/handler/flow-looping.test.ts
{
  "name": "conditional_loop",
  "type": "LOOP_ON_ITEMS",
  "skip": "{{ trigger.skipProcessing }}",  // Dynamic skip
  "settings": {
    "items": "{{ trigger.items }}"
  }
}
When a loop or router is skipped, it produces no output and subsequent steps cannot reference its results.

Error Handling in Control Flow

Loop Error Handling

// From packages/server/engine/test/handler/flow-looping.test.ts
// If a step fails inside a loop:
{
  "output": {
    "iterations": [
      {
        "index": 1,
        "item": 4,
        "process_item": {
          "status": "FAILED",
          "errorMessage": "Custom Runtime Error"
        }
      }
    ],
    "index": 1,  // Stopped at first iteration
    "item": 4
  }
}
By default, loops stop on the first error. Use error handling options to continue:
{
  "firstLoopAction": {
    "name": "risky_step",
    "settings": {
      "errorHandlingOptions": {
        "continueOnFailure": {
          "value": true  // Continue loop even if this step fails
        }
      }
    }
  }
}

Best Practices

Be mindful of loop size. Processing 1000+ items can take time and consume resources.
// Filter before looping
{{ trigger.items.filter(item => item.needsProcessing).slice(0, 100) }}
Name your router actions to describe the decision being made:
"displayName": "Route by Priority Level"  // Good
"displayName": "Branch"                   // Bad
Test your control flow with:
  • Empty arrays for loops
  • Null/undefined values in conditions
  • Both branches of routers
If branches become complex, consider splitting into separate flows and using subflows.
Use notes to explain why certain branches or loops exist.

Common Patterns

Filter-Then-Loop

// Code action to filter
{
  "name": "filter_data",
  "type": "CODE",
  "settings": {
    "sourceCode": {
      "code": "export const code = async (inputs) => inputs.items.filter(i => i.active);"
    },
    "input": {
      "items": "{{ trigger.items }}"
    }
  }
}

// Then loop over filtered results
{
  "name": "process_filtered",
  "type": "LOOP_ON_ITEMS",
  "settings": {
    "items": "{{ filter_data.output }}"
  }
}

Priority-Based Routing

// Multiple conditions checked in order
{
  "name": "priority_router",
  "type": "ROUTER",
  "settings": {
    "executionType": "EXECUTE_FIRST_MATCH"
  },
  "children": [
    // Check high priority first
    { /* condition: score > 90 */ },
    // Then medium priority
    { /* condition: score > 70 */ },
    // Fallback: low priority
    { /* no condition - always matches */ }
  ]
}

Next Steps

Error Handling

Handle errors in loops and branches

Passing Data

Learn more about data flow

Debugging

Debug control flow issues

Best Practices

Optimize your workflows