Skip to main content
Control execution with conditions, handlers, and decision routing.

Pre-Conditions

Skip a step if a condition is false.
- name: nuclei-scan
  type: bash
  pre_condition: 'fileLength("{{Output}}/live.txt") > 0'
  command: nuclei -l {{Output}}/live.txt -o {{Output}}/vulns.txt

Common Conditions

# File exists
pre_condition: 'fileExists("{{Output}}/targets.txt")'

# File has content
pre_condition: 'fileLength("{{Output}}/hosts.txt") > 0'

# Check export value
pre_condition: '{{host_count}} > 10'

# Check parameter
pre_condition: '{{enable_scan}} == "true"'

# Combine conditions
pre_condition: 'fileExists("{{Output}}/subs.txt") && {{threads}} > 0'

Condition Functions

FunctionDescription
fileExists(path)True if file exists
fileLength(path)Number of non-empty lines
dirLength(path)Number of directory entries
isEmpty(str)True if string is empty
contains(str, substr)True if string contains substring

Step Dependencies (DAG Execution)

Steps can declare dependencies on other steps using the depends_on field. This enables:
  • Parallel execution of independent steps
  • Automatic ordering based on dependencies
  • DAG (Directed Acyclic Graph) execution

Basic Dependencies

steps:
  - name: subfinder
    type: bash
    command: "subfinder -d {{Target}} -o {{Output}}/subfinder.txt"

  - name: amass
    type: bash
    command: "amass enum -d {{Target}} -o {{Output}}/amass.txt"

  - name: merge-results
    type: function
    depends_on:
      - subfinder
      - amass
    functions:
      - "appendFile('{{Output}}/all-subs.txt', '{{Output}}/subfinder.txt')"
      - "appendFile('{{Output}}/all-subs.txt', '{{Output}}/amass.txt')"
In this example:
  • subfinder and amass run in parallel (no dependencies)
  • merge-results waits for both to complete before executing

DAG Execution

The executor builds a dependency graph and uses topological sorting (Kahn’s algorithm) to determine execution order:
┌───────────┐     ┌───────────┐
│ subfinder │     │   amass   │
└─────┬─────┘     └─────┬─────┘
      │                 │
      │  ┌──────────────┘
      │  │
      ▼  ▼
┌─────────────┐
│merge-results│
└──────┬──────┘


┌─────────────┐
│ http-probe  │
└─────────────┘
Steps at the same “level” (no dependencies between them) execute concurrently.

Multiple Dependencies

- name: final-report
  type: bash
  depends_on:
    - vuln-scan
    - port-scan
    - screenshot
  command: "generate-report {{Output}}"
The step waits for all listed dependencies to complete successfully.

Cascade Failure

If a dependency fails:
  1. The dependent step is marked as failed (not executed)
  2. All downstream steps are also marked as failed
  3. Independent branches continue execution
# If subfinder fails:
# - merge-results is skipped
# - amass continues (independent)

Dependencies vs Sequential Execution

Without depends_on, steps execute sequentially in order:
steps:
  - name: step1     # Runs first
    type: bash
    command: "..."

  - name: step2     # Runs second (waits for step1)
    type: bash
    command: "..."
With depends_on, steps can run in parallel:
steps:
  - name: step1     # Runs first (parallel with step2)
    type: bash
    command: "..."

  - name: step2     # Runs first (parallel with step1)
    type: bash
    command: "..."

  - name: step3     # Waits for both
    type: bash
    depends_on: [step1, step2]
    command: "..."

Linter Validation

The workflow linter validates dependencies:
RuleDescription
invalid-depends-onDependency references non-existent step
circular-dependencyCircular dependencies detected
osmedeus workflow lint my-workflow.yaml

Flow Module Dependencies

Flows also support depends_on for modules:
kind: flow
name: full-pipeline

modules:
  - name: subdomain-enum
    path: modules/subdomain-enum.yaml

  - name: port-scan
    path: modules/port-scan.yaml

  - name: http-probe
    path: modules/http-probe.yaml
    depends_on:
      - subdomain-enum
      - port-scan

Decision Routing

Route to different steps based on variable values using switch/case syntax.
steps:
  - name: check-hosts
    type: bash
    command: wc -l < {{Output}}/hosts.txt
    exports:
      count: "{{stdout}}"
    decision:
      switch: "{{count}}"
      cases:
        "0": { goto: no-hosts-found }
      default: { goto: continue-scan }

  - name: no-hosts-found
    type: function
    function: log_warning("No hosts found, skipping scan")
    decision:
      switch: "true"
      cases:
        "true": { goto: _end }    # Special: end workflow

  - name: continue-scan
    type: bash
    command: nuclei -l {{Output}}/hosts.txt -t {{Data}}/templates/

Decision Syntax

decision:
  switch: "{{variable}}"      # Template expression to evaluate
  cases:                       # Map of values to actions
    "value1": { goto: step-a }
    "value2": { goto: step-b }
  default: { goto: fallback }  # Optional: when no case matches
  • switch: Template variable evaluated at runtime (exact string match)
  • cases: Map of string values to goto targets
  • default: Fallback when no case matches (optional)
  • goto: Target step name or _end to terminate

Special Goto Targets

TargetDescription
_endEnd workflow immediately
step-nameJump to named step

Success Handlers

Execute actions when a step succeeds.
- name: scan
  type: bash
  command: nuclei -l {{Output}}/hosts.txt -o {{Output}}/vulns.txt
  on_success:
    - action: log
      message: "Scan completed successfully"

    - action: export
      key: scan_status
      value: "completed"

    - action: notify
      message: "Vulnerability scan finished for {{target}}"

