> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bentralabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Workflow syntax for Bentra

A workflow is a YAML file listing the steps a runner executes. Each step is a bash script. Steps run in order, in one workspace that holds the workflow's files.

## About YAML syntax for workflows

* Workflows live at `.bento/workflow/workflow.yml` in the repository (VCS workflows) or in the uploaded file set (local workflows).
* The runner parses the file with `yaml.safe_load`. Bentra itself only checks that the file exists and is a YAML mapping before a version can be cut. Everything else is validated by the runner when a run starts.
* Local workflow file sets are limited to 256 KB per file, 2 MB in total, 100 files, and a path depth of 10.
* Bentra workflows are **not** GitHub Actions. There is no `on`, `jobs`, `runs-on`, `needs`, `uses`, or `${{ }}` expression syntax. Triggers, approvals, schedules, and runner selection are configured in the Bentra UI. See [Configured outside the YAML](#configured-outside-the-yaml).

### Example: a complete workflow

```yaml theme={"theme":{"light":"github-light","dark":"night-owl"}}
name: Provision storage bucket
description: Creates a bucket from the request form.
version: "2.0"

tools:
  terraform_version: "1.7.5"

steps:
  - name: Show inputs
    run: |
      echo "Provisioning $BUCKET_NAME in $REGION"
      cat bento_inputs.json

  - name: Terraform init
    run: terraform -chdir=infra init -input=false

  - name: Terraform plan
    run: |
      terraform -chdir=infra plan -input=false \
        -var "bucket_name=$BUCKET_NAME" -var "region=$REGION" -out=tfplan

  - name: Lint (non-blocking)
    continue_on_error: true
    run: tflint --chdir=infra

  - name: Terraform apply
    run: terraform -chdir=infra apply -input=false tfplan

  - name: Smoke check
    run: |
      if aws s3api head-bucket --bucket "$BUCKET_NAME"; then
        echo "::bentra-check status=passed,name=Bucket exists::$BUCKET_NAME is reachable"
      else
        echo "::bentra-check status=failed,name=Bucket exists::$BUCKET_NAME not found"
      fi
```

***

## `name`

**Optional.** The workflow's display name, shown in runner logs. Defaults to `Unnamed`.

## `description`

**Optional.** Free-text documentation. The runner ignores it.

## `version`

**Optional.** The syntax version, by convention `"2.0"`, which is what the starter template uses. The runner ignores it.

***

## `tools`

**Optional.** Pins tool versions for the whole run. The versions are installed during the "Set up job" phase, before any step runs. If a requested version can't be installed, the run fails.

### `tools.terraform_version`

A string. The Terraform version to install and select with `tfenv`, for example `"1.7.5"`. If omitted, the runner image's default is used.

### `tools.opentofu_version`

A string. The OpenTofu version to install and select with `tofuenv`. If omitted, the runner image's default is used.

```yaml theme={"theme":{"light":"github-light","dark":"night-owl"}}
tools:
  terraform_version: "1.7.5"
  opentofu_version: "1.6.2"
```

***

## `steps`

**Required.** The list of steps to execute. If `steps` is missing or is not a list, the run fails. Items that are not mappings are skipped with a warning. A run in which no step executes fails.

Steps run one at a time, in order. By default the first failing step stops the run.

## `steps[*].name`

**Optional.** The step's name, shown in the run view and logs. At most 255 characters. Defaults to `Unnamed step`.

## `steps[*].run`

**Required in practice.** The bash script to execute. It runs as:

```text theme={"theme":{"light":"github-light","dark":"night-owl"}}
bash --noprofile --norc -eo pipefail -c "<run>"
```

* `-e`: the step fails on the first failing command.
* `-o pipefail`: a failure anywhere in a pipeline fails the step.
* The working directory is the workspace root, which contains the workflow's files plus [`bento.env` and `bento_inputs.json`](#files-in-the-workspace).
* Each step gets a fresh shell. Variables exported in one step are **not** visible to later steps. To pass data between steps, write it to a file in the workspace.
* A step with an empty or missing `run` is marked **skipped**.

```yaml theme={"theme":{"light":"github-light","dark":"night-owl"}}
steps:
  - name: Multi-line script
    run: |
      set -x
      ./scripts/deploy.sh "$ENVIRONMENT"
```

## `steps[*].continue_on_error`

**Optional.** Boolean, default `false`. When `true`, later steps still run if this step fails. The run as a whole is still reported as failed.

> [!NOTE]
>
> A step **timeout** always stops the run, even with `continue_on_error: true`.

```yaml theme={"theme":{"light":"github-light","dark":"night-owl"}}
- name: Optional lint
  continue_on_error: true
  run: tflint
```

## `steps[*].tools`

**Optional.** Switches the tool version from this step onward. It takes the same keys as the top-level [`tools`](#tools): `terraform_version` and `opentofu_version`.

> [!WARNING]
>
> The version is **not** reset after the step. Later steps keep using it until another step sets a different version.

```yaml theme={"theme":{"light":"github-light","dark":"night-owl"}}
steps:
  - name: Plan with 1.5
    tools:
      terraform_version: "1.5.7"
    run: terraform plan
  - name: Plan with 1.7
    tools:
      terraform_version: "1.7.5"
    run: terraform plan
```

## `steps[*].retry`

> [!WARNING]
>
> **Not supported yet**
>
> `retry` (both top-level and per step) is accepted in the YAML and appears in some older examples, but the runner doesn't apply it. A failing step is not retried.

***

## `outputs`

**Optional.** Declares data the run publishes.

> [!NOTE]
>
> Datasets need runner **0.7.0+**. Only **job**-type workflows can declare outputs; request workflows are rejected.

### `outputs.datasets.<name>`

Declares a dataset: a small table that the run publishes and other jobs and views can read.

* The name must match `^[a-z][a-z0-9_-]*$` and be at most 64 characters.
* A job can declare at most 10 datasets.
* The declaration is checked when the run is planned, and an invalid declaration fails the run before any step executes.
* A dataset the job previously declared but no longer declares is marked retired.

### `outputs.datasets.<name>.columns`

**Required.** A non-empty list of unique column names. Each name is at most 64 characters, and there can be at most 20 columns.

### `outputs.datasets.<name>.max_rows`

**Optional.** An integer from 1 to 1000. Default `1000`.

### `outputs.datasets.<name>.description`

**Optional.** A string.

### `outputs.datasets.<name>.allow_empty`

**Optional.** Boolean, default `false`. By default, publishing zero rows is rejected when the current version of the dataset has rows. This guards against wiping out data by accident. Set it to `true` if an empty result is legitimate.

### Writing a dataset

Steps write each dataset to `$BENTRA_DATASETS/<name>.json` as a JSON array of objects. After the steps succeed, an automatic **Publish datasets** step uploads the files.

Each object is one row, and these rules apply:

* Keys must be declared columns. A missing column becomes `null`.
* Values must be scalars: string, number, boolean, or null.
* All datasets in a run must total at most 1 MB.
* Every declared dataset must be written.

Only successful runs publish. A new version is created only when the data has changed.

> [!WARNING]
>
> A step with `continue_on_error: true` that fails still makes the run fail, so a job whose datasets matter shouldn't rely on it: the datasets won't be published. Handle expected errors inside the script instead.

```yaml theme={"theme":{"light":"github-light","dark":"night-owl"}}
outputs:
  datasets:
    buckets:
      description: Buckets managed by this job
      columns: [name, region, versioning]
      max_rows: 500

steps:
  - name: Export bucket inventory
    run: |
      aws s3api list-buckets --query 'Buckets[].{name:Name}' --output json \
        | jq '[.[] | . + {region: "us-east-1", versioning: false}]' \
        > "$BENTRA_DATASETS/buckets.json"
```

***

## `inputs`

**Optional.** Declares other jobs' datasets this workflow reads. Both job and request workflows can declare inputs.

### `inputs.datasets`

A non-empty list of `{job, dataset}` entries. `job` is the slug of the job that publishes the dataset.

```yaml theme={"theme":{"light":"github-light","dark":"night-owl"}}
inputs:
  datasets:
    - job: org-repository-inventory
      dataset: repos
```

* The workflow needs a **read grant** on each job (the job's Data sharing settings), and the job must have published the dataset. Otherwise the run fails before any step executes.
* Before the first step, the runner downloads the current version of each dataset to `$BENTRA_INPUTS/<job>/<dataset>.json`, as a JSON array of row objects (the same shape a job writes to `$BENTRA_DATASETS`). The version it loaded is printed in the setup log.
* The version each run read is recorded on the attempt.

```yaml theme={"theme":{"light":"github-light","dark":"night-owl"}}
- name: Count active repositories
  run: jq '[.[] | select(.archived | not)] | length' "$BENTRA_INPUTS/org-repository-inventory/repos.json"
```

***

## Inputs and environment variables

There is no templating language. Values reach your steps as **environment variables** and as **files** in the workspace.

### Form inputs

Each field in the submitted [form](https://docs.bentralabs.com/reference/form-syntax.md) becomes an environment variable named after the field `id` in upper case:

| Form value | Environment variable |
| - | - |
| string (`input`, `textarea`, `radio`, `dropdown`) | the string: `bucket_name` becomes `$BUCKET_NAME` |
| list (multi-select `dropdown`, `checkboxes`) | comma-joined: `a,b,c` |
| boolean | `true` or `false` |
| object (`tags`) | not usable. Read `bento_inputs.json` instead. |

### Variables and secrets

Variables and secrets from the workflow's variable collections are injected as upper-cased environment variables. They **override** a form input with the same name.

### System variables

| Variable | Value |
| - | - |
| `BENTO_EXECUTION_ID` | The run's execution id |
| `BENTO_WORKFLOW_ID` | The workflow id |
| `BENTRA_DATASETS` | The directory for [dataset](#writing-a-dataset) files. It is set last, so a secret can't override it. |
| `BENTRA_INPUTS` | The directory holding the [input datasets](#inputsdatasets) |

The runner's own credentials (`BENTRA_RUNNER_TOKEN`, and anything prefixed `OPENBAO_`, `BAO_`, or `VAULT_`) are removed from the step environment.

### Files in the workspace

Both files are written before each step:

* **`bento.env`**: a sourceable bash file with the form inputs and the `BENTO_*` variables. Other secrets are not included. A form input that a secret overrides is written with the secret's value.
* **`bento_inputs.json`**: the submitted form data as typed JSON. Lists stay lists and tags stay objects.

```yaml theme={"theme":{"light":"github-light","dark":"night-owl"}}
- name: Read structured inputs
  run: |
    source bento.env
    TEAM=$(jq -r '.labels.team' bento_inputs.json)
    echo "Deploying $SERVICE_NAME for $TEAM"
```

***

## Reporting checks

A step can report a check (a pass/fail annotation shown on the run) by printing a line to **stdout**:

```text theme={"theme":{"light":"github-light","dark":"night-owl"}}
::bentra-check status=<status>,name=<name>[,key=value…]::<message>
```

* `status` is required and must be one of `passed`, `failed`, `warning`, or `info`.
* `name` defaults to `Check`.
* Any other `key=value` pairs are attached as details.
* To use a literal comma inside a value, write `%2C`. To use a literal `::`, write `%3A%3A`.
* A `failed` check fails the step, even if the script exits 0.

```bash theme={"theme":{"light":"github-light","dark":"night-owl"}}
echo "::bentra-check status=warning,name=Cost,monthly=412::Above the 400 USD budget"
```

***

## Timeouts and implicit setup

* **Step timeout:** 300 s by default. This is set by the runner's `STEP_TIMEOUT` environment variable, not in the YAML.
* **Run timeout:** 1800 s by default, set by the runner's `EXECUTION_TIMEOUT`.
* **Dependency install:** before your steps, the runner adds an "Install Python dependencies" step (`pip install -r requirements.txt`) if `requirements.txt` exists in the workspace root, and an "Install Node.js dependencies" step (`npm install --production`) if `package.json` exists. If either fails, the run stops.

***

## Configured outside the YAML

These settings are made in the Bentra UI or API rather than in `workflow.yml`:

| Setting | Where |
| - | - |
| Workflow type (`request` with a form, or `job`) | Workflow settings |
| Source (VCS repository or local files), branch/tag tracking | Workflow settings. See [Connectors](https://docs.bentralabs.com/reference/connectors.md). |
| Form | `.bento/form/form.yml`, found by convention. Request workflows only. |
| Approvals, auto-deploy on approval, notifications, form visibility | Request settings |
| Manual and API triggers | Job settings |
| Schedules (cron and timezone) | Job settings |
| Runner pool | Workflow settings. See [Install a runner](https://docs.bentralabs.com/runners/install.md). |
| Variables and secrets | Variable collections attached to the workflow |

Published workflow versions are immutable snapshots of these files, taken when the version is cut.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.