Skip to main content
You can find comprehensive examples of all available workflow types and steps in the Workflow test suites here.You can also use the skills provided at osmedeus/osmedeus-skills. These can help your AI agent generate workflows automatically for you.
This guide walks you through creating workflows in Osmedeus, from basic concepts to advanced patterns.

Workflow Kinds

Osmedeus supports two workflow kinds:
KindPurpose
moduleSingle execution unit with steps
flowOrchestrates multiple modules

Basic Structure

Module Workflow

name: my-first-workflow
kind: module
description: A simple workflow example
tags: example,tutorial

params:
  - name: custom_param
    required: false
    default: "default_value"

steps:
  - name: hello-world
    type: bash
    command: echo "Hello, {{Target}}!"

Flow Workflow

name: my-flow
kind: flow
description: Orchestrates multiple modules

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

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

Step Types

bash - Execute Shell Commands

# Single command
- name: simple-command
  type: bash
  command: echo "Hello {{Target}}"

# Multiple sequential commands
- name: multiple-commands
  type: bash
  commands:
    - mkdir -p {{Output}}/results
    - echo "{{Target}}" > {{Output}}/target.txt

# Parallel commands
- name: parallel-commands
  type: bash
  parallel_commands:
    - 'curl -s https://api1.example.com'
    - 'curl -s https://api2.example.com'

# Structured arguments (for tools like nuclei)
- name: nuclei-scan
  type: bash
  command: nuclei
  speed_args: '-c {{threads}}'
  config_args: '-t /templates'
  input_args: '-l {{Output}}/urls.txt'
  output_args: '-o {{Output}}/nuclei.json'

function - JavaScript Utility Functions

# Single function
- name: check-file
  type: function
  function: 'fileExists("{{Output}}/results.txt")'

# Multiple functions
- name: process-results
  type: function
  functions:
    - 'log_info("Processing results...")'
    - 'var count = fileLength("{{Output}}/results.txt")'
    - 'log_info("Found " + count + " results")'

# Parallel functions
- name: parallel-logging
  type: function
  parallel_functions:
    - 'log_info("Task A")'
    - 'log_info("Task B")'

parallel-steps - Run Steps Concurrently

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

    - name: assetfinder
      type: bash
      command: assetfinder {{Target}} > {{Output}}/assetfinder.txt

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

foreach - Loop Over Input

- name: scan-subdomains
  type: foreach
  input: "{{Output}}/subdomains.txt"
  variable: subdomain
  threads: 10

  step:
    name: httpx-probe
    type: bash
    command: 'httpx -u [[subdomain]] -silent'
Note: Use [[variable]] syntax inside foreach loops to avoid template conflicts.

http - Make HTTP Requests

- name: api-call
  type: http
  url: "https://api.example.com/scan"
  method: POST
  headers:
    Content-Type: application/json
    Authorization: "Bearer {{api_token}}"
  request_body: |
    {
      "target": "{{Target}}",
      "options": {"deep": true}
    }
  exports:
    response_data: "{{response.body}}"

llm - AI-Powered Analysis

- name: ai-analysis
  type: llm
  messages:
    - role: system
      content: "You are a security analyst."
    - role: user
      content: "Analyze the scan results for {{Target}}"
  llm_config:
    model: gpt-4
    max_tokens: 1000
    temperature: 0.7
  exports:
    analysis: "{{llm_step_content}}"

agent - Agentic LLM Execution

- name: analyze-target
  type: agent
  query: "Enumerate subdomains of {{Target}} and summarize findings."
  system_prompt: "You are a security reconnaissance agent."
  max_iterations: 10
  agent_tools:
    - preset: bash
    - preset: read_file
    - preset: save_content
  memory:
    max_messages: 30
    persist_path: "{{Output}}/agent/conversation.json"
  exports:
    findings: "{{agent_content}}"
The agent step type creates an autonomous tool-calling loop. The agent receives a task, plans its approach, calls tools iteratively, and produces a final answer. Key fields:
  • query — task prompt for the agent
  • max_iterations — maximum tool-calling loop iterations (required)
  • agent_tools — list of preset or custom tools (e.g., bash, read_file, save_content, grep_regex, http_get)
  • memory — conversation memory configuration
  • exports — use {{agent_content}} for the final response text
See Step Types - agent for the full reference.

Template Variables

Built-in Variables

