specification icon indicating copy to clipboard operation
specification copied to clipboard

Break Task Implementation for Loop Control

Open nyamathshaik opened this issue 7 months ago • 14 comments

What would you like to be added?

Introduce native support for a break task to provide enhanced control within looping constructs (e.g., for). This allows workflow authors to exit the innermost loop early based on a condition or directly.

Proposal(s):

Currently, exiting from a loop prematurely requires workarounds such as manipulating the loop condition externally or using switches and transitions, which makes workflow logic harder to maintain and less readable.

Proposal:

  • Add a native break task type to the Serverless Workflow specification.

  • Support simple unconditional breaks and conditional breaks based on JQ expressions.

  • Allow break tasks to be used within any loop

  • Execution engine should honor the break directive and exit the innermost loop immediately when invoked.

  • Optional: Validate that break can only be used inside looping contexts.

Example I

do:
  - for_loop:
      for:
        in: "[1, 2, 3, 4, 5]"
        each: "item"
      do:
        - check_condition:
            switch:
              - case:
                  when: "$item > 3"
                  then: "break_loop"
              - default:
                  then: "continue_processing"
        - break_loop:
            break: true
        - continue_processing:
            do:
              # other tasks

Example II

do:
  - for_loop:
      for:
        in: "[1, 2, 3, 4, 5]"
        each: "item"
      do:
        - break_loop:
            break:
              condition: "$item > 3"
        - process_item:
            do:
              # your logic here

Alternative(s):

Currently, there is no direct way to exit an inner loop once execution has entered it.

Additional info:

This feature would greatly simplify:

  • Data scanning with early exits

  • Threshold-based batching

  • Custom pagination and retry logic with controlled exits

Let me know if you'd like me to open a PR to help explore this idea further! @ricardozanini @cdavernas

Community Notes

  • Please vote by adding a 👍 reaction to the feature to help us prioritize.
  • If you are interested to work on this feature, please leave a comment.

nyamathshaik avatar May 06 '25 11:05 nyamathshaik

Hello @nyamathshaik

There is actually already a way to exit a block, it's the exit flow direct.

do:
  - for_loop:
      for:
        in: "[1, 2, 3, 4, 5]"
        each: "item"
      do:
        - break_loop:
            if: "$item > 3"
            then: exit
        - process_item:
            do:
              # your logic here

Am I missing something ?

JBBianchi avatar May 06 '25 16:05 JBBianchi

@JBBianchi is right, the exit directive achieves what you are proposing.

cdavernas avatar May 06 '25 17:05 cdavernas

@JBBianchi @cdavernas No, we can't simply use EXIT as a replacement for a dedicated break task.

Here's why: Different scope:

  • EXIT is a transition type that exits the entire current task list execution
  • break specifically exits only the innermost loop in which it's positioned

Different behavior in nested loops:

  • If we have nested loops and want to break out of only the inner loop, EXIT would exit both loops
  • The break task allows for more precise control flow by only breaking the innermost loop

Implementation details:

  • EXIT is handled at the task list execution level, not within loop handlers
  • Our break implementation specifically works with loop handlers to only break the current loop iteration

Usage pattern:

  • EXIT requires using the "then" property on a task, which changes the transition flow
  • break is a standalone task type that's more semantically clear for its purpose
  • The break task provides a more granular and intuitive control mechanism specifically for loops, while EXIT is a more general transition mechanism for the entire task list.

nyamathshaik avatar May 07 '25 05:05 nyamathshaik

If we have nested loops and want to break out of only the inner loop, EXIT would exit both loops

The initial claim made above isn't accurate. You can achieve the behavior you're aiming for using the existing then: exit directive, similar to your break construct.

I think that the doc clearly expresses that:

Completes the current scope's execution


The first example provided seems poorly structured: declaring tasks that would never run (those after a break) does not make sense AFAIK, as it does not make any sense to declare a task you have chosen to declaratively and explicitly never run. In typical code scenarios, IDEs would flag such unreachable tasks.

