> ## Documentation Index
> Fetch the complete documentation index at: https://docs.enconvo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Advanced Workflows

> Branching, loops, output contracts, subworkflows, per-task models, and complete example workflows

## Overview

This page covers the control-flow and reliability features of the
`agent-workflow/v1` DSL, plus several complete, runnable example workflows. If
you're new to the DSL, start with [Creating Workflows](/workflows/creating-workflows).

## Branching

### `if` — conditional tasks

Attach `if:` to run a task only when a condition is true. When the condition is
false, the task **and everything downstream of it** cascade-skip:

```yaml theme={null}
tasks:
  deploy:
    if: inputs.ready          # false → deploy and its dependents skip
    command: ./deploy.sh
```

Conditions are template expressions evaluated for truthiness — for example
`inputs.score >= 60`, `tasks.check.output.exitCode == 0`, or a plain boolean
input.

### `else_of` — the else branch

`else_of:` runs a task only when the referenced task was condition-skipped. It's
the "else" side of an `if`:

```yaml theme={null}
tasks:
  deploy:
    if: inputs.ready
    command: ./deploy.sh
  notify_skip:
    else_of: deploy           # runs only when `deploy` was skipped by its `if`
    command: osascript -e 'display notification "deploy skipped"'
```

### `run_when: always` — join and cleanup

A task with `needs` normally skips if any dependency skipped, and the run stops
if a required dependency fails. `run_when: always` overrides both — the task runs
as long as its dependencies have finished, whether they succeeded, skipped, or
failed. Use it for joins and cleanup:

```yaml theme={null}
tasks:
  build:
    command: ./build.sh
  cleanup:
    needs: [build]
    run_when: always          # runs even if `build` failed
    command: rm -rf ./.tmp
```

## Loops

### `foreach` — fan out over a list

`foreach:` runs the task once per item. Inside, `{{ item }}` is the current
element and `{{ index }}` is its position. Downstream tasks receive the collected
array of outputs:

```yaml theme={null}
inputs:
  questions: ["What is it?", "Why does it matter?"]
tasks:
  answer:
    foreach: "{{ inputs.questions }}"
    prompt: "Answer in one sentence: {{ item }}"
  digest:
    uses: transform
    value: "{% for a in tasks.answer.output %}- {{ a }}\n{% endfor %}"
```

`foreach` also accepts an inline array, and it composes with the other executors
(a `foreach` of `tool:` calls, of subworkflows, and so on). An empty list
produces an empty result and downstream tasks still run.

### `loop` — repeat until a condition holds

