Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 226 additions & 47 deletions docs/features/conditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,112 +2,282 @@

Execute steps conditionally based on expressions.

## Basic Conditions
## The `if` Field

Skip steps based on expressions:
The recommended way to conditionally execute steps is with the `if` field:

```yaml
steps:
- id: check
llm:
prompt: "Is it raining? Answer yes or no."
- id: bring_umbrella
condition: ${steps.check.output == "yes"}
if: "{{.steps.check.output}} == 'yes'"
llm:
prompt: "Remind me to bring an umbrella"
```

The `bring_umbrella` step only runs if the condition is true.
The `bring_umbrella` step only runs if the condition evaluates to `true`. If `false`, the step is skipped (not errored).

### Basic Syntax

The `if` field accepts a string expression that must evaluate to boolean:

```yaml
# Template syntax (recommended)
if: "{{.steps.check.stdout}} == 'value'"

# Bare syntax (also supported)
if: "steps.check.stdout == 'value'"

# Workflow inputs
if: "{{.inputs.mode}} == 'strict'"
if: "inputs.mode == 'strict'"

# Boolean values
if: "{{.inputs.enabled}}"
if: "inputs.enabled == true"
```

### Relationship to `condition.expression`

The `if` field is shorthand for `condition.expression`. These are equivalent:

```yaml
# Using if (recommended)
- id: step1
if: "inputs.enabled == true"

# Using condition
- id: step2
condition:
expression: "inputs.enabled == true"
```

You cannot use both `if` and `condition` on the same step.

## Basic Conditions (Legacy Syntax)

The older `condition` syntax is still supported:

```yaml
steps:
- id: check
llm:
prompt: "Is it raining? Answer yes or no."
- id: bring_umbrella
condition: ${steps.check.output == "yes"}
llm:
prompt: "Remind me to bring an umbrella"
```

## Condition Syntax
## Expression Operators

Conditions use simple expressions:
The `if` field supports standard comparison and boolean operators:

```yaml
# Equality
condition: ${steps.step1.output == "expected"}
if: "{{.steps.step1.output}} == 'expected'"

# Inequality
condition: ${steps.step1.output != "skip"}
if: "{{.steps.step1.output}} != 'skip'"

# Numeric comparison
condition: ${steps.count.output > 10}
if: "{{.steps.count.output}} > 10"

# Boolean
condition: ${inputs.enabled == true}
if: "{{.inputs.enabled}} == true"
if: "inputs.enabled"

# Contains
condition: ${steps.check.output | contains("keyword")}
# String contains
if: "contains({{.steps.check.output}}, 'keyword')"
```

## Multiple Conditions
### Comparison Operators
- `==` - Equal
- `!=` - Not equal
- `<` - Less than
- `>` - Greater than
- `<=` - Less than or equal
- `>=` - Greater than or equal

### Boolean Operators

Combine conditions with logical operators:

```yaml
# AND
condition: ${steps.check1.output == "yes" && steps.check2.output == "yes"}
# AND - All conditions must be true
if: "inputs.env == 'prod' && inputs.region == 'us'"

# OR - At least one condition must be true
if: "inputs.region == 'us' || inputs.region == 'eu'"

# OR
condition: ${steps.check1.output == "yes" || steps.check2.output == "yes"}
# NOT - Negate a condition
if: "!inputs.dry_run"

# NOT
condition: ${!(steps.check.output == "skip")}
# Complex expressions
if: "inputs.env == 'prod' && (inputs.region == 'us' || inputs.region == 'eu') && !inputs.dry_run"
```

## Input-Based Conditions
### Array Membership

Conditionally execute based on inputs:
Check if a value exists in an array:

```yaml
# Using 'in' operator
if: "'feature' in inputs.features"

# Check multiple values
if: "'security' in inputs.personas || 'compliance' in inputs.personas"
```

## Common Patterns

### Input-Based Conditions

Branch based on workflow inputs:

```yaml
inputs:
- name: environment
type: string

steps:
- id: production_deploy
condition: ${inputs.environment == "production"}
if: "inputs.environment == 'production'"
shell:
command: ./deploy-production.sh

- id: staging_deploy
condition: ${inputs.environment == "staging"}
if: "inputs.environment == 'staging'"
shell:
command: ./deploy-staging.sh
```

## Default Values
### Conditional Based on Previous Step Output

Handle missing data with defaults:
Execute steps only if previous steps produce specific output:

```yaml
steps:
- id: optional_step
condition: ${inputs.runOptional | default(false)}
- id: check_tests
shell:
command: test -d tests && echo 'has_tests' || echo 'no_tests'

- id: run_tests
if: "{{.steps.check_tests.stdout}} == 'has_tests'"
shell:
command: npm test

- id: skip_message
if: "{{.steps.check_tests.stdout}} == 'no_tests'"
llm:
prompt: "This runs if runOptional is true"
prompt: "No tests directory found, skipping test execution"
```