VariableDescription
{{Target}}Current target
{{Output}}Output directory for this run
{{BaseFolder}}Osmedeus installation directory
{{Binaries}}Binary tools directory
{{Data}}Data directory (wordlists, etc.)
{{Workflows}}Workflows directory
{{Workspaces}}Workspaces directory
{{threads}}Thread count based on tactic
{{Version}}Osmedeus version
{{PlatformOS}}Operating system (linux, darwin, windows)
{{PlatformArch}}CPU architecture (amd64, arm64)
{{PlatformInDocker}}"true" if running in Docker
{{PlatformInKubernetes}}"true" if running in Kubernetes
{{PlatformCloudProvider}}Cloud provider (aws, gcp, azure, local)

Foreach Loop Variables

Use double brackets [[variable]] inside foreach loops:
- name: process-items
  type: foreach
  input: "{{Output}}/items.txt"
  variable: item
  step:
    type: bash
    command: 'process [[item]] --output {{Output}}/[[item]].json'

Exports and Variable Passing

Pass data between steps using exports:
- name: count-results
  type: bash
  command: wc -l {{Output}}/results.txt | awk '{print $1}'
  exports:
    result_count: "output"  # Special: captures stdout

- name: log-count
  type: function
  function: 'log_info("Found {{result_count}} results")'

Decision Routing

Branch workflow execution based on conditions. Decisions support two modes: switch/case (exact string matching) and conditions (boolean expressions).

Switch/Case Mode

Match a variable’s value against exact strings:
- name: detect-type
  type: bash
  command: 'detect-target-type {{Target}}'
  exports:
    target_type: "output"

  decision:
    switch: "{{target_type}}"
    cases:
      "domain":
        goto: subdomain-enum
      "ip":
        goto: port-scan
      "url":
        goto: web-scan
    default:
      goto: generic-recon

- name: subdomain-enum
  type: bash
  command: subfinder -d {{Target}}

- name: port-scan
  type: bash
  command: nmap {{Target}}

# Use goto: _end to terminate workflow early

Inline Actions in Cases

Each case can run inline commands or functions instead of (or in addition to) a goto. When combined with goto, inline actions execute first, then the jump happens. Available case fields:
FieldTypeDescription
gotostringJump to a step by name, or _end to terminate
commandstringRun a single bash command inline
commandsstring[]Run multiple bash commands in sequence
functionstringExecute a single utility function
functionsstring[]Execute multiple utility functions in sequence
- name: setup-scan
  type: bash
  command: echo "Setting up scan"
  exports:
    setup_mode: "{{scan_mode}}"
  decision:
    switch: "{{setup_mode}}"
    cases:
      "quick":
        functions:
          - "log_info('Configuring quick scan')"
          - "log_info('Quick scan configured')"
      "deep":
        functions:
          - "log_info('Configuring deep scan')"
          - "log_info('Deep scan configured')"
        goto: deep-scan-step
      "custom":
        command: echo "Running custom setup"
      "multi":
        commands:
          - echo "Step 1: prepare environment"
          - echo "Step 2: configure tools"
    default:
      function: "log_info('Using default scan mode')"

Conditions Mode

Use JavaScript boolean expressions for more flexible routing. All matching conditions execute (no short-circuit), and the last matching goto wins.
- name: check-results
  type: bash
  command: echo "Checking results"
  decision:
    conditions:
      - if: "{{enable_extra}} && {{target}} != ''"
        function: "log_info('Extra scanning enabled')"
        goto: extra-scan
      - if: "file_length('{{Output}}/results.txt') > 100"
        functions:
          - "log_info('Large result set detected')"
          - "log_info('Switching to batch processing')"
        goto: batch-process
      - if: "file_exists('{{Output}}/errors.log')"
        command: echo "Errors detected, reviewing..."
Conditions support template variables, function calls, and standard JavaScript operators.

Handlers (on_success / on_error)

- name: critical-scan
  type: bash
  command: 'nuclei -u {{Target}}'

  on_success:
    - action: log
      message: "Scan completed for {{Target}}"
    - action: notify
      notify: "Scan finished: {{Target}}"
    - action: export
      name: scan_status
      value: "success"

  on_error:
    - action: log
      message: "Scan failed for {{Target}}"
    - action: continue  # Continue despite error
    # Or: action: abort to stop workflow

Workflow Hooks

