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

# Workflow Chaining

> Compose workflows across files with call and include — parameters, state isolation, and pipeline patterns.

# Workflow Chaining

Replay lets you compose workflows across files. You can inject steps at parse time with `include`, or call a separate workflow file at runtime with `call`. This enables reusable workflow fragments and pipeline patterns.

## Include — Compile-Time Injection

The `include` directive loads steps from another file and injects them into the current workflow *before execution*. Included steps run as if they were written inline in the original file.

```yaml theme={null}
name: full-test
include:
  - file: setup.yaml
    with:
      env: test

steps:
  - name: run-test
    type: http
    request:
      method: GET
      url: /health
```

Included steps are prepended to the workflow's own steps in the order they appear.

### Passing Parameters to Includes

Parameters in the `with` block are substituted using `{{ var }}` syntax in the included file:

```yaml theme={null}
# setup.yaml
steps:
  - name: seed-db
    type: shell
    command: "./seed.sh {{ env }}"

  - name: migrate
    type: shell
    command: "./migrate.sh {{ env }}"
```

The `include` directive applies these substitutions at parse time, before any execution starts.

## Call — Runtime Composition

The `call` step loads and executes steps from another workflow file at runtime, mid-execution. Unlike `include`, the called file is loaded when the step runs, and it can return values back to the caller.

### Basic Call

```yaml theme={null}
# main-workflow.yaml
steps:
  - name: setup
    type: call
    file: setup.yaml

  - name: run-tests
    type: http
    request:
      method: GET
      url: /health
```

### Passing Parameters

```yaml theme={null}
- name: create-user
  type: call
  file: create-user.yaml
  with:
    email: qa@example.com
    role: admin
```

Parameters are available as `{{ email }}` and `{{ role }}` inside the called file's steps.

### Calling a Specific Step

Use `target` to execute only one named step from the called file:

```yaml theme={null}
- name: reset-db
  type: call
  file: db-operations.yaml
  target: truncate-tables
```

This is useful when a file contains multiple operations and you only need one.

### Isolating State with Returns

By default, called workflows share the caller's state bag — variables set inside the call are visible after the call returns. Use `returns` to control which variables leak back:

```yaml theme={null}
- name: authenticate
  type: call
  file: auth.yaml
  with:
    username: qa
  returns:
    - token
    - session_id
```

When `returns` is specified, Replay:

1. Snapshots the caller's state before the call
2. Executes the called steps
3. Saves only the listed variables from the called state
4. Restores the caller's state to the snapshot
5. Copies the saved variables back into the caller's state

This prevents the called workflow from polluting the caller's state with temporary or internal variables.

### Example: Reusable Auth Workflow

```yaml theme={null}
# auth.yaml
name: login
steps:
  - name: login
    type: http
    request:
      method: POST
      url: /auth/login
      body:
        email: "{{ email }}"
        password: "{{ password }}"
    extract:
      token: $.data.token
      expires_in: $.data.expiresIn
```

```yaml theme={null}
# test.yaml
steps:
  - name: authenticate
    type: call
    file: auth.yaml
    with:
      email: admin@example.com
      password: "{{ ADMIN_PASSWORD }}"
    returns:
      - token

  - name: use-token
    type: http
    request:
      method: GET
      url: /admin/dashboard
      headers:
        Authorization: Bearer {{ token }}
```

Only `token` leaks back — `expires_in` stays contained inside the call.

## Safeguards — Cycle Detection and Depth Limits

Workflow chaining is powerful, but recursive calls can cause infinite loops. Replay provides two safeguards.

### Cycle Detection

The engine tracks every `file:step` pair during execution using an internal call stack. If the same pair is encountered twice in a single chain, execution stops immediately.

```text theme={null}
a.yaml calls a.yaml
→ cycle detected: a.yaml → a.yaml

a.yaml → b.yaml → a.yaml (mutual recursion)
→ cycle detected: a.yaml → b.yaml → a.yaml
```

`replay validate` catches direct self-calls statically:

```text theme={null}
$ replay validate a.yaml
✓ valid workflow: a (1 steps)
✗ cycle detected: a → a.yaml
```

It also finds cycles nested inside `if`/`then`/`else` and `loop` blocks. Cross-file cycles (e.g., `a.yaml → b.yaml → a.yaml`) are detected at runtime by the engine.

### Call Depth Limit

Even without cycles, deeply nested calls can exhaust resources. Use `--max-call-depth` to cap the call chain:

```bash theme={null}
replay run pipeline.yaml --max-call-depth 10
```

Default is **100**. When exceeded, you see:

```text theme={null}
call depth exceeded (10): pipeline.yaml → setup.yaml → seed.yaml
```

The call stack resets between workflows, so parallel runs do not interfere with each other.

## Include vs Call

|                | Include                                | Call                              |
| -------------- | -------------------------------------- | --------------------------------- |
| **When**       | Parse time (before execution)          | Runtime (mid-execution)           |
| **State**      | Shares full state                      | Supports `returns` isolation      |
| **Parameters** | `{{ var }}` substitution               | `with` block + `{{ var }}`        |
| **Targeting**  | Cannot target — all steps are injected | Can target a `specific step`      |
| **Best for**   | Setup/teardown that always runs        | Conditional or reusable sub-flows |

## Pipeline Patterns

### Setup → Test → Teardown

```yaml theme={null}
name: managed-test
include:
  - file: seed-database.yaml
    with:
      env: test
steps:
  - name: run-tests
    type: http
    request:
      method: GET
      url: /users
    assert:
      - ["$.status", "eq", 200]
  - name: cleanup
    type: call
    file: cleanup.yaml
    with:
      env: test
```

### Auth → Action → Verify

```yaml theme={null}
steps:
  - name: get-token
    type: call
    file: auth.yaml
    with:
      email: qa@example.com
      password: "{{ QA_PASSWORD }}"
    returns:
      - token

  - name: create-resource
    type: http
    request:
      method: POST
      url: /resources
      headers:
        Authorization: Bearer {{ token }}
    extract:
      resource_id: $.data.id

  - name: verify-in-db
    type: call
    file: verify-resource.yaml
    with:
      resource_id: "{{ resource_id }}"
```

### Conditional Chaining

Combine `if` with `call` to branch between different workflows:

```yaml theme={null}
- name: check-flag
  type: if
  condition: ["feature_enabled", "==", true]
  then:
    - name: run-experiment
      type: call
      file: workflows/experiment.yaml
      with:
        user_id: "{{ user_id }}"
  else:
    - name: run-control
      type: call
      file: workflows/control.yaml
      with:
        user_id: "{{ user_id }}"
```

## What's Next?

* Run workflows in parallel with [parallel execution](/replay/guides/parallel-execution)
* Validate outcomes with [assertions](/replay/guides/assertions)
