For-each node

Updated at:

A for-each node iterates over an upstream result set, such as a list of filenames or partitions, and executes the same subtask for each element. This eliminates the need to create individual tasks manually and enables dynamic, automated workflows.

Use cases

A for-each node enables parameterized execution when you need to apply the same analysis or processing logic to different business units, product lines, or configuration items. For example, if your company has multiple product lines and you need to generate a separate daily report for each, the processing logic is identical; only the target data differs.

Similar to a for loop in a programming language, a for-each node iterates over a list, such as table names, partition names, or filenames, and executes a predefined sub-workflow for each item.

Usage notes

  • Version requirements: Available only in DataWorks Standard Edition and later.

  • Permissions: Your RAM account must be added to the target workspace and assigned the developer or workspace administrator role. For more information, see Add members to a workspace.

How it works

The for-each node acts as a container that encapsulates a customizable sub-workflow, known as the loop body. It works as follows:

image
  1. Data input: The for-each node depends on an upstream assignment node or other assignable node (such as an EMR Hive node). It retrieves the array-formatted result set by binding to the loopDataArray parameter.

  2. Loop execution: When the node starts, it iterates through each element in the result set in order. For each element, it fully executes the inner loop body once, from the Start node to the End node.

    Note

    The Start and End nodes are not editable. They only mark the beginning and end of the loop body.

  3. Data passing: During each iteration, the value of the current element is passed to the nodes inside the loop body via built-in variables. The internal business nodes use ${dag.foreach.current} to access the data item being processed.

Built-in parameters

Important

Variables in the ${...} format are a template syntax specific to DataWorks. DataWorks directly parses these parameters and replaces them with their values before execution.

Nodes within the for-each loop body can use the following built-in variables to access the loop status and data:

Built-in parameter

Description

For loop analogy

${dag.loopDataArray}

The complete result set passed from the upstream assignment node.

Consider the following for loop code:

for(int i=0;i<data.length;i++) {
   print(data[i]);
}
  • ${dag.loopDataArray} corresponds to data.

  • ${dag.foreach.current} corresponds to data[i].

  • ${dag.offset} corresponds to i.

  • ${dag.loopTimes} corresponds to i+1.

${dag.foreach.current}

The data item being processed in the current iteration.

${dag.offset}

The current loop offset (0-indexed).

${dag.loopTimes}

The current loop count (1-indexed).

If the upstream output is a two-dimensional array, such as a SQL query result, you can also use the following syntax to access specific values:

Other parameters

Description

${dag.foreach.current}

Gets a string by separating the elements of the current data row (a one-dimensional array) with a comma ,.

${dag.foreach.current[n]}

The n-th item from the current data row.

${dag.loopDataArray[i][j]}

The data from the i-th row and j-th column of the entire result set.

The for-each node does not currently support nested loops. This example is for value retrieval demonstration only.

Limitations

  • Execution mechanism: The loop supports both serial execution and parallel execution. You can choose parallel execution when the iterations are independent of each other.

  • Loop limit: The default maximum number of loops is 128, which can be adjusted up to 1024.

  • Debugging constraints: You cannot run a for-each node directly in Data Studio. You must deploy the task and then test it in Operation Center by using the smoke testing feature.

  • Execution constraints: A for-each node cannot be run in isolation. This includes smoke testing, backfill, and manual runs.

  • Flow control in the loop body: If you use a branch node inside a for-each loop body, you must ensure all branches eventually converge at a single merge node before connecting to the End node. This guarantees the logical integrity of the loop body.

  • Rerun constraints: After a node is deployed, an automatic rerun on failure resumes from the point of failure. However, a manual rerun triggers a complete rerun of the entire for-each node.

Procedure