Hooks let you run steps before and after the main workflow execution. Use them for setup, cleanup, notifications, or result post-processing.
name: recon-with-hooks
kind: module
description: Reconnaissance with setup and cleanup hooks

hooks:
  pre_scan_steps:
    - name: setup-workspace
      type: bash
      commands:
        - mkdir -p {{Output}}/results
        - echo "Scan started at $(date)" > {{Output}}/scan.log

    - name: notify-start
      type: function
      function: |
        generate_event("{{Workspace}}", "scan.started", "workflow", "status", "{{Target}}")

  post_scan_steps:
    - name: generate-report
      type: function
      function: |
        convert_sarif_to_markdown("{{Output}}/results.sarif", "{{Output}}/report.md")

    - name: notify-complete
      type: function
      function: |
        generate_event("{{Workspace}}", "scan.completed", "workflow", "status", "{{Target}}")

    - name: cleanup-temp
      type: bash
      command: rm -rf {{Output}}/tmp

steps:
  - name: run-scan
    type: bash
    command: nuclei -u {{Target}} -sarif-export {{Output}}/results.sarif

Hook Execution Order

pre_scan_steps  →  steps (main workflow)  →  post_scan_steps
  • pre_scan_steps run before any main steps execute
  • post_scan_steps run after all main steps complete
  • Both support all step types (bash, function, parallel-steps, foreach, etc.)
  • Hook steps have access to the same template variables as main steps

Flow-Level Hooks

Hooks also work on flows. They run once around the entire flow, not per module:
name: full-recon
kind: flow
description: Full recon flow with hooks

hooks:
  pre_scan_steps:
    - name: pre-flight-check
      type: function
      function: 'log_info("Starting flow for " + "{{Target}}")'

  post_scan_steps:
    - name: aggregate-results
      type: bash
      command: cat {{Output}}/*/findings.txt | sort -u > {{Output}}/all-findings.txt

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

Runner Configuration

Host Runner (Default)

runner: host  # Runs locally, this is the default

Docker Runner

runner: docker
runner_config:
  image: ubuntu:22.04
  env:
    API_KEY: "{{api_key}}"
  volumes:
    - "{{Output}}:/output"
  network: host
  persistent: false

SSH Runner

runner: ssh
runner_config:
  host: 192.168.1.100
  port: 22
  user: scanner
  key_file: ~/.ssh/id_rsa

Per-Step Runner Override

- name: docker-step
  type: bash
  step_runner: docker
  step_runner_config:
    image: projectdiscovery/nuclei:latest
  command: 'nuclei -u {{Target}}'

Complete Example

name: basic-recon
kind: module
description: Basic reconnaissance workflow
tags: recon,subdomain,fast

params:
  - name: threads
    default: "10"

steps:
  - name: setup
    type: bash
    commands:
      - mkdir -p {{Output}}/subdomains
      - mkdir -p {{Output}}/urls

  - name: passive-enum
    type: parallel-steps
    parallel_steps:
      - name: subfinder
        type: bash
        command: subfinder -d {{Target}} -silent -o {{Output}}/subdomains/subfinder.txt

      - name: assetfinder
        type: bash
        command: assetfinder --subs-only {{Target}} > {{Output}}/subdomains/assetfinder.txt

  - name: merge-results
    type: bash
    commands:
      - cat {{Output}}/subdomains/*.txt | sort -u > {{Output}}/subdomains.txt
    exports:
      subdomain_count: "output"

  - name: probe-http
    type: foreach
    input: "{{Output}}/subdomains.txt"
    variable: sub
    threads: "{{threads}}"
    step:
      name: httpx
      type: bash
      command: 'httpx -u [[sub]] -silent >> {{Output}}/urls/live.txt'

  - name: summary
    type: function
    functions:
      - 'var total = fileLength("{{Output}}/subdomains.txt")'
      - 'var live = fileLength("{{Output}}/urls/live.txt")'
      - 'log_info("Found " + total + " subdomains, " + live + " live hosts")'

Running Your Workflow

# Run a module workflow
osmedeus run -m basic-recon -t example.com

# Run with custom parameters
osmedeus run -m basic-recon -t example.com --params 'threads=20'

# Dry run (preview without executing)
osmedeus run -m basic-recon -t example.com --dry-run

# Run with verbose output
osmedeus run -m basic-recon -t example.com -v

Next Steps