Here's a clearer version of your second example using then: exit:

do:
  - for_loop:
      for:
        in: "[1, 2, 3, 4, 5]"
        each: "item"
      do:
        - break_loop:
            if: "$item > 3"
            then: exit
        - process_item:
            do:
              # your logic here

Regarding your proposed alternative, I see several concerns:

  1. The existing exit directive already accomplishes the goal more elegantly.
  2. Introducing a contextual no-operation task diverges from consistency and clarity of task definitions.
  3. Such an addition might complicate the system unnecessarily and confuse users.

cdavernas avatar May 07 '25 06:05 cdavernas

@cdavernas @JBBianchi @ricardozanini I was trying to check if this works on synapse looks like it doesn't

I have tried both below and it doesn't break the loop.

  • for -> if/switch -> break

  • for -> do -> break

Image

Image

document:
  dsl: 1.0.0-alpha5
  namespace: default
  name: new-workflow
  version: 0.2.32
do:
- initialize:
    run:
      script:
        language: js
        code: 'return { items: [1, 2, 3, 4, 5], processed: [], shouldBreak: false };'
- forLoop:
    for:
      each: item
      in: ${[1, 2, 3, 4, 5]}
      at: index
    do:
    - processItem:
        run:
          script:
            language: js
            code: 'return { items: [1, 2, 3, 4, 5], processed: [], shouldBreak: false };'
            arguments:
              item: ${$item}
              index: ${$index}
    - conditionalBreak:
        switch:
        - case:
            when: ${1 != 1}
            then: breakForLoop
        - default:
            then: continueForLoop
    - breakForLoop:
        if: "true"
        run:
          script:
            language: js
            code: 'return { items: [1, 2, 3, 4, 5], processed: [], shouldBreak: false };'
        then: exit
    - continueForLoop:
        run:
          script:
            language: js
            code: return 1;
            arguments:
              data: ${.}

document:
  dsl: 1.0.0-alpha5
  namespace: default
  name: new-workflow
  version: 0.2.33
do:
- initialize:
    run:
      script:
        language: js
        code: 'return { items: [1, 2, 3, 4, 5], processed: [], shouldBreak: false };'
- forLoop:
    for:
      each: item
      in: ${[1, 2, 3, 4, 5]}
      at: index
    do:
    - processItem:
        run:
          script:
            language: js
            code: 'return { items: [1, 2, 3, 4, 5], processed: [], shouldBreak: false };'
            arguments:
              item: ${$item}
              index: ${$index}
    - breakForLoop:
        if: "true"
        run:
          script:
            language: js
            code: 'return { items: [1, 2, 3, 4, 5], processed: [], shouldBreak: false };'
        then: exit
    - continueForLoop:
        run:
          script:
            language: js
            code: return 1;
            arguments:
              data: ${.}

please let me know if I am missing something here.

nyamathshaik avatar May 27 '25 08:05 nyamathshaik

@nyamathshaik it might be a bug then, as your second example should work. The first example does not define an exit directive, so it will not work.

On a side note, the second example defines an if with true, which serves no purpose.

cdavernas avatar May 27 '25 08:05 cdavernas

I think there might be a bug indeed with exit in Synapse.

For example, using this flow with an end directive causes the conditionalBreak task to exit the loop and end the flow (as expected):

document:
  dsl: 1.0.0-alpha5
  namespace: default
  name: new-workflow
  version: 0.1.11
do:
- initialize:
    set:
      items: [5,4,3,2,1]
      processed: []
      continued: []
      shouldBreak: false