This procedure uses an assignment node as the upstream node and a Shell node inside the loop body to print the results. It walks through configuring a complete for-each task:

  1. Prepare the upstream data (configure an assignment node)

    Create and configure an assignment node to provide an iterable result set for the downstream for-each node.

    1. In the workflow, create an assignment node (for example, assign) and place it upstream of the for-each node.

    2. Double-click the assignment node and select a Python 2 environment. For example, use Python 2 to output an array with four elements:

      The node outputs [10,20,30,40] to downstream nodes by automatically splitting the last output line into an array at each comma.
      print "10,20,30,40"
    3. The assignment node automatically generates an output parameter named outputs, which represents its result set.

    4. Save the assignment node.

  2. Configure the for-each node to consume data

    Configure the for-each node to receive the upstream data and use it within its loop body.

    1. Double-click the for-each node to open its internal canvas.

    2. In the Scheduling panel on the right, find the loopDataArray parameter under Scheduling Parameters and click Bind.

      Select the outputs parameter of the assign node to create the binding. After the binding is complete, the value of the loopDataArray parameter reflects its bound status.

    3. In the dialog box that appears, set the Value Source to the upstream assignment node (assign) and select its outputs parameter. This action automatically creates a dependency between the two nodes.

    4. In the for-each loop body, click Create Internal Node and create a Shell node.

      In a real-world scenario, you can configure any type of node.
    5. Double-click the new Shell node and use built-in variables in the code to retrieve and print information about the loop:

      #!/bin/bash
      # Use ${dag.loopTimes} to get the current loop count
      echo "Current loop number is: ${dag.loopTimes}"
      # Use ${dag.foreach.current} to get the data item for the current iteration
      echo "Current item is: ${dag.foreach.current}"
    6. (Optional) In the Scheduling Settings panel on the right, configure properties under Scheduling Policy.

      • Maximum Number of Loops: The default is 128, and the maximum is 1024.

        Important

        This parameter determines the maximum number of iterations for the loop body. If the number of upstream data items is large, increase this value to ensure all items are processed.

      • Execute Policy: Select Serial for this example.

        • Serial: Runs iterations sequentially.

        • Parallel: Runs loop iterations concurrently to improve task efficiency. In Parallel mode, if one iteration fails, it does not affect other iterations. The scheduler attempts to run all iterations to completion. The default concurrency is 5, and the maximum is 20.

    7. Save the Shell node.

  3. Deploy, run, and verify

    Deploy the workflow to Operation Center for execution and verify the results of the for-each node.

    1. Return to the main workflow canvas and click the Deploy button on the toolbar to publish the entire workflow.

    2. Go to Node O&M > Auto Triggered Task O&M > Auto Triggered Task and perform a smoke test on the target workflow.

      Important

      Do not perform a smoke test on the for-each node individually. Because the for-each node depends on the output of the upstream assignment node, you must start the test from the assignment node to ensure the data lineage is complete.

    3. After the test instance runs successfully, find the for-each node instance in the list, open it, and right-click to select View Internal Nodes.

    4. In the internal node view, check the Shell node instances generated by each loop. Open the running log of any instance to view the output for that iteration and verify that the output is correct.

      The left panel shows that all four loop iterations are complete. The running log for the fourth iteration outputs Current loop number is: 4 and Current item is: 40, and the Shell command exits with code 0, indicating successful execution.

Note

In addition to using a traditional assignment node as the upstream node, a for-each node also supports achieving the same iteration effect through the assignment parameter feature of an upstream SQL node. For node types that support assignment parameters, such as EMR Hive, Hologres SQL, EMR Spark SQL, AnalyticDB for PostgreSQL, ClickHouse SQL, and MySQL, you can add an assignment parameter in the Node Context Parameters > Output Parameters of This Node section.

Use case: Process different data formats

Scenario 1: Process a one-dimensional array

  • assignment node output: 2025-11-01,2025-11-02,2025-11-03

  • Iteration count: 3

  • During the second iteration:

    • The value of ${dag.foreach.current} is 2025-11-02.

    • The value of ${dag.loopTimes} is 2.

Scenario 2: Process a two-dimensional array

  • assignment node (MaxCompute SQL) output:

    +-----+----------+
    | id  | city     |
    +-----+----------+
    | 101 | beijing  |
    | 102 | shanghai |
    +-----+----------+
  • Iteration count: 2

  • During the second iteration:

    • The value of ${dag.foreach.current} is 102,shanghai.

    • The value of ${dag.loopTimes} is 2.

    • The value of ${dag.foreach.current[0]} is 102.

    • The value of ${dag.foreach.current[1]} is shanghai.

Scenario: Batch process partition table data across multiple business lines

This example demonstrates how to use an assignment node and a for-each node to batch process user behavior data across multiple business lines, automating data processing with a single set of logic that serves multiple product lines.

image

Background

