semaphore Documentation

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

14,273 stars Go Go 12 files ยท ~10,350 tokens #ansible#awx#ci#cicd#devops#docker#docker-ui#go
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy semaphore on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name semaphore -p 389:389 --restart always semaphoreui/semaphore:latest
Configured Services & Ports
โš™๏ธ ldap
image: osixia/openldap:1.4.0
๐Ÿ”Œ Ports: 389:389, 636:636
โš™๏ธ ldap_admin
image: osixia/phpldapadmin:0.9.0
๐Ÿ”Œ Ports: 6443:443
โš™๏ธ semaphore
image: semaphoreui/semaphore:latest
๐Ÿ”Œ Ports: 3000:3000
docker-compose.yml
yaml (docker-compose.yml)
version: '3.8'

services:
  ldap:
    image: osixia/openldap:1.4.0
#    container_name: openldap
    environment:
      LDAP_ORGANISATION: "Example Inc."
      LDAP_DOMAIN: "example.org"
      LDAP_ADMIN_PASSWORD: "admin"
    ports:
      - "389:389"
      - "636:636"
    volumes:
      - ldap_data:/var/lib/ldap
      - ldap_config:/etc/ldap/slapd.d

  ldap_admin:
    image: osixia/phpldapadmin:0.9.0
#    container_name: phpldapadmin
    environment:
      PHPLDAPADMIN_LDAP_HOSTS: ldap
    ports:
      - "6443:443"
    depends_on:
      - ldap

  semaphore:
    image: semaphoreui/semaphore:latest
#    container_name: semaphore
    environment:
      SEMAPHORE_DB_DIALECT: "sqlite"
      SEMAPHORE_ADMIN_PASSWORD: "changeme"
      SEMAPHORE_ADMIN_NAME: "admin"
      SEMAPHORE_ADMIN_EMAIL: "[email protected]"
      SEMAPHORE_LDAP_ACTIVATED: "yes"
      SEMAPHORE_LDAP_SERVER: "ldap:389"
      SEMAPHORE_LDAP_SEARCH_DN: "dc=example,dc=org"
      SEMAPHORE_LDAP_BIND_DN: "cn=admin,dc=example,dc=org"
      SEMAPHORE_LDAP_BIND_PASSWORD: "admin"
      SEMAPHORE_LDAP_SEARCH_FILTER: "(&(objectClass=inetOrgPerson)(uid=%s))"
      SEMAPHORE_NON_ADMIN_CAN_CREATE_PROJECT: "yes"
    ports:
      - "3000:3000"
    volumes:
      - semaphore_data:/var/lib/semaphore
    depends_on:
      - ldap

volumes:
  ldap_data:
  ldap_config:
  semaphore_data:

Technical Documentation: semaphoreui/semaphore

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

Semaphore UI

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


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

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

The most popular way to install Semaphore is via Docker.

text
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 to get the ideal Docker configuration for Semaphore.

Other Installation Methods

For more installation options, visit our Installation page.

Documentation

Awesome Semaphore

A curated list of awesome things related to Semaphore UI.

Contribution

License

MIT ยฉ Denis Gukov


Overview (`web/README.md`)

web

Project setup

text
npm install

Compiles and hot-reloads for development

text
npm run serve

Compiles and minifies for production

text
npm run build

Run your unit tests

text
npm run test:unit

Lints and fixes files

text
npm run lint

Customize configuration

See Configuration Reference.


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. If
you still need to build your own image you can easily do that, you just need
install Docker and Task on your system.

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

If you want to use docker-compose to start Semaphore you could
also read about it on our documentation or take a look at our
collection of 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
instead of Docker you are able to provide some set of environment
variables to the Task 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 to ensure we are mostly following best-practices and
Goss which is using a configuration file to define the requirements.

To install the required tools you also need to install Golang on your
system, the installation of 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

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:
    text
    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.
  4. Set up Athentik LDAP and Semaphore containers:
    1. Copy AUTHENTIK_TOKEN to clipboard.

    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:

      text
      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

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

Semaphore with OpenLDAP example

  1. Start stack by command:

    text
    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
  3. Create new Semaphore project:

    1. Open http://localhost:3000
    2. Login as john
    3. Create demo project

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 Critical
02 TC-002 Auth Login fails with wrong password and locks brute-force Critical
03 TC-003 Auth / 2FA Enable TOTP and log in with one-time code High
04 TC-004 Users Admin creates and deactivates a user High
05 TC-005 API Tokens Create and revoke a personal API token High
06 TC-006 Projects Create a new project Critical
07 TC-007 Projects Update max_parallel_tasks and observe queueing Medium
08 TC-008 Projects Backup and restore a project High
09 TC-009 Projects Delete project only after dependents are removed High
10 TC-010 Repositories Add Git repository with SSH deploy key Critical
11 TC-011 Repositories Add Git repository with HTTPS token High
12 TC-012 Key Store Create SSH key and use it for inventory access Critical
13 TC-013 Key Store Create login/password key and bind become user High
14 TC-014 Key Store Bind HashiCorp Vault secret storage Medium
15 TC-015 Inventory Static inventory with two host groups Critical
16 TC-016 Inventory File-based Ansible inventory from repo High
17 TC-017 Inventory Terraform workspace inventory binding Medium
18 TC-018 Variable Groups JSON extra-vars + ENV vars + secret High
19 TC-019 Variable Groups TF_VAR_ secrets reach Terraform plan High
20 TC-020 Templates Create an Ansible task template and run it Critical
21 TC-021 Templates Build โ†’ Deploy template chain with artifact version High
22 TC-022 Templates Survey variables: required, enum, default High
23 TC-023 Templates Override branch, limit, tags at task launch Medium
24 TC-024 Tasks Stop a running task and verify status stopped High
25 TC-025 Schedules Create a cron schedule and validate next run High
26 TC-026 Schedules One-shot run_at schedule with delete-after-run Medium
27 TC-027 Runners Register a remote runner via registration token High
28 TC-028 Runners Tag-scoped task routing to matching runner Medium
29 TC-029 Integrations GitHub webhook + HMAC + matcher triggers template High
30 TC-030 RBAC Invited user with Task Runner role cannot edit 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):

text
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:

text
$ 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__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= 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> `` 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 ---