Step Properties

Steps are the execution blocks inside Gherkio's setup, steps, and teardown lifecycle lists. They are evaluated sequentially, passing context variables, environment tokens, and parsed JSON fields down the scenario chain.


⚡ The Step Structure

A scenario step is defined as a YAML map containing structural blocks that configure the action, validate the response, extract variables, or control loops.

- name: Create checkout order        # Optional human-readable step label
  request:                       # 1. Action: Trigger HTTP request
    method: POST
    url: /v1/checkout
    timeout: 5s
  expect:                        # 2. Assert: Validate status & schema
    status: 201
    body.success: true
  save:                          # 3. Context: Store data for next steps
    orderId: body.id
  timing:                        # 4. Perform: Latency budget check
    max: 500ms
```text

---

## 🧭 Step Configuration Properties

Each step in a scenario sequence supports the following top-level keys:

| Property Key | Type | Required | Description | Example |
| :--- | :--- | :--- | :--- | :--- |
| `name` | `string` | No | Human-readable label for the step. Shown in test output and HTML report instead of `METHOD /url`. | `name: Create new order` |
| `if` | `string` | No | Conditional guard clause. Step is skipped if the expression evaluates to false. | `if: $responseCode == 200` |
| `request` | `object` | Conditional | HTTP request payload block. Mutually exclusive with other step operations. | (See Request Properties below) |
| `redis` | `object` | Conditional | Controlled read-only Redis operation. Mutually exclusive with other step operations. | `redis: { connection: local-cache, command: get, key: "product:42" }` |
| `use` | `string` | Conditional | Scenario composition. Imports and executes another scenario YAML file inline. Mutually exclusive with other step operations. | `use: shared/login.yaml` |
| `set` | `map[string]string`| Conditional | Inline variable assignment. Explicitly assigns or overrides variables without I/O. Mutually exclusive with other step operations. See [Variable Assignment (set)](set.md). | `set: { QUEUE_ID: "01KT4EBA37Y" }` |
| `export` | `object` | Conditional | Materializes a saved collection into an Excel (`.xlsx`) file. Mutually exclusive with other step operations. | `export: { file: fixtures/bulk.xlsx, from: "$items", columns: [...] }` |
| `import` | `object` | Conditional | Reads data from an Excel (`.xlsx`) file into runtime variables. Mutually exclusive with other step operations. | `import: { file: fixtures/users.xlsx, as: users }` |
| `repeat` | `object` | Conditional | Repeats a group of steps until a condition is true or the attempt limit is exhausted. Mutually exclusive with other step operations. | `repeat: { attempts: 20, until: "$count == 0", steps: [...] }` |
| `for_each` | `object` | Conditional | Executes nested steps sequentially once per item in a saved array. Mutually exclusive with other step operations. | `for_each: { from: "$items", as: item, steps: [...] }` |
| `with` | `map[string]string`| No | Variable overrides injected into a `use:` step. Values interpolated before injection; original values restored after completion. | `with: { PARENT_CLAIM_ISSUE_ID: $STATUS_APPROVED_ID }` |
| `expect` | `object` | No | Assertions mapping target dot-notation paths to expected formats or matchers. | `expect: { status: 200 }` |
| `save` | `map[string]string`| No | Context extraction map. Binds response parameters to dynamic variables. | `save: { token: body.accessToken }` |
| `retry` | `object` | No | Automated polling loop rules for testing eventually consistent resources. | (See Retry & Polling chapter) |
| `timing` | `object` | No | Latency budget validation. Asserts that request execution did not exceed duration limits. | `timing: { max: 300ms }` |

---

## 🌐 Request Configuration Properties (`request`)

The `request` block defines the HTTP action Gherkio will execute. It supports the following keys:

| Property Key | Type | Required | Description | Example |
| :--- | :--- | :--- | :--- | :--- |
| `method` | `string` | **Yes** | HTTP request verb. Sourced as `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`, `HEAD`. | `method: POST` |
| `url` | `string` | **Yes** | Target URL. Supports relative paths (resolves to environment `baseUrl`) or absolute URLs. | `url: /v1/users` |
| `service` | `string` | No | Routes the request to a specific microservice defined in the active environment. | `service: auth` |
| `headers` | `map[string]string`| No | Key-value mapping of custom HTTP headers. Supports variable interpolation. | `headers: { Content-Type: "application/json" }` |
| `body` | `any` | No | Request body payload. Supports JSON maps, lists, raw strings, and variable injection. | `body: { role: "admin" }` |
| `query` | `map[string]string` | No | Query parameters appended to the URL. Supports variable interpolation in values. | `query: { status: available }` |
| `transform` | `object` | No | Declarative collection projections: filter, slice, and reshape arrays from saved variables into the request payload. | (See Requests chapter) |
| `multipart` | `object` | No | Multipart form-data wrapper used for sending form fields and binary file uploads. | (See Requests chapter) |
| `timeout` | `string` | No | HTTP socket timeout limit (parsed via standard Go duration strings like `5s`, `500ms`, `1m`). | `timeout: 10s` |

## Redis Steps (`redis`)

Redis steps expose only the read-only commands `get`, `exists`, `ttl`, and
`hgetall`. Arbitrary commands and Lua scripts are intentionally unsupported.

```yaml
- name: Verify product cache
  redis:
    connection: local-cache
    command: get
    key: "product:$productId"
  expect:
    redis.exists: true
    redis.value.id: "$productId"
  save:
    cachedName: redis.value.name
  retry:
    attempts: 5
    interval: 200