Available Actions

ActionDescriptionParameters
logLog a messagemessage
exportExport a valuekey, value
runRun a commandcommand
notifySend notificationmessage
continueContinue execution-
on_success:
  - action: log
    message: "Step completed"

  - action: export
    key: result
    value: "success"

  - action: run
    command: echo "Done" >> {{Output}}/log.txt

  - action: notify
    message: "{{target}} scan finished"

Error Handlers

Handle step failures.
- name: risky-scan
  type: bash
  command: aggressive-tool {{target}}
  on_error:
    - action: log
      message: "Scan failed, continuing with fallback"

    - action: continue    # Don't stop workflow

    - action: run
      command: fallback-tool {{target}}

Error Action Types

ActionDescription
logLog error message
abortStop workflow (default)
continueContinue to next step
runRun recovery command
notifySend error notification
on_error:
  - action: abort       # Stop workflow on error

# OR

on_error:
  - action: continue    # Ignore error, continue

Combined Example

steps:
  - name: enumerate
    type: bash
    command: subfinder -d {{target}} -o {{Output}}/subs.txt
    exports:
      sub_count: "{{stdout}}"
    on_success:
      - action: log
        message: "Found subdomains"
    on_error:
      - action: log
        message: "Enumeration failed"
      - action: continue

  - name: validate-results
    type: function
    function: fileLength("{{Output}}/subs.txt")
    exports:
      count: "{{result}}"
    decision:
      switch: "{{count}}"
      cases:
        "0": { goto: no-results }
      default: { goto: probe-hosts }

  - name: no-results
    type: function
    function: log_warning("No subdomains found for {{target}}")
    decision:
      switch: "true"
      cases:
        "true": { goto: _end }

  - name: probe-hosts
    type: bash
    pre_condition: 'fileLength("{{Output}}/subs.txt") > 0'
    command: httpx -l {{Output}}/subs.txt -o {{Output}}/live.txt
    on_success:
      - action: export
        key: probe_status
        value: "done"
      - action: notify
        message: "Probing complete for {{target}}"
    on_error:
      - action: log
        message: "HTTP probing failed"
      - action: abort

  - name: screenshot
    type: bash
    pre_condition: 'fileLength("{{Output}}/live.txt") > 0 && {{probe_status}} == "done"'
    command: gowitness file -f {{Output}}/live.txt -P {{Output}}/screenshots

Flow-Level Conditions

Conditional module execution in flows:
kind: flow
name: conditional-flow

params:
  - name: target
  - name: enable_active
    default: "false"

modules:
  - name: passive-recon
    path: modules/passive.yaml

  - name: active-scan
    path: modules/active.yaml
    depends_on: [passive-recon]
    condition: '{{enable_active}} == "true"'

  - name: vuln-scan
    path: modules/vuln.yaml
    depends_on: [passive-recon]
    condition: 'fileLength("{{Output}}/live.txt") > 0'

Branching Patterns

If-Then-Else

- name: check
  type: function
  function: fileLength("{{Output}}/data.txt")
  exports:
    has_data: "{{result}}"
  decision:
    switch: "{{has_data}}"
    cases:
      "0": { goto: handle-empty }
    default: { goto: process-data }

- name: process-data
  type: bash
  command: process {{Output}}/data.txt
  decision:
    switch: "true"
    cases:
      "true": { goto: finalize }

- name: handle-empty
  type: function
  function: log_warning("No data to process")
  decision:
    switch: "true"
    cases:
      "true": { goto: finalize }

- name: finalize
  type: function
  function: log_info("Workflow complete")

Early Exit

- name: validate
  type: function
  function: fileExists("{{Output}}/required.txt")
  exports:
    valid: "{{result}}"
  decision:
    switch: "{{valid}}"
    cases:
      "false": { goto: _end }       # Exit if invalid
    default: { goto: continue-scan }

- name: continue-scan
  type: bash
  command: scan {{target}}

Loop with Retry

- name: attempt-scan
  type: bash
  command: flaky-scanner {{target}}
  exports:
    attempt: "1"
    failed: "false"
  on_error:
    - action: export
      key: failed
      value: "true"
    - action: continue

- name: retry-check
  type: function
  function: log_info("Checking retry status")
  decision:
    switch: "{{failed}}"
    cases:
      "false": { goto: success }
      "true": { goto: check-attempts }
    default: { goto: success }

- name: check-attempts
  type: function
  function: log_info("Attempt {{attempt}}")
  decision:
    switch: "{{attempt}}"
    cases:
      "3": { goto: give-up }
    default: { goto: retry-scan }

- name: retry-scan
  type: bash
  command: flaky-scanner {{target}} --retry
  exports:
    attempt: "{{parseInt({{attempt}}) + 1}}"
  on_error:
    - action: continue
  decision:
    switch: "true"
    cases:
      "true": { goto: retry-check }

Best Practices

  1. Always check file existence before processing
    pre_condition: 'fileExists("{{Output}}/input.txt")'
    
  2. Use meaningful log messages
    on_success:
      - action: log
        message: "Found {{count}} subdomains for {{target}}"
    
  3. Handle errors gracefully
    on_error:
      - action: log
        message: "Step failed, attempting fallback"
      - action: continue
    
  4. Use decision routing for complex logic
    decision:
      switch: "{{dataset_size}}"
      cases:
        "large": { goto: large-dataset-handler }
      default: { goto: standard-handler }
    
  5. End workflows cleanly
    decision:
      switch: "{{fatal_error}}"
      cases:
        "true": { goto: _end }
    

Next Steps