## Condition Functions
### Chained Conditions

Reference multiple previous steps:

```yaml
steps:
- id: validate
shell:
command: ./validate.sh

- id: build
if: "{{.steps.validate.exit_code}} == 0"
shell:
command: ./build.sh

- id: deploy
if: "steps.validate.status == 'success' && steps.build.status == 'success'"
shell:
command: ./deploy.sh
```

Available functions:
### Available Functions

- `contains(substring)` - Check if string contains substring
- `startsWith(prefix)` - Check if string starts with prefix
- `endsWith(suffix)` - Check if string ends with suffix
- `default(value)` - Use default if undefined
- `length()` - Get array/string length
- `isEmpty()` - Check if empty
The `if` field supports these expression functions:

- `contains(string, substring)` - Check if string contains substring
- `startsWith(string, prefix)` - Check if string starts with prefix
- `endsWith(string, suffix)` - Check if string ends with suffix
- `length(array)` - Get array/string length

```yaml
# Length check
condition: ${steps.items.output | length() > 0}
# Check substring
if: "contains({{.steps.result.output}}, 'success')"

# Check array length
if: "length(inputs.features) > 0"

# Empty check
condition: ${!(steps.result.output | isEmpty())}
# Check prefix
if: "startsWith({{.steps.check.output}}, 'ERROR')"
```

## Skipped Steps
## Skipped Step Behavior

When a step is skipped because its `if` condition evaluates to `false`:

- The step has status `skipped` (not `failed` or `success`)
- Output fields are set to zero values: strings → `""`, numbers → `0`, booleans → `false`
- Downstream steps can safely reference the skipped step's outputs (they return empty values)
- Skipped steps don't trigger error handlers
- The workflow continues executing subsequent steps

Example:

When a step is skipped by a condition:
- Its output is undefined
- Steps depending on it are also skipped
- The workflow continues with other steps
```yaml
steps:
- id: optional_check
if: "inputs.run_check == true"
shell:
command: ./check.sh

- id: continue
# This runs even if optional_check was skipped
llm:
prompt: "Continuing workflow..."
```

## Type Safety

The `if` field requires boolean expressions. Non-boolean results cause an error:

```yaml
# ✓ Valid - evaluates to boolean
if: "inputs.enabled == true"
if: "{{.steps.count.output}} > 5"
if: "inputs.enabled"

# ✗ Invalid - evaluates to non-boolean
if: "{{.steps.check.stdout}}" # Error: returns string
if: "{{.steps.count.output}}" # Error: returns number
if: "42" # Error: not a boolean
```

Use explicit comparisons:

```yaml
# Check if output is non-empty
if: "{{.steps.check.stdout}} != ''"

# Check if count is non-zero
if: "{{.steps.count.output}} > 0"
```

### Nil Handling

When a step is skipped, its outputs return nil. Use explicit nil checks:

```yaml
# Safe nil handling
if: "steps.previous.result != nil && steps.previous.result > 5"

# Without nil check (may cause error if previous was skipped)
if: "steps.previous.result > 5"
```

## Error Handling

Expand All @@ -119,13 +289,22 @@ steps:
http:
method: GET
url: https://api.example.com/data

- id: fallback
condition: ${steps.try_api.error != null}
if: "steps.try_api.status != 'success'"
file:
action: read
path: cached-data.json
```

## Best Practices

1. **Use template syntax** - `{{.steps.id.field}}` is more consistent with the rest of Conductor
2. **Check for success explicitly** - `if: "steps.validate.status == 'success'"` is clearer than relying on truthy values
3. **Handle nil safely** - Always check for nil when referencing potentially skipped steps
4. **Keep expressions simple** - Complex logic should be in steps, not conditions
5. **Document intent** - Use step names that explain why something is conditional

## Performance

Conditions are evaluated before step execution. Skipped steps don't consume resources.
Conditions are evaluated before step execution. Skipped steps don't consume resources or call external services.
5 changes: 3 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ Progressive meal planning workflows from the [documentation tutorial](../docs/tu

Complete real-world workflows demonstrating best practices:

| Directory | Description |
|-----------|-------------|
| File | Description |
|------|-------------|
| `showcase/code-review.yaml` | Multi-persona code review |
| `showcase/conditional-workflow.yaml` | Conditional execution patterns with `if` field |
| `showcase/issue-triage.yaml` | GitHub issue classification |
| `showcase/slack-notify.yaml` | Formatted Slack notifications |

Expand Down
Loading
Loading