When Blocks

Multi-condition when blocks enable complex state transitions with rich boolean expressions for advanced routing logic.

Overview

When blocks replace simple status-based transitions with complex decision logic supporting:

  • HTTP status code matching (exact, range, multiple)

  • Response body content matching

  • Header checks and comparisons

  • Extracted variable conditions using Jinja2

  • Response time analysis

  • AND/OR logic for complex routing

Basic Syntax

states:
  check_auth:
    request: |
      GET /api/user/profile HTTP/1.1

    extract:
      role:
        type: jpath
        pattern: "$.user.role"

    next:
      - when:
          - status: 200
          - condition: "{{ role == 'admin' }}"
        goto: admin_panel

      - otherwise:
        goto: normal_flow

Condition Types

Status Code Matching

when:
  - status: 200                    # Exact match
  - status_in: [200, 201, 202]     # Multiple statuses
  - status_range: [200, 299]       # Range (inclusive)

Examples:

# Success codes
- when:
    - status_range: [200, 299]
  goto: success

# Client errors
- when:
    - status_range: [400, 499]
  goto: client_error

# Specific codes
- when:
    - status_in: [401, 403]
  goto: unauthorized

Jinja2 Expressions

Evaluate complex boolean expressions using extracted variables:

when:
  - condition: "{{ role == 'admin' }}"
  - condition: "{{ balance > 1000 }}"
  - condition: "{{ age >= 18 and age <= 65 }}"
  - condition: "{{ 'premium' in user.features }}"

Supported operators:

  • Comparison: ==, !=, >, <, >=, <=

  • Membership: in, not in

  • Boolean: and, or, not

  • String operations via Jinja2 filters

Examples:

# Numeric comparisons
- condition: "{{ balance | int > 1000 }}"

# String operations
- condition: "{{ username | lower == 'admin' }}"

# List operations
- condition: "{{ user_id in [1, 2, 3, 4, 5] }}"

# Complex logic
- condition: "{{ (balance > 100 and role == 'premium') or is_vip }}"

Body Content Matching

when:
  - body_contains: "success"           # Substring match
  - body_not_contains: "error"         # Negative match
  - body_matches: "^HTTP/1\\.1 200"    # Regex match
  - body_equals: '{"status": "ok"}'    # Exact match

Examples:

# Check for success message
- when:
    - status: 200
    - body_contains: "operation completed"
  goto: success

# Detect errors in response
- when:
    - body_contains: "error"
    - body_not_contains: "no errors"
  goto: error_handler

# Regex pattern matching
- when:
    - body_matches: "user_id\":\\s*(\\d+)"
  goto: user_found

Header Checks

when:
  # Simple existence check
  - header_exists: "X-Auth-Token"
  - header_not_exists: "X-Debug"

  # Value matching
  - header_equals:
      name: "Content-Type"
      value: "application/json"

  # Substring matching
  - header_contains:
      name: "Content-Type"
      value: "json"

  # Numeric comparison
  - header_gt:
      name: "Content-Length"
      value: 1000

Available header conditions:

  • header_exists - Check if header is present

  • header_not_exists - Check if header is absent

  • header_equals - Exact value match

  • header_contains - Substring match

  • header_gt - Greater than (numeric)

  • header_lt - Less than (numeric)

  • header_gte - Greater than or equal (numeric)

  • header_lte - Less than or equal (numeric)

Examples:

# Rate limiting detection
- when:
    - header_exists: "X-RateLimit-Remaining"
    - header_lt:
        name: "X-RateLimit-Remaining"
        value: 10
  goto: slow_down

# Content type validation
- when:
    - header_equals:
        name: "Content-Type"
        value: "application/json"
  goto: parse_json

# Check response size
- when:
    - header_gt:
        name: "Content-Length"
        value: 10000
  goto: large_response

Response Time Analysis

when:
  - response_time_lt: 100    # Less than 100ms
  - response_time_gt: 1000   # Greater than 1000ms
  - response_time_between:
      min: 100
      max: 500

Examples:

# Fast response detection
- when:
    - response_time_lt: 50
  goto: cache_hit

# Slow response detection
- when:
    - response_time_gt: 2000
  goto: timeout_handling

# Acceptable range
- when:
    - response_time_between:
        min: 100
        max: 500
  goto: normal_flow

Combining Conditions

Multiple conditions in a when block are implicitly AND’ed:

# All conditions must be true
- when:
    - status: 200
    - body_contains: "success"
    - header_exists: "X-Auth-Token"
    - condition: "{{ balance > 100 }}"
  goto: all_conditions_met

For OR logic, use separate when blocks:

# If status is 200 OR 201
- when:
    - status: 200
  goto: success

- when:
    - status: 201
  goto: success

Or use Jinja2 conditions:

- when:
    - condition: "{{ status == 200 or status == 201 }}"
  goto: success

Complex Examples

Role-Based Routing

states:
  check_user:
    request: |
      GET /api/user/profile HTTP/1.1
      Authorization: Bearer {{ token }}

    extract:
      role:
        type: jpath
        pattern: "$.role"
      permissions:
        type: jpath
        pattern: "$.permissions"

    next:
      # Admin with full access
      - when:
          - status: 200
          - condition: "{{ role == 'admin' }}"
          - condition: "{{ 'full_access' in permissions }}"
        goto: admin_panel

      # Moderator
      - when:
          - status: 200
          - condition: "{{ role == 'moderator' }}"
        goto: moderator_panel

      # Regular user
      - when:
          - status: 200
        goto: user_dashboard

      # Unauthorized
      - when:
          - status_in: [401, 403]
        goto: login

Error Detection

states:
  api_call:
    request: |
      POST /api/process HTTP/1.1

    next:
      # Success
      - when:
          - status_range: [200, 299]
          - body_contains: "success"
          - body_not_contains: "error"
        goto: success

      # Rate limited
      - when:
          - status: 429
          - header_exists: "Retry-After"
        goto: rate_limited

      # Server error
      - when:
          - status_range: [500, 599]
        goto: server_error

      # Validation error
      - when:
          - status: 400
          - body_contains: "validation"
        goto: validation_error

      # Unknown error
      - otherwise:
        goto: unknown_error

Race Condition Detection

states:
  race_attack:
    race:
      threads: 20

    request: |
      POST /api/redeem HTTP/1.1

    extract:
      remaining:
        type: jpath
        pattern: "$.vouchers_remaining"

    next:
      # Multiple successes = vulnerability
      - when:
          - status: 200
          - body_contains: "redeemed"
          - condition: "{{ successful_requests | int > 1 }}"
        goto: vulnerability_found

      # Single success = expected
      - when:
          - status: 200
          - condition: "{{ successful_requests | int == 1 }}"
        goto: no_vulnerability

      # All failed = race condition prevented
      - when:
          - condition: "{{ successful_requests | int == 0 }}"
        goto: protected

Performance-Based Routing

states:
  load_test:
    request: |
      GET /api/heavy-operation HTTP/1.1

    next:
      # Fast cache hit
      - when:
          - status: 200
          - response_time_lt: 100
          - header_exists: "X-Cache-Hit"
        goto: cache_hit

      # Slow but successful
      - when:
          - status: 200
          - response_time_gt: 1000
        goto: slow_response

      # Timeout
      - when:
          - response_time_gt: 5000
        goto: timeout

      # Normal response
      - when:
          - status: 200
          - response_time_between:
              min: 100
              max: 1000
        goto: normal_response

Best Practices

  1. Order Matters

    When blocks are evaluated in order. Put more specific conditions first:

    # Good - specific first
    - when:
        - status: 200
        - condition: "{{ role == 'admin' }}"
      goto: admin
    
    - when:
        - status: 200
      goto: user
    
    # Bad - general first (admin never reached)
    - when:
        - status: 200
      goto: user
    
    - when:
        - status: 200
        - condition: "{{ role == 'admin' }}"
      goto: admin
    
  2. Always Provide Fallback

    Use otherwise to handle unexpected cases:

    - when:
        - status: 200
      goto: success
    
    - otherwise:
      goto: error_handler
    
  3. Keep Conditions Simple

    Break complex logic into multiple when blocks:

    # Good
    - when:
        - status: 200
      goto: check_role
    
    # In check_role state
    - when:
        - condition: "{{ role == 'admin' }}"
      goto: admin
    
    # Bad - too complex
    - when:
        - condition: "{{ (status == 200 and role == 'admin') or (status == 201 and role == 'moderator') }}"
      goto: privileged
    
  4. Test Extracted Variables

    Ensure variables exist before using in conditions:

    extract:
      balance:
        type: jpath
        pattern: "$.balance"
        default: 0  # Provide default
    
    next:
      - when:
          - condition: "{{ balance | int > 100 }}"
        goto: high_balance
    
  5. Use Descriptive State Names

    Make flow easy to understand:

    - when:
        - status: 429
      goto: handle_rate_limit  # Not goto: state_x
    

Common Patterns

Authentication Flow

next:
  - when:
      - status: 200
      - body_contains: "token"
    goto: authenticated

  - when:
      - status: 401
    goto: login_failed

  - when:
      - status: 429
    goto: rate_limited

  - otherwise:
    goto: error

API Error Handling

next:
  - when:
      - status_range: [200, 299]
    goto: success

  - when:
      - status_range: [400, 499]
    goto: client_error

  - when:
      - status_range: [500, 599]
    goto: server_error

  - otherwise:
    goto: unknown_error

See Also