# Technical Documentation: semaphoreui/semaphore

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/semaphoreui/semaphore)

### Overview (`README.md`)

# Semaphore UI

Modern UI for Ansible, Terraform/OpenTofu/Terragrunt, PowerShell and other DevOps tools.


[](https://github.com/semaphoreui/semaphore/actions/workflows/dev.yml)
[](https://hub.docker.com/r/semaphoreui/semaphore)






If your project has grown and deploying from the terminal is no longer feasible, then Semaphore UI is the tool you need.

## Demo

[Try Semaphore UI online](https://portal.semaphoreui.com/demo)



## What is Semaphore UI?

Semaphore UI is a modern web interface for managing popular DevOps tools.

Semaphore UI allows you to:
* Easily run Ansible playbooks, Terraform and OpenTofu code, as well as Bash and PowerShell scripts.
* Receive notifications about failed tasks.
* Control access to your deployment system.

## Key Concepts

1. **Projects** is a collection of related resources, configurations, and tasks.
2. **Task Templates** are reusable definitions of tasks that can be executed on demand or scheduled.
3. **Task** is a specific instance of a job or operation executed by Semaphore.
4. **Schedules** allow you to automate task execution at specified times or intervals.
5. **Inventory** is a collection of target hosts (servers, virtual machines, containers, etc.) on which tasks will be executed.
6. **Variable Group** refers to a configuration context that holds sensitive information such as environment variables and secrets used by tasks during execution.

## Getting Started

You can install Semaphore using the following methods:
* [Docker](https://semaphoreui.com/install/docker)
* Deploy a VM from a marketplace:
  * [AWS](https://aws.amazon.com/marketplace/pp/prodview-xavlsdkqybxtq)
  * [Cloudzy](https://cloudzy.com/marketplace/semaphore-ui)
  * [DigitalOcean](https://marketplace.digitalocean.com/apps/semaphore?refcode=b55d7c0077b8&action=deploy)
  * [Vultr](https://www.vultr.com/marketplace/apps/semaphore)
  * [Yandex Cloud](https://yandex.cloud/ru/marketplace/products/fastlix/semaphore)
  * [RepoCloud](https://repocloud.io/details/Semaphore/)
  * [Pethost](https://pethost.dev/templates/semaphore/)
* [Snap](http://snapcraft.io/semaphore)
* [Binary file](https://semaphoreui.com/install/binary)
* [Debian or RPM package](https://semaphoreui.com/install/binary)

### Docker

The most popular way to install Semaphore is via Docker.

```
docker run -p 3000:3000 --name semaphore \
	-e SEMAPHORE_DB_DIALECT=sqlite \
	-e SEMAPHORE_ADMIN=admin \
	-e SEMAPHORE_ADMIN_PASSWORD=changeme \
	-e SEMAPHORE_ADMIN_NAME=Admin \
	-e SEMAPHORE_ADMIN_EMAIL=admin@localhost \
	-d semaphoreui/semaphore:latest
```

We recommend using the [Container Configurator](https://semaphoreui.com/install/docker/) to get the ideal Docker configuration for Semaphore.



### Other Installation Methods

For more installation options, visit our [Installation page](https://semaphoreui.com/install).

## Documentation

* [User Guide](https://docs.semaphoreui.com)
* [API Reference](https://semaphoreui.com/api-docs)
* [Postman Collection](https://www.postman.com/semaphoreui)

## Awesome Semaphore

A curated list of awesome things related to Semaphore UI.

* [Ebdruplab — Ansible Collections](https://github.com/Ebdruplab/ansible-collection_ebdruplab) &mdash; Ansible modules and a role for managing Semaphore.
* [SemaphoreUI MCP Server](https://github.com/cloin/semaphore-mcp) &mdash; A Model Context Protocol (MCP) server that provides AI assistants with powerful automation capabilities for SemaphoreUI.
* [Terraform SemaphoreUI Provider](https://github.com/CruGlobal/terraform-provider-semaphoreui) &mdash; Manage Semaphore UI resources using Terraform.
* [PSSemaphore](https://github.com/robinmalik/PSSemaphore) &mdash; A PowerShell module designed to work against the Ansible Semaphore REST API.

[//]: # (* [Ansible UI Semaphore]&#40;https://github.com/morbidick/ansible-role-semaphore&#41; &mdash; Ansible role to install and configure the Ansible UI Semaphore.)

## Contribution

* [Contribution Guide](https://github.com/semaphoreui/semaphore/blob/develop/CONTRIBUTING.md)
* [Dev Container](https://codespaces.new/semaphoreui/semaphore) (default user `admin` / `changeme`)

## License

MIT © [Denis Gukov](https://github.com/fiftin)

---

### Overview (`web/README.md`)

# web

## Project setup
```
npm install
```

### Compiles and hot-reloads for development
```
npm run serve
```

### Compiles and minifies for production
```
npm run build
```

### Run your unit tests
```
npm run test:unit
```

### Lints and fixes files
```
npm run lint
```

### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

---

### Overview (`deployment/compose/README.md`)

# Compose

With the `docker-compose` snippets within this directory you are able to plug
different setups of Semaphore UI together. Below you can find some example
combinations.

Some of the snippets define environment variables which could be optionally
overwritten if needed.

## Server

First of all we need the server definition and we need to decide if we want to
build the image dynamically or if we just want to use a released image.

### Build

This simply takes the currently cloned source and builds a new image including
all local changes.

```console
docker-compose -f deployment/compose/server/base.yml -f deployment/compose/server/build.yml up
```

### Image

This simply downloads the defined image from DockerHub and starts/configures it
properly based on the integrated bootstrapping scripts.

```console
docker-compose -f deployment/compose/server/base.yml -f deployment/compose/server/image.yml up
```

### Config

If you want to provide a custom `config.json` file to add options which are not
exposed as environment variables you could add this snippet which sources the
file from the current working directory.

```console
docker-compose <server from above> -f deployment/compose/server/config.yml up
```

## Runner

If you want to try the remote runner functionality of Semaphore you could just
add this snippet to get a runner up and connected to semaphore. Similar to the
examples above for the server you got different options like building the runner
from the source or using our prebuilt images.

### Registration modes

Runners connect to the server in one of two ways:

1. **Global registration token** — set `runner_registration_token` in the server config
   (or use the token shown in **Admin → Runners**). The runner process presents this
   token on first connect and receives a long-lived auth token in return.

2. **Per-runner registration token** (`smrs_…`) — create an *unregistered* runner in the
   UI (uncheck "Registered") or via the API with `"registered": false`. Generate a
   one-time registration token for that runner and pass it to `semaphore runner register`.
   This mode is useful for Terraform-provisioned infrastructure where each host gets its
   own token. Tokens expire after one hour; regenerate from the UI if needed.

A runner is considered *registered* when it has an auth token (`runner.token` in the
database). Unregistered runners appear in the UI with a filter and cannot pick up tasks
until registration completes.

### Build

This simply takes the currently cloned source and builds a new image including
all local changes.

```console
docker-compose <server from above> -f deployment/compose/runner/base.yml -f deployment/compose/runner/build.yml up
```

### Image

This simply downloads the defined image from DockerHub and starts/configures it
properly based on the integrated bootstrapping scripts.

```console
docker-compose <server from above> -f deployment/compose/runner/base.yml -f deployment/compose/runner/image.yml up
```

### Config

If you want to provide a custom `config.json` file to add options which are not
exposed as environment variables you could add this snippet which sources the
file from the current working directory.

```console
docker-compose <runner from above> -f deployment/compose/runner/config.yml up
```

## Database

After deciding the base of it you should choose one of the supported databases.
Here we got currently the following options so far.

### SQLite

This simply configures a named volume for the SQLite storage used as a database
backend.

```console
docker-compose <server/runner from above> -f deployment/compose/store/sqlite.yml up
```

### SQLite

This simply configures a named volume for the SQLite storage used as a database
backend.

```console
docker-compose <server/runner from above> -f deployment/compose/store/sqlite.yml up
```

### MariaDB

This simply starts an additional container for a MariaDB instance used as a
database backend including the required credentials.

```console
docker-compose <server/runner from above> -f deployment/compose/store/mariadb.yml up
```

### MySQL

This simply starts an additional container for a MySQL instance used as a
database backend including the required credentials.

```console
docker-compose <server/runner from above> -f deployment/compose/store/mysql.yml up
```

### PostgreSQL

This simply starts an additional container for a PostgreSQL instance used as a
database backend including the required credentials.

```console
docker-compose <server/runner from above> -f deployment/compose/store/postgres.yml up
```

## Cleanup

After playing with the setup you are able to stop the whole setup by just
replacing `up` at the end of the command with `down`.

---

### Overview (`deployment/docker/README.md`)

# Docker

Generally we are building production-grade images for each tag, latest and even
for the development branch which will be pushed to [DockerHub][dockerhub]. If
you still need to build your own image you can easily do that, you just need
install [Docker][docker] and [Task][gotask] on your system.

If you just want to use our pre-built images please follow the instructions on
our [documentation][documentation].

If you want to use [docker-compose][dockercompose] to start Semaphore you could
also read about it on our [documentation][documentation] or take a look at our
collection of [snippets][snippets] within this repository.

## Build

We have prepared multiple tasks to build an publish container images, including
tasks to verify the image contains all required tools:

```console
task docker:build
task docker:push
```

If you want to customize the image names or if you want to use [Podman][podman]
instead of [Docker][docker] you are able to provide some set of environment
variables to the [Task][gotask] command:

* `DOCKER_ORG`: Define a custom organization for the image, defaults to `semaphoreui`
* `DOCKER_SERVER`: Define a different name for the server image, defaults to `semaphore`
* `DOCKER_RUNNER`: Define a different name for the runner image, defaults to `runner`
* `DOCKER_CMD`: Use another command to build the image, defaults to `docker`

## Test

We defined tasks to handle some linting and to verify the images contain the
tools and binaries that are required to run Semaphore. Here we are using
[Hadolint][hadolint] to ensure we are mostly following best-practices and
[Goss][goss] which is using a configuration file to define the requirements.

To install the required tools you also need to install [Golang][golang] on your
system, the installation of [Golang][golang] is not covered by us.

The installation of the dependencies can be customized by providing environment
variables for `INSTALL_PATH` (`/usr/local/bin`) and `REQUIRE_SUDO` (true).

```console
task docker:test
```

[dockerhub]: https://hub.docker.com/r/semaphoreui/semaphore
[docker]: https://docs.docker.com/engine/install/
[podman]: https://podman.io/docs/installation
[gotask]: https://taskfile.dev/installation/
[dockercompose]: https://docs.docker.com/compose/
[golang]: https://go.dev/doc/install
[hadolint]: https://github.com/hadolint/hadolint
[goss]: https://github.com/goss-org/goss
[snippets]: ../compose/README.md
[documentation]: https://docs.semaphoreui.com/administration-guide/installation

---

### Overview (`deployment/systemd/README.md`)

# Systemd

This is a sample systemd unit and environment file that you could use to run Semaphore with.
It makes no assumptions about running proxies or databases on the same machine, 
therefore if you do this you may wish to add addition requirements to the unit.
The unit will write logs to the journal which you can read with
`journalctl -u semaphore.service`

Example install, and for convenience uninstall, scripts are located in the util subdir.
The scripts expect that you manually install semaphore in /usr/bin and have the config file
`/etc/semaphore/config.json`. The config file location can be altered via the env file,
which the script installs as `/etc/semaphore/env`.

## Environment variables

The sample `env` file sets `SEMAPHORE_CONFIG`. You can add optional runtime overrides:

| Variable                 | Purpose                                                    |
|--------------------------|------------------------------------------------------------|
| `SEMAPHORE_LOG_LEVEL`    | Log verbosity (`DEBUG`, `INFO`, `WARN`, `ERROR`)           |
| `SEMAPHORE_DEBUG_FILTER` | Namespace filter for debug output (requires `DEBUG` level) |
| `SEMAPHORE_DB_DIALECT`   | Database dialect override (`sqlite`, `mysql`, `postgres`)  |

Example `/etc/semaphore/env` snippet for troubleshooting:

```bash
SEMAPHORE_CONFIG=/etc/semaphore/config.json
SEMAPHORE_LOG_LEVEL=DEBUG
SEMAPHORE_DEBUG_FILTER=runner,task_pool
```

After editing the env file, reload and restart:

```bash
sudo systemctl daemon-reload
sudo systemctl restart semaphore.service
journalctl -u semaphore.service -f
```

> **BoltDB removed in 2.19:** Configs with `"dialect": "bolt"` will not start. Use
> `sqlite`, `mysql`, or `postgres` instead.

---

### Overview (`examples/authentik_ldap/README.md`)

# Semaphore with Authentik LDAP example


1. Start stack by command:
   ```
   echo "PG_PASS=$(openssl rand 36 | base64)" >> .env
   echo "AUTHENTIK_SECRET_KEY=$(openssl rand 60 | base64)" >> .env
   echo AUTHENTIK_TOKEN= >> .env
   echo SEMAPHORE_LDAP_BIND_PASSWORD= >> .env
   docker-compose up -d
   ```
2. To start the initial setup, navigate to http://localhsot:9000/if/flow/initial-setup/. 
   There you are prompted to set a password for the `akadmin` user (the default user).
3. Set up LDAP in Athentik according to the video tutorial [Authentik - LDAP Generic Setup](https://youtu.be/RtPKMMKRT_E).
4. Set up Athentik LDAP and Semaphore containers:
   1. Copy `AUTHENTIK_TOKEN` to clipboard.
      
      <img src="https://github.com/semaphoreui/semaphore/assets/914224/9e08d4cc-4526-4ace-8b45-2c065188857b" width="600">
   2. Open `.env` file and:
      1. Paste copied value after `AUTHENTIK_TOKEN=`
      2. Enter your `ldapservice` user password after `SEMAPHORE_LDAP_BIND_PASSWORD=`
   3. Down and up the stack to apply changes:
      ```
      docker-compose down
      docker-compose up -d
      ```
5. Create new Semaphore project:
    1. Open http://localhost:3000
    2. Login as `ldapservice`
    3. Create demo project

       <img src="https://github.com/semaphoreui/semaphore/assets/914224/98b780a7-bfbc-4b45-941f-7dd6ca337685" width="600">

---

### Overview (`examples/openldap/README.md`)

# Semaphore with OpenLDAP example

1. Start stack by command:
   ```
   docker-compose up -d
   ```
2. Create new LDAP user:
   1. Open https://localhost:6443
   2. Login as `cn=admin,dc=example,dc=org` with password `admin`
   3. Create new user `john`
   
      <img src="https://github.com/semaphoreui/semaphore/assets/914224/4eee81d7-0e22-4e20-9bc2-385add519ab5" width="600">

3. Create new Semaphore project:
   1. Open http://localhost:3000
   2. Login as `john`
   3. Create demo project

      <img src="https://github.com/semaphoreui/semaphore/assets/914224/98b780a7-bfbc-4b45-941f-7dd6ca337685" width="600">

---

### Overview (`test/test-cases/README.md`)

# Semaphore UI — Manual QA Test Cases

This directory contains 30 real-world manual test cases for Semaphore UI, covering
end-to-end behavior of the web UI and API across the major feature areas:
authentication, projects, repositories, inventory, key store, variable groups,
task templates, tasks, schedules, runners, integrations / webhooks, RBAC, and
notifications.

## How to use

* Each test case is self-contained: preconditions, data, steps, expected results.
* Priority is one of `Critical`, `High`, `Medium`, `Low`.
* Run a clean Semaphore instance (Docker recommended) before executing the full
  suite. A single admin user and one project are assumed unless noted otherwise.

## Reference environment

* Semaphore UI deployed via Docker (`semaphoreui/semaphore:latest`).
* Database: SQLite (default) — re-run on Postgres/MySQL when DB compatibility is
  in scope.
* At least one remote host reachable over SSH for inventory-related cases.
* A Git repository (public or with deploy key) for repository-related cases.

## Index

| #   | ID      | Area              | Title                                                                          | Priority |
|-----|---------|-------------------|--------------------------------------------------------------------------------|----------|
| 01  | TC-001  | Auth              | [Admin login with valid credentials](TC-001-admin-login-valid.md)              | Critical |
| 02  | TC-002  | Auth              | [Login fails with wrong password and locks brute-force](TC-002-login-invalid.md)| Critical |
| 03  | TC-003  | Auth / 2FA        | [Enable TOTP and log in with one-time code](TC-003-totp-enrollment.md)         | High     |
| 04  | TC-004  | Users             | [Admin creates and deactivates a user](TC-004-user-lifecycle.md)               | High     |
| 05  | TC-005  | API Tokens        | [Create and revoke a personal API token](TC-005-api-token.md)                  | High     |
| 06  | TC-006  | Projects          | [Create a new project](TC-006-create-project.md)                               | Critical |
| 07  | TC-007  | Projects          | [Update `max_parallel_tasks` and observe queueing](TC-007-max-parallel-tasks.md)| Medium   |
| 08  | TC-008  | Projects          | [Backup and restore a project](TC-008-backup-restore.md)                       | High     |
| 09  | TC-009  | Projects          | [Delete project only after dependents are removed](TC-009-delete-project.md)   | High     |
| 10  | TC-010  | Repositories      | [Add Git repository with SSH deploy key](TC-010-repo-ssh.md)                   | Critical |
| 11  | TC-011  | Repositories      | [Add Git repository with HTTPS token](TC-011-repo-https.md)                    | High     |
| 12  | TC-012  | Key Store         | [Create SSH key and use it for inventory access](TC-012-key-ssh.md)            | Critical |
| 13  | TC-013  | Key Store         | [Create login/password key and bind become user](TC-013-key-login-password.md) | High     |
| 14  | TC-014  | Key Store         | [Bind HashiCorp Vault secret storage](TC-014-vault-storage.md)                 | Medium   |
| 15  | TC-015  | Inventory         | [Static inventory with two host groups](TC-015-inventory-static.md)            | Critical |
| 16  | TC-016  | Inventory         | [File-based Ansible inventory from repo](TC-016-inventory-file.md)             | High     |
| 17  | TC-017  | Inventory         | [Terraform workspace inventory binding](TC-017-inventory-terraform.md)         | Medium   |
| 18  | TC-018  | Variable Groups   | [JSON extra-vars + ENV vars + secret](TC-018-variable-group-mixed.md)          | High     |
| 19  | TC-019  | Variable Groups   | [`TF_VAR_` secrets reach Terraform plan](TC-019-tf-var-secret.md)              | High     |
| 20  | TC-020  | Templates         | [Create an Ansible task template and run it](TC-020-template-ansible.md)       | Critical |
| 21  | TC-021  | Templates         | [Build → Deploy template chain with artifact version](TC-021-build-deploy.md)  | High     |
| 22  | TC-022  | Templates         | [Survey variables: required, enum, default](TC-022-survey-vars.md)             | High     |
| 23  | TC-023  | Templates         | [Override branch, limit, tags at task launch](TC-023-template-overrides.md)    | Medium   |
| 24  | TC-024  | Tasks             | [Stop a running task and verify status `stopped`](TC-024-stop-task.md)         | High     |
| 25  | TC-025  | Schedules         | [Create a cron schedule and validate next run](TC-025-schedule-cron.md)        | High     |
| 26  | TC-026  | Schedules         | [One-shot `run_at` schedule with delete-after-run](TC-026-schedule-runat.md)   | Medium   |
| 27  | TC-027  | Runners           | [Register a remote runner via registration token](TC-027-runner-register.md)  | High     |
| 28  | TC-028  | Runners           | [Tag-scoped task routing to matching runner](TC-028-runner-tags.md)            | Medium   |
| 29  | TC-029  | Integrations      | [GitHub webhook + HMAC + matcher triggers template](TC-029-integration-github.md)| High |
| 30  | TC-030  | RBAC              | [Invited user with `Task Runner` role cannot edit](TC-030-rbac-task-runner.md) | Critical |

---

### Overview (`AGENTS/tools/wf-stand/README.md`)

# wf-stand — run and verify workflows on the local stand

Drives the agent's throwaway Semaphore stand (`http://localhost:3100`, admin/admin123, project 1,
Pro build from HEAD) to execute a workflow end to end and follow every node to a terminal status.
Exists because a workflow run is asynchronous: tasks, approvals and delays resolve over time and
only the run-details endpoint shows all of them together.

Call forms (from the repository root):

```
AGENTS/tools/wf-stand/stand.sh {start|stop|restart|build|status|log [n]}
AGENTS/tools/wf-stand/seed.sh
AGENTS/tools/wf-stand/seed-outputs.sh          # PROJECT_NAME="…" to seed another project
AGENTS/tools/wf-stand/run.sh <workflow_id> [--approve|--reject] [--stop-after SEC] [--expect success|failed|stopped] [--timeout SEC] [--run ID]
AGENTS/tools/wf-stand/runner.sh {start|stop|status|tag|untag}   # PID=<project> TAG=<tag>; remote runner for the stand
AGENTS/tools/wf-stand/inputs.sh <workflow_id> [run_id]           # outputs stored per producer + "INPUT name=value" lines per consumer
NODE_PATH=~/.npm/_npx/9833c18b2d85bc59/node_modules node AGENTS/tools/wf-stand/shoot-run.cjs <workflow_id> [run_id] [out.png] [--dark]
AGENTS/tools/wf-stand/seed-docs.sh                                # project "Demo Stand" + workflow "Build and deploy" for the docs screenshots
PID=<project> WF=<workflow> NODE_PATH=… node AGENTS/tools/wf-stand/shoot-docs.cjs   # the four screenshots of workflows.md § Outputs and inputs
```

Examples:

```
$ AGENTS/tools/wf-stand/stand.sh build && AGENTS/tools/wf-stand/stand.sh start
stand: up at http://localhost:3100 (pid 40366)
$ AGENTS/tools/wf-stand/seed.sh
workflows happy=5 branches=6                      # ids also in /tmp/semaphore-stand/wf-exec-ids.txt
$ AGENTS/tools/wf-stand/run.sh 5 --approve --expect success
run 4 started (workflow 5) — UI: http://localhost:3100/project/1/workflows/5/runs/4
17:30:14 approval version=0.1.0 n18/task=task#6:success n19/approval=approval:pending n20/delay=- n21/task=-
17:30:14 node 19 -> approved
17:30:24 success version=0.1.0 n18/task=task#6:success n19/approval=approval:approved n20/delay=delay:success n21/task=task#7:success
final: success (run 4)
$ AGENTS/tools/wf-stand/run.sh 6 --stop-after 8 --expect stopped
final: stopped (run 6)
```

Seeded workflows: "Exec happy" (task → approval → delay 5 s → task, ends `success` with `--approve`)
and "Exec branches" (ok → fail; on_failure → recover, on_success → never started; always → 30 s
slow task; ends `failed`). Templates are `bash` scripts from the local repo
`/tmp/semaphore-stand/wf-repo` (`ok.sh`, `fail.sh`, `slow.sh`), so no network or Ansible is needed.

`seed-outputs.sh` seeds the project "Artifacts QA" (project 1 since the 2026-10-07 rebuild) for
testing workflow outputs by hand: 16 templates (bash producers for every rule of the contract —
valid, empty, bad JSON, bad name, 101 outputs, 40 KB value, outputs-then-exit-1, symlink; bash and
Ansible consumers with one survey var of every type; Terraform / Tofu with a `sensitive` and a
hyphenated output; Ansible `set_stats`) and four workflows: 1 "producers -> consumers" (expect
`success`, see `task.artifacts` of tasks of nodes 2–4), 2 "invalid producers" (expect `failed`,
each bad producer `error` with the reason in its log, "empty" `success` without artifacts),
3 "failed producer" (expect `failed`, no artifacts stored, `on_failure` consumer ran), 4 "tofu ->
consume" (expect `success`). Scripts: `/tmp/semaphore-stand/wf-repo/produce*.sh`, `consume.sh`,
`tf/main.tf`, `playbooks/*.yml`. Ids land in `/tmp/semaphore-stand/wf-outputs-ids.txt`.

Since 2026-10-08 the same seed adds the mapping checks: templates "bash: produce renamed outputs" and
"ansible: set_stats renamed outputs" (output names `tag`, `count`, `flag`, `subnets`, `cfg`, `envname` that
match no survey variable, so only an explicit edge mapping can deliver them) and workflows 9 "Inputs: explicit
mapping" (renamed producers → bash and ansible consumers over `explicit` edges, `image_tag←tag`,
`replicas←count`, …), 10 "Inputs: by_name through approval" (produce → approval → consumers, run with
`--approve`), 11 "Inputs: fan-in by_name vs explicit" (produce by name and renamed explicitly into one consumer
with convergence all: expect `image_tag` from the explicit edge, `replicas` by name), 12 "Inputs: explicit empty
list" (the edge passes nothing). Consumers print one `INPUT name=value` line per survey variable (bash: parsed
`k=v` args plus `env:env_name` — a `target: env` variable keeps its own name, it is not upper-cased; ansible: a debug list), so `inputs.sh <wf>` shows producer outputs next to what
the consumer received. Expected since the resolver landed (2026-10-08): workflow 1 and 10 — the consumer's
`INPUT` lines repeat the producer's `image_tag`, `replicas`, `enabled`, `subnet_ids`, `config` (by name;
`env-name` with a hyphen is not delivered); workflow 9 — the renamed values (`image_tag=renamed-…`,
`replicas=7`, `env:env_name=prod`); workflow 11 — `image_tag=renamed-…` from the explicit edge and `replicas=3`
by name; workflow 12 — only `image_tag=static-from-node`, `replicas=1` (the default) and `env:env_name=static-env`;
workflow 3 — the on_failure consumer is `error` before start with "Required input(s) without a value: image_tag"
in its log. The task log of every consumer starts with the engine's `Input "<var>" <- output "<key>" of node N
(task #M)` lines, so a value's source is always one line above the value.

Since 2026-10-09 the seed also covers the Jinja rule (Ansible never evaluates an expression inside an
extra var — every value reaches it through a file, the ones with a Jinja2 delimiter tagged `!unsafe`): template "bash: produce jinja outputs"
(`produce_jinja.sh` writes `image_tag`, `config` and `replicas` holding `{{ lookup('pipe', …) }}`,
`{{ lookup('env', 'HOME') }}`, `{{ 6*7 }}`), the variable group "vars group with jinja" (`group_expr: "{{ 2*21 }}"`),
template "ansible: consume + vars group" and workflows 13 "Inputs: jinja injection" (→ ansible and bash
consumers) and 14 "Inputs: jinja vs vars group". Expected on the server and on the runner: `INPUT image_tag={{
lookup('pipe', 'echo INJECTED-$(id -un)') }}`, `config` with the expressions verbatim, `replicas=1` (the
default — `{{ 6*7 }}` is not an int) and `INPUT group_expr={{ 2*21 }}` — the variable group is data too. A
consumer log containing `INJECTED-` or `group_expr=42` means Ansible evaluated a value: a regression.
`inputs.sh` right after `run.sh` on the runner can miss the last log lines; re-run it with the run id.
Trap: the template API ignores `environment_id`, pass `environment_ids`.

Since 2026-10-10 the stand also checks the secret extra vars (TASK@CEA87DDD08): the survey secret
`token` and the variable-group secret `db_password` (type `var`, added to "vars group with jinja" by hand
through `PUT /project/1/environment/1` with `secrets:[{type:"var",name,secret,operation:"create"}]`) reach
Ansible through a second, vault-encrypted `--extra-vars @task_<id>_secret_vars_*.yml` with a one-off
`--vault-id <guid>@prompt`. `playbooks/consume.yml` prints `INPUT token=…` and `INPUT db_password=…`;
template 37 "ansible: consume + own vault" (access key "tpl vault", type login_password, vault `default`)
runs `playbooks/consume_vault.yml`, which loads `playbooks/vaulted.yml` (encrypted by `ansible-vault` with
that key's password) through `vars_files` and prints `INPUT vaulted_var=…`. Expected on the server and on
the runner: `INPUT token=s3cr3t {{ 1+1 }}`, `INPUT db_password=p@ss {{ 7*6 }}` literally; `ps -axo args=`
shows `--vault-id=<guid>@prompt` and two `--extra-vars @`, never a secret or the password; `grep -rl
s3cr3t /tmp/semaphore-stand/tmp` (or `runner-tmp`) finds nothing while the task runs; no `task_*_vars_*`
file is left after. With `SEMAPHORE_LOG_LEVEL=DEBUG SEMAPHORE_DEBUG_FILTER=extra_vars` exported before
`stand.sh start` / `runner.sh start`, the server and runner logs show seven `context=extra_vars` lines per
task (split, two files written, vault added, two files removed, password forgotten). Checked 2026-10-10:
server task 162, runner task 164, own vault task 171, regression runs 39 (wf 13) and 40 (wf 14).
Traps: a vault-encrypted file must not sit in `playbooks/group_vars/` — Ansible loads it for every playbook
of the directory and a template without that vault fails with "Decryption failed"; `vars_files` of the
repository are still templated (`{{ 3*3 }}` prints `9`) — only values Semaphore passes are `!unsafe`;
`runner.sh start | tail` hangs because the runner inherits the pipe — redirect to a file instead; a PUT of a
template without the `vaults` field deletes its vaults, which is why `runner.sh tag/untag` re-reads each
template by id before the PUT.

Docs screenshots (2026-10-11): `seed-docs.sh` seeds a separate project "Demo Stand" (the name the published
editor screenshot already shows) with "Provision network" (bash, outputs `vpc_id`, `subnet_ids`), "Build image"
(bash, outputs `image_tag`, `replicas` — the values quoted in the user guide), "Deploy to cluster" (ansible
`playbooks/deploy.yml`, survey `image_tag`, `replicas`, `subnets`) and the workflow "Build and deploy":
Provision → Build → Deploy by name plus Provision → Deploy explicit (`subnets ← subnet_ids`). Run it once
(`PID=<p> run.sh <wf> --expect success`), then `shoot-docs.cjs` writes `workflow-inputs-explicit.png`,
`workflow-inputs-by-name.png`, `workflow-run-outputs.png` and `task-outputs.png` (dialog clipped) to
`/tmp/semaphore-stand/shots-docs`; convert with `cwebp -q 88` into `docs/static/assets/`. Traps: a workflow must
have exactly one root node (the explicit edge alone would make Provision and Build two roots); key hints need a
run of the current revision, so re-run after any re-seed; the editor fits a three-node graph at ~45 % on a 1440 px
viewport — the script collapses the palette (`.WorkflowEditor__sideToggle`) and clicks the fit control after the
side panel opens instead of zooming in, which pushes nodes under the panel.

Seeding traps learned there: a Terraform-family template must be POSTed without `inventory_id`
(the API creates a `terraform-workspace` inventory; a shared one gets claimed and hidden) and with
`task_params.auto_approve`, or a plan with changes parks the task in `waiting_confirmation`; a
workflow must have exactly one root node; `survey_vars[].default_value` is a plain string; a JSON
`\"` inside `$( )` inside a double-quoted bash string is mangled — build such fragments in a
variable first.

Traps: `run.sh` exits 1 when `--expect` does not match or `--timeout` (default 180 s) passes;
`--approve` resolves every pending approval it sees, so use `--run ID` to only follow an existing
run. `stand.sh build` rebuilds the Vue app into `api/public` (a tracked-ignored embed input) and
the binary at `/tmp/semaphore-stand-bin`; it needs the `pro_impl` workspace for Workflows to be
enabled. The stand DB is `/tmp/semaphore-stand/database-wf.sqlite` and persists between sessions.

`runner.sh` registers a global runner "wf-runner" (tag `wf`, local executor, `/tmp/semaphore-stand/runner-config.json`,
log `runner.log`) and starts it from the same binary; `tag` sets `runner_tag` on every template of project
`PID` (default 1) so their tasks go to it, `untag` sends them back to the server. The remote-runner check
of workflow outputs (2026-10-08): `runner.sh start && runner.sh tag && run.sh 1 --expect success && run.sh 2
--expect failed`, then `sqlite3 /tmp/semaphore-stand/database-wf.sqlite "select id,status,runner_id,length(artifacts)
from task order by id desc limit 13"` shows `runner_id` of the runner and `artifacts` on the producers only.
Traps: the token is returned once at creation, so a runner whose config file is gone is deleted and recreated;
stop the runner and `untag` before a session that expects tasks to run on the server, or they wait forever;
`status` keeps saying `online` for up to 120 s after `stop` (the server's heartbeat timeout), trust the
`process:` line.

---

### Overview (`AGENTS/agents/setup/db-query/README.md`)

# db-query

Ad-hoc SQL runners for the project's local databases — **one script per database**. Each runs the
query as that database's app user, read-only by default, and logs every query it runs.

| Script | Engine | Mode | Database | `.env` file | `.env` keys | Access |
| --- | --- | --- | --- | --- | --- | --- |
| `query_<db>.sh` | PostgreSQL / MariaDB | docker / host | `<database>` | `<path>` | `DB_*` | read-only |

## Setup

None. Each script reads its connection from the project's own `.env`; the path to it is
`ENV_FILE` in the script's settings block. Credentials are never copied into this folder.

## Usage

~~~shell
AGENTS/tools/db-query/query_<db>.sh "SELECT id, email FROM users LIMIT 5"
AGENTS/tools/db-query/query_<db>.sh -f report.sql
echo "SELECT 1" | AGENTS/tools/db-query/query_<db>.sh
AGENTS/tools/db-query/query_<db>.sh "SELECT ..." -t -A     # trailing flags go to psql / mariadb
~~~

## Log

Every query is appended to `AGENTS/tools/db-query/log/queries.log` before it runs — timestamp,
script, target, database, user, access mode, the SQL — and its exit code after. Result rows are not
logged. The folder is gitignored.

## Traps

- ⚠️ Local only: a remote docker daemon or a non-loopback host is refused. `ALLOW_REMOTE=1` in a
  script is set only on the user's explicit request — record who asked and when here.
- ⚠️ Read-only by default: the session refuses any write (`READ_ONLY=1`). It guards against
  accidents, not against intent — SQL can switch it off. `READ_ONLY=0` in a script is set only on
  the user's explicit request — record who asked and when here.

---

### Overview (`AGENTS/work/workflow-editor/mocks/README.md`)

# mocks — approved mock, screenshots and the scripts that shoot them

Design artefacts of the editor redesign (`TASK@88179c9bc1`); decisions are in
`../editor-architecture.md`, not here. Every script targets the local stand
`http://localhost:3100` (admin/admin123, project 1) and Playwright loaded through `NODE_PATH`
with `channel: 'chrome'` — stand and Playwright recipes are in `AGENTS/memory/ui-stands.md`.

## Files

| File | What it is |
| --- | --- |
| `2026-09-26-workflow-node-mock.html` | Approved static mock: editor canvas, run view, card states, empty state; `#dark` in the URL flips the theme. Open from the repo checkout — MDI icons load from `web/node_modules`. Its "Tab opens quick-add" hint is stale (see ⚠️ in `editor-architecture.md`). |
| `2026-09-26-workflow-node-mock-{light,dark}.png` | Renders of the mock as approved. |
| `workflow-editor/editor-{light,dark}.png`, `editor-selected-*.png`, `editor-quickadd-light.png`, `editor-legacy-*.png`, `editor-empty-light.png`, `run-{light,dark}.png` | Screenshots of the shipped UI at 1440 × 900 @2x, shot by `shoot-wf.cjs`. "Legacy" is a workflow saved without positions (auto layout). |
| `workflow-editor/editor-interactions.png` | End state of the `interact-wf.cjs` checklist. |
| `workflow-editor/run-approval-pending.png`, `run-delay-countdown.png` | Run view with a pending approval and the delay countdown, from `approve-wf.cjs`. |
| `workflow-editor/editor-edge-explicit-{light,dark}.png`, `editor-edge-byname-{light,dark}.png`, `run-outputs-{light,dark}.png`, `task-outputs-{light,dark}.png` | Workflow inputs (2026-10-09, `TASK@1845d9d4d7` stage 7): the edge panel with "Map inputs explicitly" and the mapping table (keys suggested from the last run), the by_name panel with the destination's survey variables (a check on those the source produced), the run view with "N outputs" on producer cards and the swap icon on explicit pills, the task dialog's Outputs panel. Shot by `shoot-inputs.cjs` on the "Artifacts QA" project. |
| `workflow-editor/editor-revision-active-run.png`, `run-after-edit.png` | Revisions (2026-09-27): the editor with the "rev. 3" and "1 active run" chips, and run #8 still rendered on its revision 1 after two later saves. Shot with `AGENTS/tools/wf-stand/shoot-run.cjs`. |

## Scripts

| Script | Does | Needs |
| --- | --- | --- |
| `seed-wf.sh` | Seeds five templates (one with an XSS name), the "Release" workflow with positions, the position-less "Nightly checks" (with a `<script>` note), starts one Release run; writes ids to `/tmp/semaphore-stand/wf-ids.txt`. Idempotent for templates. | Running stand, repository 1 and inventory 1 in project 1. |
| `shoot-wf.cjs` | Editor/run/legacy/empty screenshots in light and dark; output `OUT` (default `/tmp/semaphore-stand/shots-wf`), `RUN` selects the run id. | `wf-ids.txt`. |
| `interact-wf.cjs` | 16 PASS/FAIL checks on the editor: quick-add from the port, palette click, drag connect, cycle guard, pill condition change, snap, Delete, undo/redo round trip, problem badge + disabled Save, leave guard. Exits 1 on any FAIL or page error. | `wf-ids.txt`. Mutates the Release workflow in the browser but does not save. |
| `approve-wf.cjs` | Approve inside the card, delay countdown ticks, viewport survives polling, task log opens on click. | `/tmp/semaphore-stand/wf3.txt` with `<workflow id> <run id>` of a run waiting at an approval — seeded by hand, not by `seed-wf.sh`. |
| `keys-wf.cjs` | Keyboard pan/zoom/fit/reset in the editor and the run view; asserts no effect while typing in the properties panel. | `wf-ids.txt`, run 2 of Release. |
| `shoot-inputs.cjs` | Shoots the four inputs screenshots above and runs 7 PASS/FAIL checks (checkbox state, mapping rows, hint line, by_name chips with a matched output, output count on a card, Outputs panel in the task dialog). `--dark` for the dark theme; `WF` (default 9, explicit workflow), `WF_BYNAME` (default 1), `OUT` (default `/tmp/semaphore-stand/shots-inputs`). Edges are selected by a dispatched `mousedown` on the path — a real click lands on the condition pill. | `AGENTS/tools/wf-stand/seed-outputs.sh` seeded, a finished run of the current revision of both workflows (`run.sh 9`, `run.sh 1`) for the key hints. |
| `interact-inputs.cjs` | 10 PASS/FAIL checks of the inputs round trip on workflow `WF` (default 12): select the explicit edge, not dirty before editing, type a key, dirty, explicit icon on the pill, Save → API shows the pair, uncheck → by_name chips, Save → fields gone, original graph restored by PUT. Exits 1 on any FAIL or page error. | Running stand with the seeded project; mutates workflow 12 and restores it. |
| `shoot-nodes.cjs` | Crops one card per node kind (task hovered with the "+", approval, delay, note) with edge pills hidden and zoom 1; source of `docs/static/assets/workflow-node-*.webp`. | `WF` env = id of the "Docs node kinds" workflow on the stand (default 4). |
| `make-hotkeys-svg.py <out.svg>` | Generates the keyboard cheat-sheet `docs/static/assets/workflow-hotkeys.svg`; edit `COLUMNS` and re-run when a shortcut changes. | Python 3 only. |

---

### AGENTS Docs INDEX (`AGENTS/docs/INDEX.md`)

# docs — portable standard by zone

Approaches and conventions that would serve another project, not this project's own facts. One
folder per zone, each with its own `INDEX.md`. This file routes to a zone; the zone's index routes
to the doc.

## Zone index (example)

The rows below are example data, illustrating the shape a real routing table takes; replace them
once an actual zone exists.

| When you need it | Read |
| --- | --- |
| Example: Writing backend code: models, routes, permissions, entity codes | `backend/INDEX.md` |
| Example: Running or writing backend tests — mandatory before the first command | `backend/INDEX.md` |
| Example: Editing the frontend: app layout, import boundaries, i18n | `frontend/INDEX.md` |
| Example: Setting up a local stack: reverse proxy, HMR behind a proxy, tunnels — the approach, not this project's ports | `infra/INDEX.md` |
| Example: Editing an env file or another human-read file: grouping, order, language, line length | `standards/INDEX.md` |

---

--- METRICS ---
- Files Extracted: 12
- Estimated Token Budget: ~10266 tokens
- Canonical Reference: https://codewiki.google/github.com/semaphoreui/semaphore