- forLoop:
    for:
      each: item
      in: .items
      at: index
    do:
    - processItem:
        set:
          items: ${ .items }
          processed: ${ .processed + [ $item ] }
          shouldBreak: ${ .shouldBreak }
          continued: $ { .continued }
    - conditionalBreak:
        if: ${ .processed | any(. == 3) }
        set:
          output: ${ . }
        export:
          as: ${ .output }
        then: end
    - continueLoop:
        set:
          items: ${ .items }
          processed: ${ .processed }
          shouldBreak: ${ .shouldBreak }
          continued: $ { .continued + [ $index ] }

Image

But when I use the exit flow directive, it only ends the current iteration and doesn't exit the loop. It's like a continue in programming, whereas I was expecting a break:

document:
  dsl: 1.0.0-alpha5
  namespace: default
  name: new-workflow
  version: 0.1.11
do:
- initialize:
    set:
      items: [5,4,3,2,1]
      processed: []
      continued: []
      shouldBreak: false
- forLoop:
    for:
      each: item
      in: .items
      at: index
    do:
    - processItem:
        set:
          items: ${ .items }
          processed: ${ .processed + [ $item ] }
          shouldBreak: ${ .shouldBreak }
          continued: $ { .continued }
    - conditionalBreak:
        if: ${ .processed | any(. == 3) }
        set:
          output: ${ . }
        export:
          as: ${ .output }
        then: exit
    - continueLoop:
        set:
          items: ${ .items }
          processed: ${ .processed }
          shouldBreak: ${ .shouldBreak }
          continued: $ { .continued + [ $index ] }

Image

JBBianchi avatar May 27 '25 10:05 JBBianchi

Hi @cdavernas @JBBianchi,

One issue I’ve observed with the current then: exit approach is that it exits the current do scope only, not the entire for loop. This becomes a problem when the exit is triggered inside a nested do block that sits outside the actual for loop scope—it won't actually break the loop as intended.

For example:

  dsl: 1.0.0-alpha5
  namespace: default
  name: new-workflow
  version: 0.1.11
do:
- initialize:
    set:
      items: [5,4,3,2,1]
      processed: []
      continued: []
      shouldBreak: false

- forLoop:
    for:
      each: item
      in: .items
      at: index
    do:
    - processItem:
        set:
          items: ${ .items }
          processed: ${ .processed + [ $item ] }
          shouldBreak: ${ .shouldBreak }
          continued: ${ .continued }

    - do:
        - conditionalBreak:
            if: ${ .processed | any(. == 3) }
            set:
              output: ${ . }
            export:
              as: ${ .output }
            then: exit

    - continueLoop:
        set:
          items: ${ .items }
          processed: ${ .processed }
          shouldBreak: ${ .shouldBreak }
          continued: ${ .continued + [ $index ] }

In this case, even though the conditionalBreak is met, the loop continues because exit only affects the inner do block and not the forLoop itself.

So I’d like to revisit the original proposal: introducing a dedicated breakTask. I believe this would provide more intuitive and predictable control flow within loops.

What do you think?

nyamathshaik avatar May 28 '25 05:05 nyamathshaik

@nyamathshaik As mentionned before, I'm strongly against a noop task that has limited contextual purpose: it goes against the concept of a task and is confusing.

In addition, what you observed on Synapse is, as mentionned, a bug: the exit directive should break the loop.

However, I believe the bug has demonstrated the potential need for a new flow directive that achieves the equivalent of the current bugged behavior: skip the current iteration.

cdavernas avatar May 28 '25 06:05 cdavernas

@cdavernas Yeah, I understand that its a bug on synapse where then:exit is currently behaving like a continue instead of a break

However, if even we fix the bug, then:exit can still be not used as a break for scenarios like "when the exit is triggered inside a nested do block that sits outside the actual for loop scope"

nyamathshaik avatar May 28 '25 08:05 nyamathshaik

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] avatar Jul 13 '25 00:07 github-actions[bot]

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] avatar Aug 28 '25 00:08 github-actions[bot]

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] avatar Oct 13 '25 00:10 github-actions[bot]

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] avatar Nov 28 '25 00:11 github-actions[bot]