# How to Define a Simple YAML Workflow for Probe

> Learn to define a simple YAML workflow for Probe a Go library that executes jobs and steps with dependencies retries and conditional skipping.

- Repository: [Tomohisa Oda/probe](https://github.com/linyows/probe)
- Tags: how-to-guide
- Published: 2026-03-06

---

**Probe is a lightweight Go library that reads YAML workflow descriptions, expands variables using Go-template syntax, and executes jobs and steps with built-in support for dependencies, retries, and conditional skipping.**

Defining a YAML workflow for Probe allows you to automate sequences of actions with minimal configuration. The `linyows/probe` repository implements a declarative execution engine that parses workflow files, handles variable interpolation, and manages job dependencies through a simple YAML interface.

## Core Architecture and Data Structures

Understanding the underlying data structures helps you write valid YAML that maps correctly to Probe's execution model.

### Top-Level Workflow Configuration

The `Workflow` struct, defined in [`workflow.go`](https://github.com/linyows/probe/blob/main/workflow.go) (lines 8-19), serves as the root container for your definition. It accepts a global `name`, optional `description`, and a `vars` map for variable definitions that become available throughout the workflow.

When Probe loads your file via `Probe.Load()` in [`probe.go`](https://github.com/linyows/probe/blob/main/probe.go) (lines 72-90), it validates the YAML against these struct tags using `go-playground/validator`. The `Workflow.evalVars()` method (lines 84-105 in [`workflow.go`](https://github.com/linyows/probe/blob/main/workflow.go)) processes top-level variables immediately after loading, making them available for Go-template expansion in subsequent jobs and steps.

### Job Definitions and Dependencies

Each entry in the `jobs` array maps to a `Job` struct defined in [`job.go`](https://github.com/linyows/probe/blob/main/job.go) (lines 9-16). Key fields include:

- **name** – Display identifier that supports template expansion
- **id** – Optional unique identifier for dependency references
- **needs** – Array of job IDs that must complete successfully before this job starts
- **skipif** – Boolean expression that conditionally omits the entire job
- **steps** – Ordered list of actions to execute

The `JobScheduler`, instantiated within `Workflow.Start()` (lines 22-61 in [`workflow.go`](https://github.com/linyows/probe/blob/main/workflow.go)), resolves `needs` dependencies to ensure proper execution order.

### Step Configuration and Actions

The `Step` struct in [`step.go`](https://github.com/linyows/probe/blob/main/step.go) (lines 18-36) represents the atomic unit of work. Every step requires a `uses` field specifying the action name, and accepts a `with` map for parameters. Advanced configuration includes:

- **test** – Go-template expression evaluating to boolean; failure marks the step as error
- **skipif** – Boolean expression to conditionally bypass the step
- **wait** – Duration delay (e.g., `"5s"` or numeric seconds)
- **outputs** – Key-value pairs saved to the shared `Outputs` object
- **retry** – Configuration for `max_attempts`, `interval`, and `initial_delay`
- **timeout** – Override for the default 5-minute execution limit

The `Step.Do()` method (lines 39-58 in [`step.go`](https://github.com/linyows/probe/blob/main/step.go)) orchestrates preparation, execution via `Step.executeActionWithRetry` (lines 41-94), result handling, and output recording.

## Variable Expansion and Templating

Probe uses Go's `text/template` package for dynamic content. Define variables in the top-level `vars` section:

```yaml
vars:
  host: localhost
  port: 8080

```

Reference these anywhere using dot notation: `{{ .host }}` or `{{ .port }}`. The expansion occurs during `Workflow.evalVars()` for global variables and during `Job.Start()` for job-specific name expansion.

## Execution Pipeline

When you run `probe workflow.yml`, the execution follows this precise sequence:

1. **Loading Phase** – `Probe.Load()` reads the YAML file, validates struct constraints, and unmarshals into the `Workflow` struct
2. **Initialization Phase** – `Workflow.Start()` creates the printer, initializes shared `Outputs`, evaluates variables, and builds the `JobScheduler`
3. **Job Execution** – `Job.Start()` expands the job name, checks `skipif` conditions, and iterates through steps
4. **Step Execution** – `Step.Do()` prepares the context, executes the action with retry logic, processes results, and saves declared outputs

Each `StepResult` is collected for final reporting via the `Printer` implementation in [`printer.go`](https://github.com/linyows/probe/blob/main/printer.go).

## Practical YAML Workflow Examples

### Minimal Workflow Example

The smallest functional workflow uses the built-in `echo` action to print a message. Save this as [`simple.yml`](https://github.com/linyows/probe/blob/main/simple.yml):

```yaml
name: Simple demo
vars:
  greeting: Hello
jobs:
  - name: Echo greeting
    steps:
      - uses: echo
        with:
          message: "{{ .greeting }}, Probe!"

```

This workflow defines a single job with one step. The `greeting` variable interpolates into the `message` parameter before execution.

### Conditional Execution and Job Dependencies

For complex scenarios, combine conditional skipping with dependency chains:

```yaml
name: Conditional demo
vars:
  run_extra: false
  api_endpoint: https://api.example.com
jobs:
  - id: setup
    name: Initial setup
    steps:
      - uses: echo
        with:
          message: "Setting up environment"
          
  - id: main
    name: Main processing
    needs: [setup]
    steps:
      - uses: echo
        with:
          message: "Processing on {{ .api_endpoint }}"
          
  - id: cleanup
    name: Cleanup tasks
    needs: [main]
    skipif: "{{ not .run_extra }}"
    steps:
      - uses: echo
        with:
          message: "Running cleanup"
        retry:
          max_attempts: 3
          interval: 5s

```

The `cleanup` job depends on `main`, which depends on `setup`. The scheduler respects this chain via the `JobScheduler`. The `cleanup` job skips entirely because `run_extra` evaluates to false.

## Running Probe from the Command Line

Execute your workflow using the probe CLI:

```bash
probe simple.yml        # Standard execution with concise reporting

probe -v simple.yml     # Verbose mode showing spinners and step details

```

The command internally invokes `Probe.Load()` followed by `Workflow.Start()`, rendering progress and final results through the printer interface.

## Summary

- Probe workflows are defined in YAML files that map directly to the `Workflow`, `Job`, and `Step` structs in the `linyows/probe` source code
- Top-level `vars` support Go-template expansion via `Workflow.evalVars()` and become available to all jobs and steps
- Jobs declare dependencies using the `needs` field, resolved by the `JobScheduler` during `Workflow.Start()`
- Steps support conditional execution via `skipif`, retry logic via the `retry` map, and output collection for downstream consumption
- The execution pipeline moves from `Probe.Load()` through `Workflow.Start()` to individual `Job.Start()` and `Step.Do()` calls

## Frequently Asked Questions

### How does Probe handle variable interpolation in YAML workflows?

Probe evaluates top-level `vars` immediately after loading via `Workflow.evalVars()` (lines 84-105 in [`workflow.go`](https://github.com/linyows/probe/blob/main/workflow.go)). These variables expand using Go-template syntax (`{{ .varname }}`) in job names, step parameters, and conditional expressions. The expansion occurs before job execution, ensuring all references resolve to literal values before `Step.Do()` processes the action.

### What file format and validation does Probe require for workflow definitions?

Probe accepts files with `.yml` or `.yaml` extensions. The `Probe.Load()` function in [`probe.go`](https://github.com/linyows/probe/blob/main/probe.go) (lines 72-90) unmarshals YAML into the `Workflow` struct and validates required fields using `go-playground/validator`. Invalid YAML syntax or missing required fields trigger validation errors before execution begins.

### How do I configure retry logic for steps that may fail intermittently?

Add a `retry` configuration map to any step definition. According to [`step.go`](https://github.com/linyows/probe/blob/main/step.go) (lines 41-94), you can specify `max_attempts`, `interval` (as duration strings like `"10s"`), and `initial_delay`. The `Step.executeActionWithRetry` method implements the backoff logic, re-attempting the action until success or maximum attempts are exhausted.

### Can I skip specific jobs or steps based on runtime conditions?

Yes. Both `Job` and `Step` structs support a `skipif` field accepting a Go-template boolean expression. In [`job.go`](https://github.com/linyows/probe/blob/main/job.go) (lines 19-48), `Job.Start()` evaluates the job-level expression before processing steps. Similarly, `Step.Do()` checks step-level `skipif` during preparation. If the expression evaluates to true, Probe omits that job or step from execution and continues with the remaining workflow.