# A task that runs two subtasks
@task
async def sum_squares(a: int, b: int) -> int: # Must be async to await subtasks
  result1, result2 = await asyncio.gather(
    calculate_square(a),
    calculate_square(b)
  )
  return result1 + result2

@task
def calculate_square(a: int) -> int:
  return a * a
```

**When running subtasks:**

- In most cases, the parent task should be defined as `async`.
  - Otherwise, it can't `await` the results of its subtasks.
- You run a subtask by calling the corresponding function (e.g., `calculate_square` above).
  - _However_, this call doesn't return the function's defined return value!
  - Instead, this kicks off a task run and returns a special `TaskInstance` object.
  - As shown, you can `await` this object to obtain the subtask's _actual_ return value.
- Your task _can_ call functions that are _not_ marked as tasks. These functions run and return as normal (they do not spin up their own task instances).

> **A task _can_ run another task defined in a _different_ workflow. However:**
>
> - This requires instead using the Workflows SDK or Render API, as described in [Running Workflow Tasks](workflows-running).
> - This is not tracked as a task/subtask relationship when visualizing task execution in the [Render Dashboard][dboard].

### Parallelizing subtasks

When running subtasks, it's usually helpful to run multiple of them in parallel, such as to chunk a large workload into smaller independent pieces. Common examples include processing batches of images or analyzing different sections of a large document.

**Python**

Use `asyncio.gather` to run multiple subtasks in parallel. In the example below, the `process_photo_upload` task runs a separate `process_image` subtask for each element in its `image_urls` argument:

```python
from render_sdk.workflows import task
import asyncio

@task
async def process_photo_upload(image_urls: list[str]) -> dict:
  # Process all images in parallel by running a subtask for each
  results = await asyncio.gather( #highlight-line
    *[process_image(url) for url in image_urls] #highlight-line
  ) #highlight-line

  num_successful = sum(1 for r in results if r["success"])
  num_failed = len(results) - num_successful

  return {
    "total": len(image_urls),
    "processed": num_successful,
    "failed": num_failed,
    "results": results
  }

@task
def process_image(image_url: str) -> dict:

  # Image processing logic goes here

  return {
    "url": image_url,
    "thumbnail_url": f"{image_url}_thumb.jpg",
    "success": True
  }
```

**If you don't use `asyncio.gather` or a similar function, subtasks run serially.**

For example:

```python{3-5}
@task
async def sum_squares_slower(a: int, b: int) -> int:
  # ⚠️ Not parallel!
  result1 = await calculate_square(a)
  result2 = await calculate_square(b) # Runs after first subtask completes
  return result1 + result2

@task
def calculate_square(a: int) -> int:
  return a * a
```

Serial execution _is_ helpful when running a chain of subtasks that depend on each other. However, it dramatically slows execution for subtasks that are completely independent. Parallelize wherever your use case allows.