Assume you are a data development engineer at a comprehensive internet company, responsible for processing data from three core business lines: e-commerce (ecom), finance (finance), and logistics (logistics), with the possibility of adding more in the future. You need to run the same aggregation logic on user behavior logs from these three business lines every day to calculate daily page views (PV) per user and store the results in a unified aggregate table.

  • Upstream source tables (DWD layer):

    • dwd_user_behavior_ecom_d: E-commerce user behavior table.

    • dwd_user_behavior_finance_d: Finance user behavior table.

    • dwd_user_behavior_logistics_d: Logistics user behavior table.

    • dwd_user_behavior_${business_line}_d: User behavior tables for more potential business lines in the future.

    • These tables have the same schema and are partitioned by day (dt).

  • Downstream target table (DWS layer):

    • dws_user_summary_d: User aggregate table.

    • This table is double-partitioned by business line (biz_line) and day (dt) to store the aggregated results from all business lines in a unified manner.

Creating a separate task for each business line results in high maintenance costs and is error-prone. With a for-each node, you maintain a single set of processing logic, and the system automatically iterates through all business lines to complete the computation.

Data preparation

First, create the sample tables and insert test data (using business date 20251010 as an example).

  1. Associate a compute resource with the workspace.

  2. Go to Data Studio for data development and create a MaxCompute SQL node.

  3. Create the source tables (DWD layer): Add the following code to the MaxCompute SQL node and run it.

    -- E-commerce user behavior table
    CREATE TABLE IF NOT EXISTS dwd_user_behavior_ecom_d (
        user_id     STRING COMMENT 'User ID',
        action_type STRING COMMENT 'Action type',
        event_time  BIGINT COMMENT 'Event timestamp in milliseconds (Unix)'
    ) 
    COMMENT 'E-commerce user behavior log detail table'
    PARTITIONED BY (dt STRING COMMENT 'Date partition, format yyyymmdd');
    INSERT OVERWRITE TABLE dwd_user_behavior_ecom_d PARTITION (dt='20251010') VALUES
    ('user001', 'click',        1760004060000), -- 2025-10-10 10:01:00.000
    ('user002', 'browse',       1760004150000), -- 2025-10-10 10:02:30.000
    ('user001', 'add_to_cart',  1760004300000); -- 2025-10-10 10:05:00.000
    -- Verify e-commerce user behavior table created successfully
    SELECT * FROM dwd_user_behavior_ecom_d where dt='20251010';
    -- Finance user behavior table
    CREATE TABLE IF NOT EXISTS dwd_user_behavior_finance_d (
        user_id     STRING COMMENT 'User ID',
        action_type STRING COMMENT 'Action type',
        event_time  BIGINT COMMENT 'Event timestamp in milliseconds (Unix)'
    ) 
    COMMENT 'Finance user behavior log detail table'
    PARTITIONED BY (dt STRING COMMENT 'Date partition, format yyyymmdd');
    INSERT OVERWRITE TABLE dwd_user_behavior_finance_d PARTITION (dt='20251010') VALUES
    ('user003', 'open_app',      1760020200000), -- 2025-10-10 14:30:00.000
    ('user003', 'transfer',      1760020215000), -- 2025-10-10 14:30:15.000
    ('user003', 'check_balance', 1760020245000), -- 2025-10-10 14:30:45.000
    ('user004', 'open_app',      1760020300000); -- 2025-10-10 14:31:40.000
    -- Verify finance user behavior table created successfully
    SELECT * FROM dwd_user_behavior_finance_d where dt='20251010';
    -- Logistics user behavior table
    CREATE TABLE IF NOT EXISTS dwd_user_behavior_logistics_d (
        user_id     STRING COMMENT 'User ID',
        action_type STRING COMMENT 'Action type',
        event_time  BIGINT COMMENT 'Event timestamp in milliseconds (Unix)'
    ) 
    COMMENT 'Logistics user behavior log detail table'
    PARTITIONED BY (dt STRING COMMENT 'Date partition, format yyyymmdd');
    INSERT OVERWRITE TABLE dwd_user_behavior_logistics_d PARTITION (dt='20251010') VALUES
    ('user001', 'check_status',    1760032800000), -- 2025-10-10 18:00:00.000
    ('user005', 'schedule_pickup', 1760032920000); -- 2025-10-10 18:02:00.000
    -- Verify logistics user behavior table created successfully
    SELECT * FROM dwd_user_behavior_logistics_d where dt='20251010';
  4. Create the target table (DWS layer): Add the following code to the MaxCompute SQL node and run it.

    CREATE TABLE IF NOT EXISTS dws_user_summary_d (
        user_id     STRING COMMENT 'User ID',
        pv          BIGINT COMMENT 'Daily activity count'
    ) 
    COMMENT 'User daily activity summary table'
    PARTITIONED BY (
        dt           STRING COMMENT 'Date partition, format yyyymmdd',
        biz_line     STRING COMMENT 'Business line partition, e.g. ecom, finance, logistics'
    );
    Important

    If the workspace uses the standard mode, you need to deploy this node to the production environment and backfill data.