`loop:` repeats a task. `until:` is checked after each round against `output`
(that round's result); `{{ round }}` is the round number and `{{ last }}` is the
previous round's output. Exhausting `maxRounds` without satisfying `until` fails
the task.

```yaml theme={null}
tasks:
  poll:
    run: "curl -s -o /dev/null -w '%{http_code}' https://example.com/status"
    loop:
      until: "output.stdout | trim == '200'"
      maxRounds: 10
```

Omit `until` to loop a fixed number of times:

```yaml theme={null}
tasks:
  retry_build:
    command: ./build.sh
    loop:
      maxRounds: 3
```

## Reliability

* **`retries`** — retry a failed task. Either a count (`retries: 2`) or an object
  (`retries: { maxAttempts: 3, delayMs: 500 }`).
* **`required: false`** — a failure of this task does **not** abort the run;
  downstream tasks continue.
* **`timeoutMs`** — kill a shell task that runs too long.

```yaml theme={null}
tasks:
  flaky_fetch:
    tool: web_fetch/web_fetch
    params: { link_url: "{{ inputs.url }}" }
    retries: { maxAttempts: 3, delayMs: 1000 }
  optional_enrich:
    run: "./enrich.sh || exit 1"
    required: false           # failure here won't stop the run
```

## Output contracts — `output_schema`

Attach an `output_schema` (a JSON Schema object) to a `prompt` or `agent` task to
enforce the shape of the reply. You do **not** need to describe the JSON format in
the prompt text — the schema is enforced by the model provider and injected as an
instruction, and the reply is parsed (with repair) into an object you can index
into downstream. If the output doesn't conform, the task retries; if it never
conforms, the task fails.

```yaml theme={null}
tasks:
  extract:
    prompt: "Extract the invoice details from: {{ inputs.text }}"
    output_schema:
      type: object
      required: [payer, amount, date]
      properties:
        payer:  { type: string }
        amount: { type: number }
        date:   { type: string }
  record:
    uses: transform
    value: "{{ tasks.extract.output.payer }} paid {{ tasks.extract.output.amount }}"
```

## Per-task model overrides

`prompt` and `agent` tasks accept optional `provider:` and `model:` to switch the
LLM for that task only. This is a **run-scoped** override — your saved model
configuration is untouched. Use a cheap model for bulk steps and a stronger one
where it matters:

```yaml theme={null}
tasks:
  draft:
    prompt: "Draft a blog outline for: {{ inputs.topic }}"
    provider: enconvo_ai
    model: anthropic/claude-haiku-4-5-20251001
  polish:
    prompt: "Polish this outline into final copy:\n{{ tasks.draft.output }}"
    provider: enconvo_ai
    model: anthropic/claude-sonnet-5
```

<Note>
  `provider` is a provider command (e.g. `enconvo_ai`, `open_ai`, `ollama`) and
  `model` is a model id for that provider. Both are only valid on `prompt`/`agent`
  tasks.
</Note>

## Subworkflows

Compose workflows with `uses: workflow`, either by `workflowId:` (a saved
workflow) or an inline `workflow:` block. The task's output is the child run
result — reference the child's task outputs via `output.outputs.<taskId>`:

```yaml theme={null}
tasks:
  build_each:
    foreach: "{{ inputs.targets }}"
    uses: workflow
    inputs:
      target: "{{ item }}"
    workflow:
      tasks:
        compile:
          run: "make {{ inputs.target }}"
        package:
          run: "tar czf {{ inputs.target }}.tgz build/"
  summary:
    uses: transform
    value: "{% for r in tasks.build_each.output %}{{ r.status }} {% endfor %}"
```

## Templates reference

| Reference                    | Where                                |
| ---------------------------- | ------------------------------------ |
| `{{ inputs.x }}`             | Anywhere — workflow inputs           |
| `{{ tasks.<id>.output }}`    | Anywhere — an upstream task's output |
| `{{ item }}` / `{{ index }}` | Inside a `foreach` task              |
| `{{ round }}` / `{{ last }}` | Inside a `loop` task                 |
| `{{ now }}`                  | Current timestamp                    |
| `{{ env.HOME }}`             | Process environment variables        |

Templates support Nunjucks filters (`| trim`, `| int`, `| dump`, `| default(...)`,
`{% for %}`, etc.), which is how you reshape data between tasks.

## Complete examples

### Research → summarize → report

Search the web, summarize the findings, and assemble a Markdown report. Note the
sequential-by-default chaining — no `needs` wiring required:

```yaml theme={null}
version: agent-workflow/v1
title: Research Brief
description: Search the web on a topic and produce a short Markdown brief.
inputs:
  topic: coffee brewing
tasks:
  search:
    tool: web_search/web_search
    params:
      search_query: "{{ inputs.topic }} latest"
      limit: 5
  summarize:
    prompt: |
      Summarize the key points from these results in 4 bullets.
      Results: {{ tasks.search.output }}
    provider: enconvo_ai
    model: anthropic/claude-haiku-4-5-20251001
  report:
    uses: transform
    value: |
      # {{ inputs.topic }} — brief ({{ now }})
      {{ tasks.summarize.output }}
```

### Conditional routing

Approve or reject based on a score, then always write a final report:

```yaml theme={null}
version: agent-workflow/v1
title: Lead Router
description: Route a lead by score, reporting the outcome either way.
inputs:
  score: 42
tasks:
  approve:
    if: "inputs.score >= 60"
    uses: transform
    value: "approved"
  reject:
    else_of: approve
    uses: transform
    value: "rejected"
  report:
    needs: [approve, reject]
    run_when: always
    uses: transform
    value: "outcome: {{ tasks.approve.output | default('') }}{{ tasks.reject.output | default('') }}"
```

### Loop until converged

Refine a draft repeatedly until a self-check approves it, capping the rounds:

```yaml theme={null}
version: agent-workflow/v1
title: Refine Until Good
description: Improve a draft until a self-check approves it.
inputs:
  brief: A tweet announcing our new feature.
tasks:
  refine:
    prompt: |
      Improve this draft. Return the improved draft and whether it is ready.
      Previous attempt: {{ last | default(inputs.brief) }}
    provider: enconvo_ai
    model: anthropic/claude-haiku-4-5-20251001
    output_schema:
      type: object
      required: [draft, approved]
      properties:
        draft:    { type: string }
        approved: { type: boolean }
    loop:
      until: "output.approved == true"
      maxRounds: 4
  publish:
    uses: transform
    value: "{{ tasks.refine.output.draft }}"
```

## Related

<CardGroup cols={2}>
  <Card title="Creating workflows" icon="pen-to-square" href="/workflows/creating-workflows">
    The editor, the editing assistant, and each executor.
  </Card>

  <Card title="Introduction" icon="book-open" href="/workflows/introduction">
    The task model and how workflows are run.
  </Card>
</CardGroup>
