Skip to main content

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

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.
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:
# 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

# main-workflow.yaml
steps:
  - name: setup
    type: call
    file: setup.yaml

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

Passing Parameters

- 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:
- 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:
- 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

# 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
# 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.
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:
$ 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:
replay run pipeline.yaml --max-call-depth 10
Default is 100. When exceeded, you see:
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

IncludeCall
WhenParse time (before execution)Runtime (mid-execution)
StateShares full stateSupports returns isolation
Parameters{{ var }} substitutionwith block + {{ var }}
TargetingCannot target — all steps are injectedCan target a specific step
Best forSetup/teardown that always runsConditional or reusable sub-flows

Pipeline Patterns

Setup → Test → Teardown

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

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:
- 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?