# How to Share Data Between Steps in Probe Using Step Outputs: A Complete Guide

> Learn to share data between Probe steps using step outputs. This guide shows how to declare outputs and access them with `{{outputs.<id>.<name>}}` for seamless data flow in your workflows.

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

---

**Probe enables data sharing between steps through step outputs, where steps declare an `id` and an `outputs` map to expose values that subsequent steps can access via the `{{outputs.<id>.<name>}}` expression syntax.**

Probe is a lightweight workflow automation tool designed for HTTP-based testing and orchestration. When building multi-step workflows, you need to share data between steps in Probe using step outputs—such as authentication tokens, session IDs, or dynamic values extracted from API responses.

## Understanding Step Outputs in Probe

Probe's output system consists of three core components working together: the **Step** definition, the **Outputs** registry, and the **expression engine**.

### The Core Architecture

The `Step` struct in [`step.go`](https://github.com/linyows/probe/blob/main/step.go) defines how outputs are declared:

- **`Step.Outputs`** (lines 29-30): A map where keys are output names and values are expressions that extract data from the step's response
- **`Step.saveOutputs`** (lines 41-67): Evaluates each output expression using the step's context and writes results to the global `JobContext.Outputs` registry
- **`Outputs` struct** (lines 9-34 in [`outputs.go`](https://github.com/linyows/probe/blob/main/outputs.go)): Central store providing `Set`, `Get`, `GetAll`, and `GetAllWithFlat` methods for managing step-based and flat outputs

### How Data Flows Between Steps

When a step executes, Probe builds a data pipeline:

1. **Step A** finishes execution and calls `saveOutputs()`, which evaluates expressions like `res.body.access_token` and stores results via `Outputs.Set()`
2. **Step B** begins execution via `Step.SetCtx()` (lines 20-34), which loads all previously saved outputs into the step's evaluation context using `j.Outputs.GetAll()`
3. The **expression engine** ([`expr.go`](https://github.com/linyows/probe/blob/main/expr.go)) resolves templates like `{{outputs.login.token}}` against this combined context

## Defining Step Outputs in YAML

To share data between steps in Probe using step outputs, you must explicitly declare which values each step produces.

### Basic Output Declaration

Every producing step requires an `id` field and an `outputs` map:

```yaml
- name: Authenticate with API
  id: login                    # Required identifier for referencing

  uses: http
  with:
    post: /auth/login
    body:
      username: admin
      password: secret
  outputs:
    token: res.body.access_token
    expiry: res.body.expires_in

```

The `id` field creates the namespace (`login`) under which these outputs are stored. Without an `id`, subsequent steps cannot reference the data.

### Extracting Values from HTTP Responses

Probe evaluates output expressions against the step's response context. Common extraction patterns include:

- `res.body.<field>` for JSON response bodies
- `res.headers.<name>` for header values
- `res.code` for HTTP status codes

```yaml
- name: Create Resource
  id: creator
  uses: http
  with:
    post: /api/resources
    body:
      name: test-resource
  outputs:
    resource_id: res.body.id
    created_at: res.body.timestamp

```

## Consuming Step Outputs in Subsequent Steps

Once a step defines outputs, any subsequent step in the same job—or in dependent jobs—can access those values using Probe's expression syntax.

### Referencing Outputs with Expression Syntax

Use the `{{outputs.<step_id>.<output_name>}}` template syntax to retrieve values:

```yaml
- name: Fetch Protected Data
  uses: http
  with:
    get: /api/protected/data
    headers:
      Authorization: "Bearer {{outputs.login.token}}"
  test: res.code == 200

```

Probe's expression engine resolves this template at runtime by looking up `login` in the outputs registry and retrieving the `token` field.

### Flat Access Shortcuts

When no naming conflicts exist, Probe creates flat shortcuts that allow direct access without the step ID prefix:

```yaml
- name: Quick Token Access
  uses: http
  with:
    get: /api/verify
    headers:
      Authorization: "Bearer {{outputs.token}}"

```

According to the source code in [`outputs.go`](https://github.com/linyows/probe/blob/main/outputs.go) (lines 43-52), the `Set` method automatically creates these flat entries when the output name doesn't collide with existing keys. If two steps both output a field named `token`, the flat shortcut is omitted to prevent ambiguity, and you must use the scoped syntax `{{outputs.step1.token}}` or `{{outputs.step2.token}}`.

## Cross-Job Data Sharing with Needs

Probe extends step outputs across job boundaries using the `needs` dependency system. When a job declares `needs`, Probe ensures the dependent job waits for completion and receives access to all outputs from the required jobs.

```yaml
jobs:
- name: Authentication Job
  id: auth
  steps:
  - name: Login
    id: login
    uses: http
    with:
      post: /auth
    outputs:
      token: res.body.access_token

- name: Data Processing
  needs: [auth]              # Waits for auth job to complete

  steps:
  - name: Fetch Data
    uses: http
    with:
      get: /api/data
      headers:
        Authorization: "Bearer {{outputs.login.token}}"

```

The [`workflow.go`](https://github.com/linyows/probe/blob/main/workflow.go) file orchestrates this execution order, ensuring that the `auth` job's `saveOutputs` calls complete before the `Data Processing` job begins building its step contexts via `SetCtx`.

## Conflict Handling and Best Practices

### Output Name Collisions

The `Outputs.Set` method in [`outputs.go`](https://github.com/linyows/probe/blob/main/outputs.go) (lines 27-36) implements conflict detection. If a step's `id` collides with an existing flat output name, the method returns an error and retains only the step-scoped map, preventing accidental data overwrites.

To avoid collisions:
- Use descriptive, unique step IDs like `auth_login` rather than generic names like `step1`
- Namespace your outputs when multiple steps produce similar data (e.g., `user_id` vs `admin_id`)

### Best Practices for Naming

When you share data between steps in Probe using step outputs, follow these conventions:

- **Use snake_case for step IDs**: The `id` field becomes a namespace in expressions, so `fetch_user_data` is more readable than `fetchUserData` in YAML templates.
- **Document output schemas**: Since Probe evaluates expressions dynamically, document what fields each step outputs (e.g., `token`, `expires_at`) in workflow comments.
- **Validate outputs with tests**: Use the `test` field to ensure outputs exist before downstream steps consume them:

```yaml
outputs:
  token: res.body.token
test: outputs.token != ""   # Fails step if extraction fails

```

## Summary

- **Declare outputs** by adding an `id` to your step and defining an `outputs` map with extraction expressions.
- **Access values** in subsequent steps using the `{{outputs.<step_id>.<output_name>}}` expression syntax.
- **Use flat shortcuts** like `{{outputs.token}}` when no naming conflicts exist, but prefer scoped access for clarity.
- **Share across jobs** by declaring `needs` dependencies, which ensure output availability before dependent jobs execute.
- **Avoid collisions** by using descriptive step IDs, as the `Outputs.Set` method in [`outputs.go`](https://github.com/linyows/probe/blob/main/outputs.go) prevents overwrites but may omit flat shortcuts when conflicts occur.

## Frequently Asked Questions

### What is the syntax for accessing step outputs in Probe?

Use the template expression `{{outputs.<step_id>.<output_name>}}` to access values. For example, if a step with `id: login` defines an output named `token`, reference it as `{{outputs.login.token}}`. If no naming conflicts exist, you can also use the flat syntax `{{outputs.token}}` for direct access.

### Can I share data between different jobs in Probe?

Yes. Declare a `needs` dependency in the consuming job to ensure the producing job completes first. Once the dependency is satisfied, you can reference outputs from any step in the required job using the same `{{outputs.<step_id>.<output_name>}}` syntax, as the [`workflow.go`](https://github.com/linyows/probe/blob/main/workflow.go) orchestrator ensures outputs are persisted before dependent jobs begin.

### What happens if two steps define outputs with the same name?

Probe handles this through conflict detection in [`outputs.go`](https://github.com/linyows/probe/blob/main/outputs.go). If two steps output fields with identical names, the system retains both step-scoped values (accessible via `{{outputs.step1.name}}` and `{{outputs.step2.name}}`), but omits the flat shortcut (`{{outputs.name}}`) to prevent ambiguity. If a step ID collides with a flat output name, `Outputs.Set` returns an error and preserves only the step-scoped map.

### How does Probe handle output extraction from HTTP responses?

Probe evaluates output expressions against the step's response context after execution completes. In the `outputs` map, write expressions like `res.body.access_token` for JSON fields, `res.headers.location` for headers, or `res.code` for status codes. The `Step.saveOutputs` method (lines 41-67 in [`step.go`](https://github.com/linyows/probe/blob/main/step.go)) evaluates these expressions using the step's context and stores the results in the global `Outputs` registry, making them available to subsequent steps via the expression engine.