---
id: workflows
title: Workflows and steps
sidebar_label: Workflows
---

A **step** is a reusable script with declared inputs, outputs and secret
references. A **workflow** composes steps in order and targets a set of servers.
Running one dispatches the steps to each target's agent and streams the output
back live.

## Steps

A step has:

| Field                 | Meaning                                         |
| --------------------- | ----------------------------------------------- |
| `name`, `description` | Library identity                                |
| `interpreter`         | `bash` or `powershell`                          |
| `script`              | The body                                        |
| `declared_inputs`     | Named parameters with defaults and descriptions |
| `declared_outputs`    | Names this step promises to export              |
| `secret_refs`         | Vault entries injected as environment variables |

### Passing values between steps

Each step runs with `WORKFLOW_ENV` set to a file path. Anything written there as
`KEY=value` becomes an environment variable for the **later steps of the same
run on the same server**.

```bash
HOSTNAME=$(hostname)
echo "$HOSTNAME"
echo "HOSTNAME=$HOSTNAME" >> $WORKFLOW_ENV
```

That is the whole mechanism. `declared_outputs` documents what a step exports so
the designer can show it; the file is what actually carries the value.

### Secrets

List a vault group in `secret_refs` and its key/value pairs are injected as
environment variables when the step runs. They are not written to the run log
unless your own script echoes them. See [Secrets](./secrets.md).

### The workspace

Every run gets a per-run working directory on each target. Steps share it, so
one step can leave a file for the next. The agent removes it at the end of the
run (`CleanupWorkspaceCmd`).

Do not use it for anything that must outlive the run.

## Default steps

A small library is seeded into every organisation at boot from the image, so a
new install is not staring at an empty page.

:::warning Default steps are read-only
Editing or deleting one is refused with `409`. Seeding rewrites them on every
boot, so an edit would silently revert and a delete would come back at the next
restart refusing is the honest answer.

To customise one, use the per-step **script override** in the workflow designer,
which belongs to that workflow and is not touched by seeding. To add to the
shared library permanently, a file has to be committed to the repository and the
server image rebuilt.
:::

The UI mirrors this the step modal opens read-only and Delete is hidden but
the API is the boundary; the UI is the courtesy.

## Building a workflow

1. **Workflows → New**.
2. Add steps in order from the library.
3. Set inputs per step.
4. Set failure behaviour per step.
5. Choose targets: named servers, a tag selector, or both. See
   [Targeting](#targeting).

### Failure behaviour

| `on_failure` | Effect                                                          |
| ------------ | --------------------------------------------------------------- |
| `stop`       | Abort this server's run. Other servers continue                 |
| `continue`   | Record the failure, run the next step anyway                    |
| `retry`      | Re-run the step up to `max_retries`, then treat it as a failure |

### Per-step overrides

A workflow can override a step's script or its secret references without
touching the library entry. This is how you adapt a default step, and it is
scoped to that workflow.

## Targeting

A workflow names servers two ways, and it can use both at once:

- **Target servers** an explicit list you pick from the fleet.
- **Target tags** a `key:value` selector matched against
  [server tags](./servers.md#tags). More than one key ANDs: a server must carry
  every pair to match.

A run goes to the **union** of the two, with duplicates removed. A server that is
both named explicitly and matched by the selector runs once, not twice. This is
what lets a workflow say "every production web server, plus this one box I am
watching" without maintaining a list.

The designer shows the resolved count as you edit, so you can see how many
machines a change to the selector just added or removed before you save.

:::warning An empty selector matches nothing
Clearing the tag selector does not mean "all servers". A workflow with no named
servers and no tags matches nothing and is refused at run time rather than
reported as a success over zero machines.

The alternative reading, where an empty field means the whole fleet, turns a
cleared box into a fleet-wide run. That is not a mistake anyone should be able to
make by deleting text.
:::

Tags are read **at run time**, not when you save. Tag a new machine `env:prod`
and the next run of an `env:prod` workflow includes it, with nothing to update on
the workflow itself. The same is true in reverse: removing a tag removes the
machine from every workflow that selected on it.

### Offline servers are still targeted

A server matched by tag is dispatched to even if its agent is offline, and that
step fails visibly on that machine. Vantage does not quietly shrink your target
list to the machines that happened to be reachable a patch run that skipped
three servers and reported success is worse than one that failed on three and
said so.

Re-run the workflow once they are back, or fix the agent first.

## Running

**Run** snapshots the resolved steps into the run record and dispatches each step
to the target's agent over the command stream no waiting for the next poll.

:::info Runs freeze their steps
The snapshot is why editing a step tomorrow never rewrites what happened today.
A run shows the script that actually executed, not the current library version.
:::

Targets run **in parallel**; steps within one server run **in order**.

## Schedules

A workflow can carry a schedule, and Vantage will start it the same way a person
would — the same dispatch, the same snapshot, the same run page. A scheduled run
is an ordinary run with `schedule` recorded as who triggered it.

Open a workflow, choose **Edit**, and tick **Run on a schedule**. The expression
is standard five-field cron:

```
minute  hour  day-of-month  month  day-of-week
```

The presets write cron underneath, so you can start from one and adjust:

| Preset              | Cron        |
| ------------------- | ----------- |
| Hourly              | `0 * * * *` |
| Nightly, 02:00      | `0 2 * * *` |
| Weekly, Sun 02:00   | `0 2 * * 0` |
| Monthly, 1st 02:00  | `0 2 1 * *` |

There is no seconds field and no `@daily`-style shorthand. The next three
occurrences are shown as you type, and they are computed by the server rather
than the browser, so what you see is exactly what will fire.

### Timezones

A schedule stores an IANA timezone by name — `Europe/London`, not an offset.
That is what makes a 02:00 job stay at 02:00 across a daylight-saving change
instead of drifting an hour for half the year. An unknown zone is refused when
you save it, not at 2am.

### Overlaps are skipped, not queued

If a run of the same workflow is still going when the next occurrence comes
round, the occurrence is **skipped** and the reason recorded. It is not queued
behind the running one. A patch workflow that takes longer than its interval
should fall behind visibly rather than pile up.

### Missed occurrences

If the control plane was not running when an occurrence was due, it still fires
when the control plane comes back — but only within **one hour** of the due
time. Anything older is recorded as missed and dropped. A job missed by ten
minutes during an upgrade should still run; one missed by two days should not
suddenly fire at lunchtime.

Either kind of skip is shown on the workflow's schedule panel, with the time it
was due and why it did not run.

## Watching a run

Step stdout and stderr stream back as chunks, are appended to a log file on the
server, and the UI follows them live. Each step records status, attempts, exit
code and its exported environment.

**Cancel** stops a run in progress. Steps already running on an agent finish;
nothing further is dispatched.

## Log retention

Run logs are swept on a schedule set by `workflow_log_retention_days` in
Settings:

| Value    | Meaning        |
| -------- | -------------- |
| unset    | 30 days        |
| a number | that many days |
| `0`      | keep forever   |

## Import and export

Steps export to a JSON file (`vantage.step/v1`) and import back, which is how
you move a step between instances or keep one in version control. There is also
a parse endpoint that turns a pasted script into a draft step by reading its
declared inputs and outputs.

## Practical notes

- A step is a script. It runs as root, on the target, with no sandbox. Review
  what you import.
- Keep steps small and single-purpose; compose them in the workflow. That is
  what makes the library reusable rather than a folder of near-duplicates.
- PowerShell steps only make sense on Windows targets and bash steps on Linux
  ones. Nothing stops you targeting the wrong one; the step simply fails.