```text

`get` automatically decodes JSON values. Redis assertions and saved values use
the `redis.*` path: `redis.exists`, `redis.value`, `redis.value.<field>`, and
`redis.ttl`. Existing matchers, timing assertions, and retry strategies apply.

See [Redis Cache Checks](redis.md) for complete API-plus-cache scenarios,
command-specific result paths, TTL and hash examples, polling, and Sentinel use.

## Collection Request Loops (`for_each`)

Use `for_each` when an endpoint requires one request per item rather than a
single batch payload. `from` resolves a saved array, `as` names the scoped
current item (default: `item`), and nested `steps` execute sequentially.

```yaml
steps:
  - request:
      method: GET
      url: /api-a/items
    save:
      items: body.items

  - name: Send every item to API B
    for_each:
      from: $items
      as: item
      steps:
        - request:
            method: POST
            url: /api-b/items
            body:
              external_id: $item.id
              name: $item.name
          expect:
            status: 201
```text

The loop stops at the first failing nested step. Variables saved inside the
loop remain available afterward, while the item alias is restored when the
loop ends. An empty source array succeeds without executing nested steps; a
missing or non-array source fails the loop. Terminal and report output label
each nested execution as `for_each N/M`.

## Bounded Multi-Step Loops (`repeat`)

Use `repeat` when one polling attempt needs multiple operations. The nested
`steps` run sequentially, then `until` is evaluated against the updated
variables. A true condition preserves those variables for following steps and
ends the loop. An inner failure stops immediately; if the condition remains
false after every attempt, the repeat step fails.

```yaml
- name: Find an unused issue tag
  repeat:
    attempts: 20
    until: $existingTicketCount == 0
    steps:
      - name: Select candidate issue tag
        set:
          ISSUE_TAG_L3: ${randomItem(respIssueTagL3)}

      - name: Check tickets using candidate
        request:
          method: GET
          url: /v1/tickets
          query:
            issue_tag_id: $ISSUE_TAG_L3.id
        expect:
          status: 200
        save:
          existingTicketCount: count(body.data)
```text

This is a bounded loop, not unbounded recursion. Reports label every nested
execution as `repeat N/M`. A dry run previews the block once because no live
response exists for evaluating response-dependent exit conditions.

---

## 📤 Excel Export (`export`)

The `export` step materializes a saved collection into an Excel (`.xlsx`) file.
It is a **sink**: it writes data to disk instead of making an HTTP request, so
it pairs naturally with `multipart.files` to generate fixtures for bulk-import
APIs.

```yaml
steps:
  - name: Fetch product catalog
    request:
      method: GET
      url: /products
    save:
      products: body.products

  - name: Generate bulk upload Excel fixture
    export:
      file: fixtures/bulk_upload.xlsx
      from: $products
      sheet: "BulkData"
      columns:
        - header: "product_title"
          value: "$string(item.title)"
        - header: "price"
          value: "$float(item.price)"
        - header: "priority"
          value: "$if(item.price > 500, 'priority', 'normal')"
        - header: "generated_at"
          value: "${dateNow(\"2006-01-02\")}"
