So far, all our flows have run tasks sequentially — one after another in a straight line. But real-world workflows often need more sophisticated patterns. What if you only want to run certain tasks when a condition is met? What if you need to process a list of files, running the same tasks for each one? What if you want multiple tasks to run at the same time for better performance?

Flowable tasks give you this control. Unlike runnable tasks that perform computation, flowable tasks control how your workflow executes. They enable conditional branching, loops, parallel execution, and more — turning simple sequential flows into sophisticated orchestration logic.

Types of Flowable Tasks

Kestra provides several flowable task types for different orchestration patterns:

Conditional Tasks let you decide whether to run branches of your workflow based on a condition:

  • If — run tasks when a condition is true, with optional else branch

  • Switch — choose between multiple branches based on a value

Looping Tasks let you iterate through data:

  • ForEach — run a set of tasks for each value in an array

  • ForEachItem — run a subflow for each value in a file, better for large amounts of data for performance

  • LoopUntil — repeat the same tasks until a condition is met

Parallel runs multiple tasks at the same time for better performance.

Subflow lets you create reusable flows that other flows can trigger, like functions you can call.

There are several other flowable task types available — check the flowable tasks documentation for the complete list.

Using Conditional Logic

Let's improve our HTTP Request flow with conditional logic. Previously, we just logged the status code. Now we'll use an If task to provide different messages based on whether the request succeeded:

id: myflow
namespace: company.team

inputs:
  - id: uri
    type: URI
    defaults: https://kestra.io

tasks:
  - id: make_request
    type: io.kestra.plugin.core.http.Request
    uri: "{{ inputs.uri }}"

  - id: check_status
    type: io.kestra.plugin.core.flow.If
    condition: "{{ outputs.make_request.code == 200 }}"
    then:
      - id: log_success
        type: io.kestra.plugin.core.log.Log
        message: "Request successful"
    else:
      - id: log_error
        type: io.kestra.plugin.core.log.Log
        message: "Request was not successful: {{ outputs.make_request.code }}"

The If task evaluates the condition using an expression. If the status code is 200, it runs the tasks in the then branch. Otherwise, it runs the tasks in the else branch. This is where expressions and flowable tasks work together — expressions evaluate the data, and flowable tasks use that evaluation to control the workflow.

Looping through data

What if you need to process multiple items with the same logic? Maybe you have a list of customer IDs and want to send each one through the same notification workflow, or an array of file paths where each file needs identical processing.

The ForEach task handles this neatly. It takes an array of values and runs a set of tasks for each item in that array. Let's see it in action with a simple example:

id: myflow
namespace: company.team

inputs:
  - id: data
    type: ARRAY
    itemType: INT
    defaults: [1,2,3,4,5]

tasks:
  - id: loop
    type: io.kestra.plugin.core.flow.ForEach
    values: "{{ inputs.data }}"
    tasks:
      - id: log_data
        type: io.kestra.plugin.core.log.Log
        message: "{{ taskrun.value }}"

The values property specifies the array to loop through. Within the loop, you can access the current item using {{ taskrun.value }} — this expression gives you the value for the current iteration. In this example, the log task will execute five times, once for each number in the array, logging 1, 2, 3, 4, and 5.

Subflows

As your workflows grow, you'll find yourself repeating the same sets of tasks across multiple flows. Subflows solve this by letting you package tasks into reusable components.

Think of subflows like functions in programming: define the logic once, then call it from anywhere. For example, you might create a subflow that handles error notifications by posting to both Slack and email. Instead of copying those two tasks into every flow that needs alerts, you can simply call your error notification subflow.

Let's take our HTTP request example and turn it into a subflow. First, we'll create the subflow itself:

id: mysubflow
namespace: company.team

inputs:
  - id: uri
    type: URI

tasks:
  - id: make_request
    type: io.kestra.plugin.core.http.Request
    uri: "{{ inputs.uri }}"

  - id: log
    type: io.kestra.plugin.core.log.Log
    message: "{{ outputs.make_request.code }}"

outputs:
  - id: data
    type: STRING
    value: "{{ outputs.make_request.code }}"

Notice the outputs section at the flow level — this is new. Just like tasks can produce outputs, entire flows can too. This subflow accepts a URI input, makes the request, logs the status, and returns the status code as a flow-level output that the calling flow can access.

Now any flow can call this subflow using the Subflow task:

id: myflow
namespace: company.team

tasks:
  - id: subflow
    type: io.kestra.plugin.core.flow.Subflow
    namespace: company.team
    flowId: mysubflow
    inputs:
      uri: https://kestra.io
      
  - id: log_status_code
    type: io.kestra.plugin.core.log.Log
    message: "{{ outputs.subflow.outputs.data }}"

The parent flow passes inputs to the subflow and can access its outputs using {{ outputs.subflow.outputs.data }}. This makes your workflows more maintainable — update the subflow once, and all flows that use it benefit from the changes.

Kestra's Dependencies view shows which flows call which subflows, helping you understand your workflow architecture at a glance.

With flowable tasks, you can build workflows that adapt to different scenarios, process large datasets efficiently, and handle complex orchestration logic. They transform Kestra from a simple task runner into a sophisticated workflow orchestration platform.