Workflow implementation

  1. Create a workflow. In the Scheduling Parameters section on the right side, set the scheduling parameter bizdate to the previous day: $[yyyymmdd-1].

  2. In the workflow, create an assignment node named get_biz_list and write the following code in MaxCompute SQL. This node outputs the list of business lines to be processed:

    -- Output all business lines to be processed
    SELECT 'ecom' AS biz_line
    UNION ALL
    SELECT 'finance' AS biz_line
    UNION ALL
    SELECT 'logistics' AS biz_line;
  3. Configure the for-each node

    • Go back to the workflow page and create a downstream for-each node for the assignment node get_biz_list.

    • Open the for-each node settings page. In the Scheduling Parameters > Script Parameters section under schedule settings on the right side, bind the loopDataArray parameter to the outputs of the get_biz_list node.

    • In the loop body of the for-each node, click Create Internal Node and create a MaxCompute SQL node. Write the processing logic inside the loop body.

      Note
      • This script is driven by the for-each node and is executed once for each business line.

      • The built-in variable ${dag.foreach.current} is dynamically replaced with the current business line name at each iteration. The expected iteration values are: 'ecom', 'finance', 'logistics'.

      SET odps.sql.allow.dynamic.partition=true;
      INSERT OVERWRITE TABLE dws_user_summary_d PARTITION (dt='${bizdate}', biz_line)
      SELECT
          user_id,
          COUNT(*) AS pv,
          '${dag.foreach.current}' AS biz_line
      FROM
          dwd_user_behavior_${dag.foreach.current}_d
      WHERE
          dt = '${bizdate}'
      GROUP BY
          user_id;
  4. Add a verification node

    Go back to the workflow. Click Create Downstream on the for-each node to create a MaxCompute SQL node and add the following code.

    SELECT * FROM dws_user_summary_d WHERE dt='20251010' ORDER BY biz_line, user_id;

Deployment and results

Deploy the workflow to the production environment. Go to the Auto Triggered Task O&M > Auto Triggered Task page in Operation Center. Find the target workflow and perform smoke testing with the business date set to '20251010'.

After the run is complete, view the runtime log in the test instance. The expected output of the final node is as follows:

user_id

pv

dt

biz_line

user001

2

20251010

ecom

user002

1

20251010

ecom

user003

3

20251010

finance

user004

1

20251010

finance

user001

1

20251010

logistics

user005

1

20251010

logistics

Advantages

  • High scalability: To add a new business line, add one line of SQL in the assignment node without modifying the processing logic.

  • Easy maintenance: All business lines share one set of processing logic. A single modification applies to all.

FAQ

  • Q: Why can't I run a for-each node directly in Data Studio to test it?

    A: This is by design. The node requires a full scheduling environment to resolve the node context and its dependencies, so it does not support direct execution in Data Studio. You must deploy the task to Operation Center and test it by using backfill or triggering a scheduled run.

  • Q: Why does a smoke test on an individual for-each node fail or do nothing?

    A: The loop data for a for-each node comes from its loopDataArray input parameter, which must be bound to the outputs parameter of an upstream assignment node. If you run the for-each node by itself, it will either fail or be skipped because it cannot receive an input result set.

  • Q: Why does my loop run only once? 

    A: This usually happens because the output from the upstream assignment node is parsed as a single element. Check your output:

    • 1. Is it a single string without a delimiter?

    • 2. If you expect to iterate over multiple items, ensure they are separated by commas (,). For example, 'item1,item2,item3' results in three loops, whereas 'item1 item2 item3' results in only one.