```text

### Configuration Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `file` | `string` | **Yes** | Output path for the Excel file. Absolute paths are used as-is; relative paths resolve against the configured `export.path` (default: project root). |
| `from` | `string` | **Yes** | Source collection array variable (must start with `$`). |
| `sheet` | `string` | No | Worksheet name (default `Sheet1`). |
| `columns` | `list` | **Yes** | Ordered column definitions. |

Each column has:

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `header` | `string` | **Yes** | Column header text. |
| `value` | `string` | **Yes** | Per-cell expression. Supports interpolation, casting (`$string`, `$int`, `$float`, `$bool`), `$if(...)`, and references to saved variables. |

### Key Behaviors
- **Mutual Exclusion**: `export` cannot be combined with `request`, `redis`, `use`, `set`, `repeat`, `for_each`, `with`, `expect`, `save`, `timing`, or `retry`.
- **Row Scoping**: Each row is evaluated with the current item bound to `item` (plus all saved variables).
- **Conditional Values**: `$if(condition, then, else)` supports full boolean expressions with comparison operators (`==`, `!=`, `>`, `>=`, `<`, `<=`, `&&`, `||`, `!`).
- **No HTTP**: The step passes when the file writes successfully.
- **Configurable Output**: Set `export.path` in `.gherkio/config.yaml` to write relative `file` paths under a dedicated directory (e.g. `fixtures`).

---

## 📥 Excel Import (`import`)

The `import` step reads an Excel (`.xlsx`) spreadsheet from disk and parses its rows into an array of objects saved in a runtime variable (`as: <variableName>`). This enables test scenarios to ingest external Excel fixtures, iterate through records with `for_each`, or validate uploaded spreadsheet contents.

```yaml
steps:
  - name: Ingest customer dataset
    import:
      file: fixtures/customers.xlsx
      sheet: "Customers"          # Optional, defaults to active/first sheet
      as: CUSTOMER_LIST          # Target variable storing array of row maps
      header_row: 1              # Optional, 1-based header row index (default: 1)
      data_start_row: 2          # Optional, 1-based data start row (default: 2)

  - name: Create each customer via API
    for_each:
      from: $CUSTOMER_LIST
      as: customer
      steps:
        - name: Send creation request
          request:
            method: POST
            url: /v1/customers
            body:
              full_name: $customer.customer_name
              email: $customer.email_address
              initial_balance: $float(customer.balance)
```text

### Configuration Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| `file` | `string` | **Yes** | Target `.xlsx` file path (absolute or relative to project root / export path). |
| `as` | `string` | **Yes** | Target variable name to store parsed rows (without leading `$`). |
| `sheet` | `string` | No | Worksheet name (default: active/first sheet). |
| `header_row` | `int` | No | 1-based row index containing column headers (default: `1`). |
| `data_start_row` | `int` | No | 1-based row index where data rows start (default: `2`). |
| `columns` | `list` | No | Optional list of explicit column alias mappings (`{ header, as }`). |

### Key Behaviors
- **Header Normalization**: When `columns` is omitted, headers containing spaces or punctuation are automatically converted to clean `snake_case` keys (e.g. `"Customer Name ($)"` becomes `customer_name`).
- **Explicit Aliasing**: Specify `columns` mappings to rename specific headers to custom field keys.
- **Mutual Exclusion**: `import` is a primary step operation and cannot be combined with `request`, `redis`, `use`, `set`, `repeat`, `for_each`, `export`, `with`, `expect`, `save`, `timing`, or `retry`.
- **Seamless Expressions**: The loaded variable is a standard array of maps, fully accessible via `$list[0].field`, `${randomItem(list)}`, and `for_each: { from: $list }`.

---

## 🔀 Conditional Execution (`if`)

Steps can be conditionally executed using the `if` guard property. If the expression evaluates to false, the step is skipped entirely (its HTTP request is not sent, assertions are ignored, and any variable extraction is bypassed). Skipped steps are tracked as `skipped` in test metrics, CLI logs, and HTML reports.

### Syntax and Comparison Operators
The `if` property expects a string expression consisting of variables, comparison operators, and literal values (strings, numbers, or booleans).

Supported operators:
*   `==` (Equal to)
*   `!=` (Not equal to)
*   `>` (Greater than)
*   `>=` (Greater than or equal to)
*   `<` (Less than)
*   `<=` (Less than or equal to)
*   `&&` (Logical AND)
*   `||` (Logical OR)
*   `!` (Logical negation)

`&&` has higher precedence than `||`. Use parentheses to group expressions.
Both operators short-circuit: a false left side skips the right side of `&&`,
and a true left side skips the right side of `||`.

### Examples

#### Basic Variable Comparison
```yaml
steps:
  - name: Generate Admin Invoice
    if: $USER_ROLE == admin
    request:
      method: POST
      url: /v1/invoices/admin
      body:
        amount: 150.00
    expect:
      status: 201
```text

#### Numeric Comparison
```yaml
steps:
  - name: Get Invoice Details
    if: $INVOICE_AMOUNT >= 1000
    request:
      method: GET
      url: /v1/audit/large-invoice/$INVOICE_ID
    expect:
      status: 200
```text

#### Truthiness Check (Check if variable exists and is not false/empty)
```yaml
steps:
  - name: Process Refund
    if: $REFUND_ENABLED
    request:
      method: POST
      url: /v1/refunds
      body:
        transaction_id: $TX_ID
```text

#### Compound Conditions

```yaml
steps:
  - name: Process complete customer record
    if: $item.ticket_name && $item.customer_name && $item.ticket_code
    request:
      method: POST
      url: /v1/webhooks/premature-tickets
