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
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
- Projects is a collection of related resources, configurations, and tasks.
- Task Templates are reusable definitions of tasks that can be executed on demand or scheduled.
- Task is a specific instance of a job or operation executed by Semaphore.
- Schedules allow you to automate task execution at specified times or intervals.
- Inventory is a collection of target hosts (servers, virtual machines, containers, etc.) on which tasks will be executed.
- 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
- Deploy a VM from a marketplace:
- Snap
- Binary file
- Debian or RPM package
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:latestWe 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.
- Ebdruplab โ Ansible Collections — Ansible modules and a role for managing Semaphore.
- SemaphoreUI MCP Server — A Model Context Protocol (MCP) server that provides AI assistants with powerful automation capabilities for SemaphoreUI.
- Terraform SemaphoreUI Provider — Manage Semaphore UI resources using Terraform.
- PSSemaphore — A PowerShell module designed to work against the Ansible Semaphore REST API.
Contribution
- Contribution Guide
- Dev Container (default user
admin/changeme)
License
MIT ยฉ Denis Gukov
Overview (`web/README.md`)
web
Project setup
npm installCompiles and hot-reloads for development
npm run serveCompiles and minifies for production
npm run buildRun your unit tests
npm run test:unitLints and fixes files
npm run lintCustomize configuration
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.
docker-compose -f deployment/compose/server/base.yml -f deployment/compose/server/build.yml upImage
This simply downloads the defined image from DockerHub and starts/configures it
properly based on the integrated bootstrapping scripts.
docker-compose -f deployment/compose/server/base.yml -f deployment/compose/server/image.yml upConfig
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.
docker-compose <server from above> -f deployment/compose/server/config.yml upRunner
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:
Global registration token โ set
runner_registration_tokenin 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.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 tosemaphore 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.
docker-compose <server from above> -f deployment/compose/runner/base.yml -f deployment/compose/runner/build.yml upImage
This simply downloads the defined image from DockerHub and starts/configures it
properly based on the integrated bootstrapping scripts.
docker-compose <server from above> -f deployment/compose/runner/base.yml -f deployment/compose/runner/image.yml upConfig
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.
docker-compose <runner from above> -f deployment/compose/runner/config.yml upDatabase
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.
docker-compose <server/runner from above> -f deployment/compose/store/sqlite.yml upSQLite
This simply configures a named volume for the SQLite storage used as a database
backend.
docker-compose <server/runner from above> -f deployment/compose/store/sqlite.yml upMariaDB
This simply starts an additional container for a MariaDB instance used as a
database backend including the required credentials.
docker-compose <server/runner from above> -f deployment/compose/store/mariadb.yml upMySQL
This simply starts an additional container for a MySQL instance used as a
database backend including the required credentials.
docker-compose <server/runner from above> -f deployment/compose/store/mysql.yml upPostgreSQL
This simply starts an additional container for a PostgreSQL instance used as a
database backend including the required credentials.
docker-compose <server/runner from above> -f deployment/compose/store/postgres.yml upCleanup
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:
task docker:build
task docker:pushIf 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 tosemaphoreuiDOCKER_SERVER: Define a different name for the server image, defaults tosemaphoreDOCKER_RUNNER: Define a different name for the runner image, defaults torunnerDOCKER_CMD: Use another command to build the image, defaults todocker
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).
task docker:testOverview (`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 withjournalctl -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:
SEMAPHORE_CONFIG=/etc/semaphore/config.json
SEMAPHORE_LOG_LEVEL=DEBUG
SEMAPHORE_DEBUG_FILTER=runner,task_poolAfter editing the env file, reload and restart:
sudo systemctl daemon-reload
sudo systemctl restart semaphore.service
journalctl -u semaphore.service -fBoltDB removed in 2.19: Configs with
"dialect": "bolt"will not start. Usesqlite,mysql, orpostgresinstead.
Overview (`examples/authentik_ldap/README.md`)
Semaphore with Authentik LDAP example
- 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 - To start the initial setup, navigate to http://localhsot:9000/if/flow/initial-setup/.
There you are prompted to set a password for theakadminuser (the default user). - Set up LDAP in Athentik according to the video tutorial Authentik - LDAP Generic Setup.
- Set up Athentik LDAP and Semaphore containers:
Copy
AUTHENTIK_TOKENto clipboard.Open
.envfile and:- Paste copied value after
AUTHENTIK_TOKEN= - Enter your
ldapserviceuser password afterSEMAPHORE_LDAP_BIND_PASSWORD=
- Paste copied value after
Down and up the stack to apply changes:
textdocker-compose down docker-compose up -d
- Create new Semaphore project:
- Open http://localhost:3000
- Login as
ldapservice - Create demo project
Overview (`examples/openldap/README.md`)
Semaphore with OpenLDAP example
Start stack by command:
textdocker-compose up -dCreate new LDAP user:
- Open https://localhost:6443
- Login as
cn=admin,dc=example,dc=orgwith passwordadmin - Create new user
john
Create new Semaphore project:
- Open http://localhost:3000
- Login as
john - 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
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 inputsExamples:
$ 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" (expectsuccess, 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: parsedk=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'sINPUT 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 secrettoken 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 beforestand.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" (ansibleplaybooks/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 withtask_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 projectPID (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 theprocess: 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 isENV_FILE in the script's settings block. Credentials are never copied into this folder.
Usage
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 / mariadbLog
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=1in 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=0in 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 standhttp://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