```text

```yaml
# Parentheses and negation
if: ($enabled && $count > 0) || !($status == blocked)
```text

For backward compatibility, negating a comparison without parentheses (for
example `!$status == blocked`) is rejected as ambiguous. Write
`!($status == blocked)` instead.

---

## 💾 Dynamic Variable Saving (`save`)

The `save` block allows steps to bind HTTP response parameters (body fields, headers, or decoded JWT claims) to variables that can be dynamically interpolated in all subsequent request URLs, headers, or bodies.

### Syntax Reference
```yaml
save:
  variable_name: response_source_path
```text

*   **`body.<path>`**: Extracts JSON fields. Supports dotted-paths, indexes, and collections.
    *   *Example*: `productId: body.items[0].id`
*   **`headers.<header-key>`**: Extracts HTTP response header values (case-insensitive).
    *   *Example*: `rateLimit: headers.X-Rate-Limit`
*   **`jwt.<claim>`**: Automatically decodes response JWT keys (looks for `token`, `accessToken`, `access_token`) and extracts claims.
    *   *Example*: `adminRole: jwt.role`
*   **`count(body.<path>)`**: Saves the length of a response array as an integer. An explicitly present `null` value saves `0`; an empty array also saves `0`. Missing paths and non-array values produce a save warning and are not stored.
    *   *Example*: `4-notesBeforeConflict: count(body.data)`

```yaml
save:
  notes: body.data
  notesCount: count(body.data)
```text

---

## ⏱️ Latency Budgets (`timing`)

In performance-critical applications, keeping endpoint response latency within a specific budget is a core contract. Gherkio allows developers to validate performance metrics directly inside test steps using the `timing` block:

### Configuration Syntax
```yaml
timing:
  max: duration_string # e.g. "200ms", "1s", "1.5s"
```text

If the combined execution time of the step exceeds the `max` threshold, Gherkio fails the step and reports a detailed latency budget violation error:

```text
❌ Step 2: GET /users/profile timing assertion failed
  - Expected latency: <= 200ms
  - Actual latency:   242ms
```text

---

## 🔌 Scenario Composition & Context Bubble Up (`use`)

To keep test suites DRY, Gherkio steps can delegate execution to a shared modular test script using the `use:` tag.

### Execution Blueprint
```yaml
# Inside login-and-query.yaml
steps:
  - use: auth/login.yaml            # 1. Runs login sequence, saves $authToken
  - request:
      method: GET
      url: /profile
      headers:
        Authorization: "Bearer ${authToken}" # 2. Automatically inherits $authToken
```text

1.  **Monotonic Variables**: Any variable saved (via `save`) inside the composed YAML file is automatically merged and bubbles up to the parent execution context.
2.  **Context Inheritance**: Composed scenarios inherit all variables defined prior to their execution (e.g. host environments, active credential credentials).

---

## ⚙️ Declarative Variable Assignment (`set`)

A `set` step explicitly assigns, updates, or overrides variables in the runtime context **without executing any request or composed scenario**. Use it to seed fixtures, override a saved value for one run, freeze a generated value so later steps agree on it, or hand state out of a loop/composed file.

```yaml
steps:
  - name: Seed the queue fixture
    set:
      QUEUE_ID: "01KT4EBA37Y"

  - name: Get Queue info
    request:
      method: GET
      url: /v1/queues/$QUEUE_ID
    expect:
      status: 200

  - name: Rotate the queue ID (separate step, see below)
    set:
      PREVIOUS_QUEUE_ID: "$QUEUE_ID"

  - name: Move to the next queue ID
    set:
      QUEUE_ID: "02HT5FCA38Z"
```text

### Key Behaviors
- **Mutual Exclusion**: A `set` step cannot be combined with `request`, `use`, `redis`, `repeat`, `for_each`, `export`, or `import` — only `name` and `if` may accompany it. `expect`, `save`, `timing`, `retry`, and `with` are *silently ignored* on a `set` step.
- **Interpolation**: Values are interpolated immediately at execution time against the active variable store, so `$QUEUE_ID`, credentials, and `save` results are all readable.
- **Keys are unordered within one block**: Derive a value from another variable in a *separate* `set` step.
- **Typed random selection**: An exact `${randomItem(array)}` value preserves the selected object, so later steps can access fields such as `$PARTNER_STATUS.id`. `${randomItem(array,id)}` stores only the selected field.
- **CLI Output**: In test logs, a `set` step is formatted to show which variables are being set (e.g., `set variables: QUEUE_ID`).

> 📖 **Full chapter**: [Variable Assignment (`set`)](set.md) covers value typing, precedence, guards, loops, and eleven worked scenarios.